Attempt to fix note resizing with React keys and Muuri sync

This commit is contained in:
2026-01-24 19:52:13 +01:00
parent d59ec592eb
commit 8e35780717
48 changed files with 3369 additions and 279 deletions

View File

@@ -102,7 +102,7 @@ export function AIAssistantActionBar({
onClick={() => handleAction(onShorten)}
disabled={disabled}
>
<Scissors className="h-3 w-3 mr-1 text-blue-600 dark:text-blue-400" />
<Scissors className="h-3 w-3 mr-1 text-primary dark:text-primary-foreground" />
{t('ai.shorten')}
</Button>
)}

View File

@@ -36,7 +36,7 @@ export function ComparisonModal({
const getNoteColor = (index: number) => {
const colors = [
'border-blue-200 dark:border-blue-800 hover:border-blue-300 dark:hover:border-blue-700',
'border-primary/20 dark:border-primary/30 hover:border-primary/30 dark:hover:border-primary/40',
'border-purple-200 dark:border-purple-800 hover:border-purple-300 dark:hover:border-purple-700',
'border-green-200 dark:border-green-800 hover:border-green-300 dark:hover:border-green-700'
]
@@ -45,7 +45,7 @@ export function ComparisonModal({
const getTitleColor = (index: number) => {
const colors = [
'text-blue-600 dark:text-blue-400',
'text-primary dark:text-primary-foreground',
'text-purple-600 dark:text-purple-400',
'text-green-600 dark:text-green-400'
]

View File

@@ -31,7 +31,7 @@ const NOTEBOOK_ICONS = [
]
const NOTEBOOK_COLORS = [
{ name: 'Blue', value: '#3B82F6', bg: 'bg-blue-500' },
{ name: 'Slate', value: '#64748B', bg: 'bg-slate-500' },
{ name: 'Purple', value: '#8B5CF6', bg: 'bg-purple-500' },
{ name: 'Red', value: '#EF4444', bg: 'bg-red-500' },
{ name: 'Orange', value: '#F59E0B', bg: 'bg-orange-500' },

View File

@@ -245,7 +245,7 @@ export function Header({
const NavItem = ({ href, icon: Icon, label, active, onClick }: any) => {
const content = (
<>
<Icon className={cn("h-5 w-5", active && "fill-current text-blue-900")} />
<Icon className={cn("h-5 w-5", active && "fill-current text-primary")} />
{label}
</>
)
@@ -257,7 +257,7 @@ export function Header({
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2 text-left",
active
? "bg-blue-100 text-blue-900"
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
style={{ minHeight: '44px' }}
@@ -275,7 +275,7 @@ export function Header({
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-blue-100 text-blue-900"
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
style={{ minHeight: '44px' }}
@@ -297,7 +297,7 @@ export function Header({
{/* Logo MEMENTO */}
<div className="flex items-center gap-3 text-slate-900 dark:text-white cursor-pointer group" onClick={() => router.push('/')}>
<div className="size-8 bg-blue-500 rounded-lg flex items-center justify-center text-white shadow-sm group-hover:shadow-md transition-all">
<div className="size-8 bg-primary rounded-lg flex items-center justify-center text-primary-foreground shadow-sm group-hover:shadow-md transition-all">
<StickyNote className="w-5 h-5" />
</div>
<h2 className="text-xl font-bold leading-tight tracking-tight">MEMENTO</h2>
@@ -333,7 +333,7 @@ export function Header({
{/* User Avatar Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex items-center justify-center bg-center bg-no-repeat bg-cover rounded-full size-10 ring-2 ring-white dark:ring-slate-700 cursor-pointer shadow-sm hover:shadow-md transition-shadow bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-200"
<div className="flex items-center justify-center bg-center bg-no-repeat bg-cover rounded-full size-10 ring-2 ring-white dark:ring-slate-700 cursor-pointer shadow-sm hover:shadow-md transition-shadow bg-primary/10 dark:bg-primary/20 text-primary dark:text-primary-foreground"
style={currentUser?.image ? { backgroundImage: `url(${currentUser?.image})` } : undefined}>
{!currentUser?.image && (
<span className="text-sm font-semibold">

View File

@@ -100,7 +100,7 @@ export function LabelSelector({
>
<div className={cn(
"h-4 w-4 border rounded flex items-center justify-center transition-colors",
isSelected ? "bg-blue-600 border-blue-600 text-white" : "border-gray-400"
isSelected ? "bg-primary border-primary text-primary-foreground" : "border-gray-400"
)}>
{isSelected && <Check className="h-3 w-3" />}
</div>

View File

@@ -19,7 +19,7 @@ export function MarkdownContent({ content, className = '' }: MarkdownContentProp
rehypePlugins={[rehypeKatex]}
components={{
a: ({ node, ...props }) => (
<a {...props} className="text-blue-500 hover:underline" target="_blank" rel="noopener noreferrer" />
<a {...props} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer" />
)
}}
>

View File

@@ -26,7 +26,7 @@
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
/* height: auto - let content determine height */
box-sizing: border-box;
}
@@ -45,16 +45,22 @@
}
/* Note Size Styles - Desktop Default */
.masonry-item[data-size="small"],
.note-card[data-size="small"] {
min-height: 150px !important;
min-height: 150px;
height: auto !important;
}
.masonry-item[data-size="medium"],
.note-card[data-size="medium"] {
min-height: 200px !important;
min-height: 200px;
height: auto !important;
}
.masonry-item[data-size="large"],
.note-card[data-size="large"] {
min-height: 300px !important;
min-height: 300px;
height: auto !important;
}
/* Drag State Styles - Clean and flat behavior requested by user */
@@ -124,16 +130,22 @@
}
/* Smaller note sizes on mobile */
.masonry-item[data-size="small"],
.masonry-item-content .note-card[data-size="small"] {
min-height: 120px;
height: auto !important;
}
.masonry-item[data-size="medium"],
.masonry-item-content .note-card[data-size="medium"] {
min-height: 160px;
height: auto !important;
}
.masonry-item[data-size="large"],
.masonry-item-content .note-card[data-size="large"] {
min-height: 240px;
height: auto !important;
}
/* Reduced drag effect on mobile */
@@ -183,7 +195,7 @@
.masonry-item,
.masonry-item-content,
.note-card {
transition-property: transform, box-shadow, opacity;
transition-property: box-shadow, opacity;
transition-duration: 0.2s;
transition-timing-function: ease-out;
}

View File

@@ -20,12 +20,13 @@ interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onNoteSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onNoteSizeChange, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
return (
@@ -43,13 +44,12 @@ const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragSt
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
onResize={onResize}
onSizeChange={(newSize) => onNoteSizeChange(note.id, newSize)}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
@@ -57,6 +57,22 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Local state for notes with dynamic size updates
// This allows size changes to propagate immediately without waiting for server
const [localNotes, setLocalNotes] = useState<Note[]>(notes);
// Sync localNotes when parent notes prop changes
useEffect(() => {
setLocalNotes(notes);
}, [notes]);
// Callback for when a note's size changes - update local state immediately
const handleNoteSizeChange = useCallback((noteId: string, newSize: 'small' | 'medium' | 'large') => {
setLocalNotes(prevNotes =>
prevNotes.map(n => n.id === noteId ? { ...n, size: newSize } : n)
);
}, []);
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
@@ -70,14 +86,14 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered notes (order comes from array)
// Memoize filtered notes from localNotes (which includes dynamic size updates)
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned),
[notes]
() => localNotes.filter(n => n.isPinned),
[localNotes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned),
[notes]
() => localNotes.filter(n => !n.isPinned),
[localNotes]
);
const handleDragEnd = useCallback(async (grid: any) => {
@@ -152,7 +168,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
console.log(`[Masonry] Container width: ${containerWidth}px, Columns: ${columns}, Item width: ${itemWidth}px`);
const layoutOptions = {
dragEnabled: true,
@@ -279,6 +295,13 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
requestAnimationFrame(() => {
syncGridItems(pinnedMuuri.current, pinnedGridRef, pinnedNotes);
syncGridItems(othersMuuri.current, othersGridRef, othersNotes);
// CRITICAL: Force a second layout after CSS transitions (padding/height changes) complete
// NoteCard has a 200ms transition. We wait 300ms to be safe.
setTimeout(() => {
if (pinnedMuuri.current) pinnedMuuri.current.refreshItems().layout();
if (othersMuuri.current) othersMuuri.current.refreshItems().layout();
}, 300);
});
}, [pinnedNotes, othersNotes]); // Re-run when notes change
@@ -295,7 +318,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const containerWidth = entries[0]?.contentRect.width || window.innerWidth - 32;
const columns = calculateColumns(containerWidth);
console.log(`[Masonry Resize] Width: ${containerWidth}px, Columns: ${columns}`);
// Apply dimensions to both grids
applyItemDimensions(pinnedMuuri.current, containerWidth);
@@ -331,10 +354,11 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
key={`${note.id}-${note.size}`}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onNoteSizeChange={handleNoteSizeChange}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
@@ -352,10 +376,11 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
key={`${note.id}-${note.size}`}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onNoteSizeChange={handleNoteSizeChange}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
@@ -373,39 +398,6 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
/>
)}
<style jsx global>{`
.masonry-container {
width: 100%;
}
.masonry-item {
display: block;
position: absolute;
z-index: 1;
box-sizing: border-box;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
opacity: 0.8;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
}
/* Ensure proper box-sizing for all elements in the grid */
.masonry-item *,
.masonry-item-content * {
box-sizing: border-box;
}
`}</style>
</div>
);
}

View File

@@ -173,7 +173,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
}}
className="cursor-pointer border dark:border-zinc-700 rounded-lg p-4 hover:border-amber-300 dark:hover:border-amber-700 transition-colors"
>
<h3 className="font-semibold text-blue-600 dark:text-blue-400 mb-2">
<h3 className="font-semibold text-primary dark:text-primary-foreground mb-2">
{note1Title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
@@ -278,7 +278,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
{/* Connected notes */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Badge variant="outline" className="border-blue-200 text-blue-700 dark:border-blue-900 dark:text-blue-300">
<Badge variant="outline" className="border-primary/20 text-primary dark:border-primary/30 dark:text-primary-foreground">
{note1Title}
</Badge>
<ArrowRight className="h-3 w-3 text-gray-400" />

View File

@@ -111,7 +111,9 @@ export function NoteActions({
{(['small', 'medium', 'large'] as const).map((size) => (
<DropdownMenuItem
key={size}
onClick={() => onSizeChange(size)}
onClick={(e) => {
onSizeChange?.(size);
}}
className={cn(
"capitalize",
currentSize === size && "bg-accent"

View File

@@ -86,6 +86,8 @@ interface NoteCardProps {
isDragOver?: boolean
onDragStart?: (noteId: string) => void
onDragEnd?: () => void
onResize?: () => void
onSizeChange?: (newSize: 'small' | 'medium' | 'large') => void
}
// Helper function to get initials from name
@@ -116,7 +118,15 @@ function getAvatarColor(name: string): string {
return colors[hash % colors.length]
}
export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, onDragEnd }: NoteCardProps) {
export function NoteCard({
note,
onEdit,
onDragStart,
onDragEnd,
isDragging,
onResize,
onSizeChange
}: NoteCardProps) {
const router = useRouter()
const searchParams = useSearchParams()
const { refreshLabels } = useLabels()
@@ -138,7 +148,14 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on
setShowNotebookMenu(false)
// No need for router.refresh() - triggerRefresh() is already called in moveNoteToNotebookOptimistic
}
const colorClasses = NOTE_COLORS[note.color as NoteColor] || NOTE_COLORS.default
// Optimistic UI state for instant feedback
const [optimisticNote, addOptimisticNote] = useOptimistic(
note,
(state, newProps: Partial<Note>) => ({ ...state, ...newProps })
)
const colorClasses = NOTE_COLORS[optimisticNote.color as NoteColor] || NOTE_COLORS.default
// Check if this note is currently open in the editor
const isNoteOpenInEditor = searchParams.get('note') === note.id
@@ -148,12 +165,6 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on
comparisonNotes && comparisonNotes.length > 0 ? comparisonNotes : null
)
// Optimistic UI state for instant feedback
const [optimisticNote, addOptimisticNote] = useOptimistic(
note,
(state, newProps: Partial<Note>) => ({ ...state, ...newProps })
)
const currentUserId = session?.user?.id
const canManageCollaborators = currentUserId && note.userId && currentUserId === note.userId
const isSharedNote = currentUserId && note.userId && currentUserId !== note.userId
@@ -226,10 +237,15 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on
}
const handleSizeChange = async (size: 'small' | 'medium' | 'large') => {
startTransition(async () => {
addOptimisticNote({ size })
await updateSize(note.id, size)
})
// Notify parent of size change so it can update its state
onSizeChange?.(size)
// Trigger layout refresh
onResize?.()
setTimeout(() => onResize?.(), 300)
// Update server in background
updateSize(note.id, size)
}
const handleCheckItem = async (checkItemId: string) => {
@@ -267,12 +283,23 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on
if (isDeleting) return null
const getMinHeight = (size?: string) => {
switch (size) {
case 'medium': return '200px'
case 'large': return '300px'
default: return '150px' // small
}
}
return (
<Card
data-testid="note-card"
data-draggable="true"
data-note-id={note.id}
data-size={note.size}
data-size={optimisticNote.size}
style={{ minHeight: getMinHeight(optimisticNote.size) }}
draggable={true}
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', note.id)
@@ -560,7 +587,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on
isPinned={optimisticNote.isPinned}
isArchived={optimisticNote.isArchived}
currentColor={optimisticNote.color}
currentSize={optimisticNote.size}
currentSize={optimisticNote.size as 'small' | 'medium' | 'large'}
onTogglePin={handleTogglePin}
onToggleArchive={handleToggleArchive}
onColorChange={handleColorChange}

View File

@@ -550,7 +550,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{readOnly ? t('notes.view') : t('notes.edit')}</h2>
{readOnly && (
<Badge variant="secondary" className="bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
<Badge variant="secondary" className="bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground">
{t('notes.readOnly')}
</Badge>
)}
@@ -607,7 +607,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
<div className="p-2 flex-1 min-w-0 flex flex-col justify-center">
<h4 className="font-medium text-sm truncate">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 truncate">{link.description}</p>}
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-500 truncate hover:underline block mt-1">
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-primary truncate hover:underline block mt-1">
{new URL(link.url).hostname}
</a>
</div>
@@ -636,7 +636,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
setIsMarkdown(!isMarkdown)
if (isMarkdown) setShowMarkdownPreview(false)
}}
className={cn("h-7 text-xs", isMarkdown && "text-blue-600")}
className={cn("h-7 text-xs", isMarkdown && "text-primary")}
>
<FileText className="h-3 w-3 mr-1" />
{isMarkdown ? t('notes.markdownOn') : t('notes.markdownOff')}
@@ -824,7 +824,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
size="sm"
onClick={() => setShowReminderDialog(true)}
title={t('notes.setReminder')}
className={currentReminder ? "text-blue-600" : ""}
className={currentReminder ? "text-primary" : ""}
>
<Bell className="h-4 w-4" />
</Button>

View File

@@ -645,7 +645,7 @@ export function NoteInput({ onNoteCreated, defaultExpanded = false, forceExpande
<div className="p-2 flex-1 min-w-0 flex flex-col justify-center">
<h4 className="font-medium text-sm truncate">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 truncate">{link.description}</p>}
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-500 truncate hover:underline block mt-1">
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-primary truncate hover:underline block mt-1">
{new URL(link.url).hostname}
</a>
</div>
@@ -781,7 +781,7 @@ export function NoteInput({ onNoteCreated, defaultExpanded = false, forceExpande
size="icon"
className={cn(
"h-8 w-8",
currentReminder && "text-blue-600"
currentReminder && "text-primary"
)}
title={t('notes.remindMe')}
onClick={handleReminderOpen}
@@ -799,7 +799,7 @@ export function NoteInput({ onNoteCreated, defaultExpanded = false, forceExpande
size="icon"
className={cn(
"h-8 w-8",
isMarkdown && "text-blue-600"
isMarkdown && "text-primary"
)}
onClick={() => {
setIsMarkdown(!isMarkdown)

View File

@@ -103,7 +103,7 @@ export function NotebookSuggestionToast({
<div
className={cn(
'fixed bottom-4 right-4 z-50 max-w-md bg-white dark:bg-zinc-800',
'border border-blue-200 dark:border-blue-800 rounded-lg shadow-lg',
'border border-border dark:border-border rounded-lg shadow-lg',
'p-4 animate-in slide-in-from-bottom-4 fade-in duration-300',
'transition-all duration-300'
)}
@@ -111,8 +111,8 @@ export function NotebookSuggestionToast({
<div className="flex items-start gap-3">
{/* Icon */}
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<FolderOpen className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div className="w-10 h-10 rounded-full bg-primary/10 dark:bg-primary/20 flex items-center justify-center">
<FolderOpen className="w-5 h-5 text-primary dark:text-primary-foreground" />
</div>
</div>
@@ -131,7 +131,7 @@ export function NotebookSuggestionToast({
{/* Move button */}
<button
onClick={handleMoveToNotebook}
className="px-3 py-1.5 text-xs font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors"
className="px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
{t('notebookSuggestion.move')}
</button>

View File

@@ -158,8 +158,8 @@ export function NotebooksList() {
onDragLeave={handleDragLeave}
className={cn(
"flex flex-col mr-2 rounded-r-full overflow-hidden transition-all",
!notebook.color && "bg-blue-50 dark:bg-blue-900/20",
isDragOver && "ring-2 ring-blue-500 ring-dashed"
!notebook.color && "bg-primary/10 dark:bg-primary/20",
isDragOver && "ring-2 ring-primary ring-dashed"
)}
style={notebook.color ? { backgroundColor: `${notebook.color}20` } : undefined}
>
@@ -167,11 +167,11 @@ export function NotebooksList() {
<div className="pointer-events-auto flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-4 min-w-0">
<NotebookIcon
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-blue-700 dark:text-blue-100")}
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
/>
<span
className={cn("text-sm font-medium tracking-wide truncate max-w-[120px]", !notebook.color && "text-blue-700 dark:text-blue-100")}
className={cn("text-sm font-medium tracking-wide truncate max-w-[120px]", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
>
{notebook.name}
@@ -179,7 +179,7 @@ export function NotebooksList() {
</div>
<button
onClick={() => handleToggleExpand(notebook.id)}
className={cn("transition-colors p-1 flex-shrink-0", !notebook.color && "text-blue-600 hover:text-blue-800 dark:text-blue-200 dark:hover:text-blue-100")}
className={cn("transition-colors p-1 flex-shrink-0", !notebook.color && "text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80")}
style={notebook.color ? { color: notebook.color } : undefined}
>
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />

View File

@@ -126,14 +126,14 @@ export function NotificationPanel() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
<div className="px-4 py-3 border-b bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20">
<div className="px-4 py-3 border-b bg-gradient-to-r from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/15">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<Bell className="h-4 w-4 text-primary dark:text-primary-foreground" />
<span className="font-semibold text-sm">{t('nav.aiSettings')}</span>
</div>
{pendingCount > 0 && (
<Badge className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
{pendingCount}
</Badge>
)}
@@ -142,7 +142,7 @@ export function NotificationPanel() {
{isLoading ? (
<div className="p-6 text-center text-sm text-muted-foreground">
<div className="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-2" />
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full mx-auto mb-2" />
{t('general.loading')}
</div>
) : requests.length === 0 ? (
@@ -171,7 +171,7 @@ export function NotificationPanel() {
</div>
<Badge
variant="secondary"
className="text-xs capitalize bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 border-0"
className="text-xs capitalize bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground border-0"
>
{request.permission}
</Badge>

View File

@@ -73,11 +73,11 @@ function CompactCard({
{/* Subtle left accent - colored based on recency */}
<div className={cn(
"absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
isFirstNote
? "bg-gradient-to-b from-blue-500 to-indigo-500"
isFirstNote
? "bg-gradient-to-b from-primary to-primary/70"
: index === 1
? "bg-blue-400 dark:bg-blue-500"
: "bg-gray-300 dark:bg-gray-600"
? "bg-primary/80 dark:bg-primary/70"
: "bg-muted dark:bg-muted/60"
)} />
{/* Content with left padding for accent line */}
@@ -105,7 +105,7 @@ function CompactCard({
<div className="flex items-center gap-1.5">
{/* Notebook indicator */}
{note.notebookId && (
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 dark:bg-blue-400" title="In notebook" />
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title="In notebook" />
)}
{/* Labels indicator */}
{note.labels && note.labels.length > 0 && (

View File

@@ -53,8 +53,8 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
"flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 transition-colors",
"text-sm font-medium tracking-wide",
active
? "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-100"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50"
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground"
: "text-muted-foreground hover:bg-muted/50 dark:hover:bg-muted/30"
)}
>
<Icon className={cn("w-5 h-5", active ? "fill-current" : "")} />

View File

@@ -9,10 +9,10 @@ interface ThemeInitializerProps {
export function ThemeInitializer({ theme, fontSize }: ThemeInitializerProps) {
useEffect(() => {
console.log('[ThemeInitializer] Received theme:', theme)
// Helper to apply theme
const applyTheme = (t?: string) => {
console.log('[ThemeInitializer] Applying theme:', t)
if (!t) return
const root = document.documentElement
@@ -66,7 +66,7 @@ export function ThemeInitializer({ theme, fontSize }: ThemeInitializerProps) {
const localTheme = localStorage.getItem('theme-preference')
const effectiveTheme = localTheme || theme
console.log('[ThemeInitializer] Local theme:', localTheme, '| Server theme:', theme, '| Using:', effectiveTheme)
applyTheme(effectiveTheme)