fix: improve note interactions and markdown LaTeX support

## Bug Fixes

### Note Card Actions
- Fix broken size change functionality (missing state declaration)
- Implement React 19 useOptimistic for instant UI feedback
- Add startTransition for non-blocking updates
- Ensure smooth animations without page refresh
- All note actions now work: pin, archive, color, size, checklist

### Markdown LaTeX Rendering
- Add remark-math and rehype-katex plugins
- Support inline equations with dollar sign syntax
- Support block equations with double dollar sign syntax
- Import KaTeX CSS for proper styling
- Equations now render correctly instead of showing raw LaTeX

## Technical Details

- Replace undefined currentNote references with optimistic state
- Add optimistic updates before server actions for instant feedback
- Use router.refresh() in transitions for smart cache invalidation
- Install remark-math, rehype-katex, and katex packages

## Testing

- Build passes successfully with no TypeScript errors
- Dev server hot-reloads changes correctly
This commit is contained in:
2026-01-09 22:13:49 +01:00
parent 3c4b9d6176
commit 640fcb26f7
218 changed files with 51363 additions and 902 deletions

View File

@@ -5,27 +5,23 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Filter } from 'lucide-react'
import { LABEL_COLORS } from '@/lib/types'
import { Filter, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLabels } from '@/context/LabelContext'
import { LabelBadge } from './label-badge'
interface LabelFilterProps {
selectedLabels: string[]
selectedColor?: string | null
onFilterChange: (labels: string[]) => void
onColorChange?: (color: string | null) => void
}
export function LabelFilter({ selectedLabels, selectedColor, onFilterChange, onColorChange }: LabelFilterProps) {
const { labels, loading, getLabelColor } = useLabels()
export function LabelFilter({ selectedLabels, onFilterChange }: LabelFilterProps) {
const { labels, loading } = useLabels()
const [allLabelNames, setAllLabelNames] = useState<string[]>([])
useEffect(() => {
@@ -43,15 +39,6 @@ export function LabelFilter({ selectedLabels, selectedColor, onFilterChange, onC
const handleClearAll = () => {
onFilterChange([])
onColorChange?.(null)
}
const handleColorFilter = (color: string) => {
if (selectedColor === color) {
onColorChange?.(null)
} else {
onColorChange?.(color)
}
}
if (loading || allLabelNames.length === 0) return null
@@ -86,56 +73,35 @@ export function LabelFilter({ selectedLabels, selectedColor, onFilterChange, onC
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Color Filter */}
<div className="p-2">
<p className="text-xs font-medium mb-2 text-gray-600 dark:text-gray-400">Filter by Color</p>
<div className="grid grid-cols-3 gap-2">
{Object.entries(LABEL_COLORS).map(([colorName, colorClasses]) => {
const isSelected = selectedColor === colorName
const labelCount = labels.filter((l: any) => l.color === colorName).length
return (
<button
key={colorName}
onClick={() => handleColorFilter(colorName)}
className={cn(
'flex items-center gap-2 p-2 rounded-lg border-2 transition-all hover:scale-105',
isSelected ? 'ring-2 ring-blue-500' : 'border-gray-300 dark:border-gray-600'
)}
>
<div
className={cn(
'w-6 h-6 rounded-full border-2',
colorClasses.bg,
isSelected ? 'border-blue-500 dark:border-blue-400' : colorClasses.border
)}
/>
<span className="text-xs ml-2">{labelCount}</span>
</button>
)
})}
</div>
</div>
<DropdownMenuSeparator />
{/* Label Filters */}
{!loading && allLabelNames.map((labelName: string) => {
const isSelected = selectedLabels.includes(labelName)
const isColorFiltered = selectedColor && selectedColor !== 'gray'
<div className="max-h-64 overflow-y-auto px-1 pb-1">
{!loading && allLabelNames.map((labelName: string) => {
const isSelected = selectedLabels.includes(labelName)
return (
<DropdownMenuCheckboxItem
key={labelName}
checked={isSelected && !isColorFiltered}
onCheckedChange={() => handleToggleLabel(labelName)}
>
<LabelBadge
label={labelName}
isDisabled={!!isColorFiltered}
/>
</DropdownMenuCheckboxItem>
)
})}
return (
<div
key={labelName}
onClick={(e) => {
e.preventDefault()
handleToggleLabel(labelName)
}}
className={cn(
"flex items-center gap-2 p-2 rounded-sm cursor-pointer text-sm hover:bg-accent hover:text-accent-foreground"
)}
>
<div className={cn(
"flex h-4 w-4 items-center justify-center rounded border border-primary",
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
)}>
<Check className="h-3 w-3" />
</div>
<LabelBadge
label={labelName}
/>
</div>
)
})}
</div>
</DropdownMenuContent>
</DropdownMenu>