Attempt to fix note resizing with React keys and Muuri sync
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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" : "")} />
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user