Files
resolutionflow/frontend/src/components/library/AddToFolderMenu.tsx
chihlasm d365c38b61 chore: Tailwind CSS v3 → v4 migration (#99)
* chore: run Tailwind v4 upgrade tool (Phase 1)

- Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss
- Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css
- Replaced @tailwind directives with @import 'tailwindcss'
- Added @custom-variant dark, @utility blocks for custom utilities
- Updated class names across 128 files (shadow-sm → shadow-xs, etc.)
- Removed autoprefixer (built into v4)
- Added migration plan doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: switch from @tailwindcss/postcss to @tailwindcss/vite (Phase 2)

- Replaced @tailwindcss/postcss with @tailwindcss/vite plugin
- Deleted postcss.config.js (no longer needed)
- Tailwind now runs as a native Vite plugin for faster HMR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: convert to OKLCH colors, move keyframes into @theme (Phase 3-4)

- Replaced all HSL color indirection with direct OKLCH values in @theme
- Moved all keyframes inside @theme block (v4 pattern)
- Eliminated hsl(var(--x)) double-indirection across 17 component files
- Replaced hsl() inline styles with var(--color-*) theme references
- Cleaned up redundant rdp-* utility blocks
- Fixed @custom-variant dark syntax to use :where()
- Added sidebar/glass/shadow vars as OKLCH in :root

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:10:44 -05:00

154 lines
4.7 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import { FolderPlus, Check, Plus } from 'lucide-react'
import { foldersApi } from '@/api/folders'
import type { FolderListItem } from '@/types'
import { cn } from '@/lib/utils'
interface AddToFolderMenuProps {
treeId: string
onFolderCreated?: () => void
}
export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProps) {
const [isOpen, setIsOpen] = useState(false)
const [folders, setFolders] = useState<FolderListItem[]>([])
const [treeFolderIds, setTreeFolderIds] = useState<Set<string>>(new Set())
const [isLoading, setIsLoading] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
loadFoldersAndAssignments()
}
}, [isOpen, treeId])
// Close on outside click
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [isOpen])
const loadFoldersAndAssignments = async () => {
setIsLoading(true)
try {
const foldersData = await foldersApi.list()
setFolders(foldersData)
// Check which folders contain this tree
const folderIds = new Set<string>()
for (const folder of foldersData) {
try {
const treeIds = await foldersApi.getTreeIds(folder.id)
if (treeIds.includes(treeId)) {
folderIds.add(folder.id)
}
} catch {
// Ignore errors for individual folder checks
}
}
setTreeFolderIds(folderIds)
} catch (err) {
console.error('Failed to load folders:', err)
} finally {
setIsLoading(false)
}
}
const toggleFolder = async (folderId: string) => {
try {
if (treeFolderIds.has(folderId)) {
await foldersApi.removeTree(folderId, treeId)
setTreeFolderIds((prev) => {
const next = new Set(prev)
next.delete(folderId)
return next
})
} else {
await foldersApi.addTree(folderId, treeId)
setTreeFolderIds((prev) => new Set([...prev, folderId]))
}
// Dispatch event to refresh folder counts
window.dispatchEvent(new Event('folder-changed'))
} catch (err) {
console.error('Failed to toggle folder:', err)
}
}
return (
<div ref={menuRef} className="relative">
<button
onClick={(e) => {
e.stopPropagation()
setIsOpen(!isOpen)
}}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Add to folder"
aria-label="Add to folder"
>
<FolderPlus className="h-4 w-4" />
</button>
{isOpen && (
<div
className={cn(
'absolute right-0 top-full z-20 mt-1 w-48 rounded-md border border-border',
'bg-card backdrop-blur-xs py-1 shadow-lg'
)}
>
{isLoading ? (
<div className="px-3 py-2 text-sm text-muted-foreground">Loading...</div>
) : folders.length === 0 ? (
<div className="px-3 py-2 text-sm text-muted-foreground">No folders yet</div>
) : (
folders.map((folder) => (
<button
key={folder.id}
onClick={(e) => {
e.stopPropagation()
toggleFolder(folder.id)
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<div
className="h-3 w-3 rounded-sm"
style={{ backgroundColor: folder.color }}
/>
<span className="flex-1 truncate text-left">{folder.name}</span>
{treeFolderIds.has(folder.id) && (
<Check className="h-4 w-4 text-foreground" />
)}
</button>
))
)}
<div className="border-t border-border my-1" />
<button
onClick={(e) => {
e.stopPropagation()
setIsOpen(false)
onFolderCreated?.()
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Plus className="h-4 w-4" />
Create new folder
</button>
</div>
)}
</div>
)
}
export default AddToFolderMenu