3,200+ hardcoded color values replaced with CSS variable-backed Tailwind classes (bg-card, text-foreground, border-border, etc.). Enables light mode via CSS variable swap. Only syntax highlighting colors and intentional one-offs remain hardcoded (~15 values). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
214 lines
7.0 KiB
TypeScript
214 lines
7.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { categoriesApi } from '@/api/categories'
|
|
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
|
import { TagInput } from '@/components/common/TagInput'
|
|
import type { CategoryListItem } from '@/types'
|
|
import { cn } from '@/lib/utils'
|
|
import { Globe, Lock } from 'lucide-react'
|
|
|
|
export function TreeMetadataForm() {
|
|
const {
|
|
name,
|
|
description,
|
|
category,
|
|
categoryId,
|
|
tags,
|
|
isPublic,
|
|
setName,
|
|
setDescription,
|
|
setCategory,
|
|
setCategoryId,
|
|
setTags,
|
|
setIsPublic,
|
|
validationErrors,
|
|
} = useTreeEditorStore()
|
|
|
|
const [categories, setCategories] = useState<CategoryListItem[]>([])
|
|
const [customCategory, setCustomCategory] = useState(false)
|
|
|
|
// Load categories
|
|
useEffect(() => {
|
|
categoriesApi.list().then(setCategories).catch(console.error)
|
|
}, [])
|
|
|
|
const handleCategoryChange = (value: string) => {
|
|
if (value === '__custom__') {
|
|
setCustomCategory(true)
|
|
setCategory('')
|
|
setCategoryId(null)
|
|
} else if (value === '') {
|
|
setCustomCategory(false)
|
|
setCategory('')
|
|
setCategoryId(null)
|
|
} else {
|
|
setCustomCategory(false)
|
|
setCategoryId(value)
|
|
// Find category name for display
|
|
const cat = categories.find((c) => c.id === value)
|
|
if (cat) {
|
|
setCategory(cat.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
const nameError = validationErrors.find(
|
|
(e) => !e.nodeId && e.message.toLowerCase().includes('name')
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-4 bg-card border border-border rounded-2xl p-4">
|
|
<h2 className="text-sm font-semibold text-foreground">Tree Details</h2>
|
|
|
|
{/* Name */}
|
|
<div>
|
|
<label htmlFor="tree-name" className="block text-sm font-medium text-foreground">
|
|
Name <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
id="tree-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g., VDA Registration Troubleshooting"
|
|
className={cn(
|
|
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
|
'bg-card text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
|
nameError ? 'border-red-400' : 'border-border'
|
|
)}
|
|
/>
|
|
{nameError && <p className="mt-1 text-xs text-red-400">{nameError.message}</p>}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label htmlFor="tree-description" className="block text-sm font-medium text-foreground">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="tree-description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Brief description of what this tree troubleshoots..."
|
|
rows={2}
|
|
className={cn(
|
|
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
|
'bg-card text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div>
|
|
<label htmlFor="tree-category" className="block text-sm font-medium text-foreground">
|
|
Category
|
|
</label>
|
|
{!customCategory ? (
|
|
<select
|
|
id="tree-category"
|
|
value={categoryId || ''}
|
|
onChange={(e) => handleCategoryChange(e.target.value)}
|
|
className={cn(
|
|
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
|
'bg-card text-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
|
)}
|
|
>
|
|
<option value="">No category</option>
|
|
{categories.map((cat) => (
|
|
<option key={cat.id} value={cat.id}>
|
|
{cat.name}
|
|
{cat.account_id ? ' (Account)' : ''}
|
|
</option>
|
|
))}
|
|
<option value="__custom__">+ Add custom category</option>
|
|
</select>
|
|
) : (
|
|
<div className="mt-1 flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
placeholder="Enter new category"
|
|
className={cn(
|
|
'block min-w-0 flex-1 rounded-md border border-border px-3 py-2 text-sm',
|
|
'bg-card text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
|
)}
|
|
autoFocus
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setCustomCategory(false)
|
|
setCategory('')
|
|
setCategoryId(null)
|
|
}}
|
|
className="shrink-0 rounded-md border border-border px-2.5 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground">Tags</label>
|
|
<div className="mt-1">
|
|
<TagInput tags={tags} onChange={setTags} maxTags={10} placeholder="Add tags..." />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Visibility */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground">Visibility</label>
|
|
<div className="mt-2 flex gap-4">
|
|
<label
|
|
className={cn(
|
|
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
|
|
'transition-colors',
|
|
!isPublic ? 'border-primary/30 bg-accent text-foreground' : 'border-border text-muted-foreground hover:bg-accent/50'
|
|
)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="visibility"
|
|
checked={!isPublic}
|
|
onChange={() => setIsPublic(false)}
|
|
className="sr-only"
|
|
/>
|
|
<Lock className="h-4 w-4" />
|
|
<span className="text-sm">Private</span>
|
|
</label>
|
|
<label
|
|
className={cn(
|
|
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
|
|
'transition-colors',
|
|
isPublic ? 'border-primary/30 bg-accent text-foreground' : 'border-border text-muted-foreground hover:bg-accent/50'
|
|
)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="visibility"
|
|
checked={isPublic}
|
|
onChange={() => setIsPublic(true)}
|
|
className="sr-only"
|
|
/>
|
|
<Globe className="h-4 w-4" />
|
|
<span className="text-sm">Public</span>
|
|
</label>
|
|
</div>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{isPublic
|
|
? 'Anyone can view this tree'
|
|
: 'Only you and your team can view this tree'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default TreeMetadataForm
|