Merge pull request #18 from johndoe6345789/copilot/refactor-large-components

Refactor large components into focused modules
This commit is contained in:
2026-01-17 23:27:27 +00:00
committed by GitHub
10 changed files with 803 additions and 807 deletions

View File

@@ -8,10 +8,7 @@ import { Input } from '@/components/ui/input'
import {
BookOpen,
MapPin,
FileCode,
CheckCircle,
Clock,
Sparkle,
Code,
Database,
Tree,
@@ -28,6 +25,17 @@ import {
MagnifyingGlass,
GitBranch
} from '@phosphor-icons/react'
import {
FeatureItem,
AIFeatureCard,
RoadmapItem,
AgentFileItem,
IntegrationPoint,
CICDPlatformItem,
PipelineStageCard,
SassComponentItem,
AnimationItem
} from './DocumentationView/DocComponents'
export function DocumentationView() {
const [activeTab, setActiveTab] = useState('readme')
@@ -1894,182 +1902,3 @@ docker pull ghcr.io/<username>/<repo>:latest`}
</div>
)
}
function CICDPlatformItem({ name, file, description, features }: {
name: string
file: string
description: string
features: string[]
}) {
return (
<div className="space-y-3 border-l-2 border-accent pl-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<GitBranch size={18} className="text-accent" />
<h3 className="text-base font-semibold">{name}</h3>
</div>
<code className="text-xs text-muted-foreground font-mono">{file}</code>
<p className="text-sm text-foreground/90">{description}</p>
</div>
<div className="space-y-1">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
<ul className="space-y-1">
{features.map((feature, idx) => (
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
)
}
function PipelineStageCard({ stage, description, duration }: {
stage: string
description: string
duration: string
}) {
return (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-4 pb-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 flex-1">
<h4 className="font-semibold text-sm">{stage}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Badge variant="secondary" className="text-xs whitespace-nowrap">
{duration}
</Badge>
</div>
</CardContent>
</Card>
)
}
function SassComponentItem({ name, classes, description }: { name: string; classes: string[]; description: string }) {
return (
<div className="space-y-2 p-4 border rounded-lg bg-card">
<h4 className="font-semibold">{name}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="space-y-1">
{classes.map((cls, idx) => (
<code key={idx} className="text-xs font-mono text-accent block">{cls}</code>
))}
</div>
</div>
)
}
function AnimationItem({ name, description }: { name: string; description: string }) {
return (
<div className="space-y-1 p-3 border rounded-lg bg-card">
<code className="text-xs font-mono text-accent">{name}</code>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
)
}
function FeatureItem({ icon, title, description }: { icon: React.ReactNode; title: string; description: string }) {
return (
<div className="flex gap-3">
<div className="text-accent mt-0.5">{icon}</div>
<div className="space-y-1">
<h4 className="font-semibold text-sm">{title}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
)
}
function AIFeatureCard({ title, description }: { title: string; description: string }) {
return (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-4 pb-4">
<div className="flex gap-3">
<Sparkle size={20} weight="duotone" className="text-accent flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="font-semibold text-sm">{title}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</CardContent>
</Card>
)
}
function RoadmapItem({ status, title, description, version }: {
status: 'completed' | 'planned'
title: string
description: string
version: string
}) {
return (
<Card className={status === 'completed' ? 'bg-green-500/5 border-green-500/20' : 'bg-muted/50'}>
<CardContent className="pt-4 pb-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold">{title}</h4>
<Badge variant={status === 'completed' ? 'default' : 'secondary'} className="text-xs">
{version}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</CardContent>
</Card>
)
}
function AgentFileItem({ filename, path, description, features }: {
filename: string
path: string
description: string
features: string[]
}) {
return (
<div className="space-y-3 border-l-2 border-accent pl-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<FileCode size={18} className="text-accent" />
<code className="text-sm font-semibold text-accent">{filename}</code>
</div>
<p className="text-xs text-muted-foreground font-mono">{path}</p>
<p className="text-sm text-foreground/90">{description}</p>
</div>
<div className="space-y-1">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
<ul className="space-y-1">
{features.map((feature, idx) => (
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
)
}
function IntegrationPoint({ component, capabilities }: { component: string; capabilities: string[] }) {
return (
<div className="space-y-2 border rounded-lg p-4 bg-card">
<h4 className="font-semibold text-sm flex items-center gap-2">
<Sparkle size={16} weight="duotone" className="text-accent" />
{component}
</h4>
<ul className="space-y-1">
{capabilities.map((capability, idx) => (
<li key={idx} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-accent"></span>
<span>{capability}</span>
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,182 @@
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { FileCode, CheckCircle, Sparkle, GitBranch } from '@phosphor-icons/react'
export function FeatureItem({ icon, title, description }: { icon: React.ReactNode; title: string; description: string }) {
return (
<div className="flex gap-3">
<div className="text-accent mt-0.5">{icon}</div>
<div className="space-y-1">
<h4 className="font-semibold text-sm">{title}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
)
}
export function AIFeatureCard({ title, description }: { title: string; description: string }) {
return (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-4 pb-4">
<div className="flex gap-3">
<Sparkle size={20} weight="duotone" className="text-accent flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="font-semibold text-sm">{title}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</CardContent>
</Card>
)
}
export function RoadmapItem({ status, title, description, version }: {
status: 'completed' | 'planned'
title: string
description: string
version: string
}) {
return (
<Card className={status === 'completed' ? 'bg-green-500/5 border-green-500/20' : 'bg-muted/50'}>
<CardContent className="pt-4 pb-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold">{title}</h4>
<Badge variant={status === 'completed' ? 'default' : 'secondary'} className="text-xs">
{version}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</CardContent>
</Card>
)
}
export function AgentFileItem({ filename, path, description, features }: {
filename: string
path: string
description: string
features: string[]
}) {
return (
<div className="space-y-3 border-l-2 border-accent pl-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<FileCode size={18} className="text-accent" />
<code className="text-sm font-semibold text-accent">{filename}</code>
</div>
<p className="text-xs text-muted-foreground font-mono">{path}</p>
<p className="text-sm text-foreground/90">{description}</p>
</div>
<div className="space-y-1">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
<ul className="space-y-1">
{features.map((feature, idx) => (
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
)
}
export function IntegrationPoint({ component, capabilities }: { component: string; capabilities: string[] }) {
return (
<div className="space-y-2 border rounded-lg p-4 bg-card">
<h4 className="font-semibold text-sm flex items-center gap-2">
<Sparkle size={16} weight="duotone" className="text-accent" />
{component}
</h4>
<ul className="space-y-1">
{capabilities.map((capability, idx) => (
<li key={idx} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-accent"></span>
<span>{capability}</span>
</li>
))}
</ul>
</div>
)
}
export function CICDPlatformItem({ name, file, description, features }: {
name: string
file: string
description: string
features: string[]
}) {
return (
<div className="space-y-3 border-l-2 border-accent pl-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<GitBranch size={18} className="text-accent" />
<h3 className="text-base font-semibold">{name}</h3>
</div>
<code className="text-xs text-muted-foreground font-mono">{file}</code>
<p className="text-sm text-foreground/90">{description}</p>
</div>
<div className="space-y-1">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
<ul className="space-y-1">
{features.map((feature, idx) => (
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
)
}
export function PipelineStageCard({ stage, description, duration }: {
stage: string
description: string
duration: string
}) {
return (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-4 pb-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 flex-1">
<h4 className="font-semibold text-sm">{stage}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Badge variant="secondary" className="text-xs whitespace-nowrap">
{duration}
</Badge>
</div>
</CardContent>
</Card>
)
}
export function SassComponentItem({ name, classes, description }: { name: string; classes: string[]; description: string }) {
return (
<div className="space-y-2 p-4 border rounded-lg bg-card">
<h4 className="font-semibold">{name}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="space-y-1">
{classes.map((cls, idx) => (
<code key={idx} className="text-xs font-mono text-accent block">{cls}</code>
))}
</div>
</div>
)
}
export function AnimationItem({ name, description }: { name: string; description: string }) {
return (
<div className="space-y-1 p-3 border rounded-lg bg-card">
<code className="text-xs font-mono text-accent">{name}</code>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
)
}

View File

@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Slider } from '@/components/ui/slider'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -14,96 +13,17 @@ import {
Plus,
Trash,
Download,
CircleNotch,
Square,
Triangle,
Star,
Heart,
Polygon,
TextT,
Image as ImageIcon,
ArrowCounterClockwise,
Copy,
FloppyDisk,
PencilSimple,
Eraser,
Gradient,
Sparkle,
Drop,
MagicWand
} from '@phosphor-icons/react'
import { toast } from 'sonner'
type BrushEffect = 'solid' | 'gradient' | 'spray' | 'glow'
type CanvasFilter = 'none' | 'blur' | 'brightness' | 'contrast' | 'grayscale' | 'sepia' | 'invert' | 'saturate' | 'hue-rotate' | 'pixelate'
interface FaviconElement {
id: string
type: 'circle' | 'square' | 'triangle' | 'star' | 'heart' | 'polygon' | 'text' | 'emoji' | 'freehand'
x: number
y: number
width: number
height: number
color: string
rotation: number
text?: string
fontSize?: number
fontWeight?: string
emoji?: string
paths?: Array<{ x: number; y: number }>
strokeWidth?: number
brushEffect?: BrushEffect
gradientColor?: string
glowIntensity?: number
}
interface FaviconDesign {
id: string
name: string
size: number
backgroundColor: string
elements: FaviconElement[]
createdAt: number
updatedAt: number
filter?: CanvasFilter
filterIntensity?: number
}
const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512]
const ELEMENT_TYPES = [
{ value: 'circle', label: 'Circle', icon: CircleNotch },
{ value: 'square', label: 'Square', icon: Square },
{ value: 'triangle', label: 'Triangle', icon: Triangle },
{ value: 'star', label: 'Star', icon: Star },
{ value: 'heart', label: 'Heart', icon: Heart },
{ value: 'polygon', label: 'Polygon', icon: Polygon },
{ value: 'text', label: 'Text', icon: TextT },
{ value: 'emoji', label: 'Emoji', icon: ImageIcon },
]
const DEFAULT_DESIGN: FaviconDesign = {
id: 'default',
name: 'My Favicon',
size: 128,
backgroundColor: '#7c3aed',
elements: [
{
id: '1',
type: 'text',
x: 64,
y: 64,
width: 100,
height: 100,
color: '#ffffff',
rotation: 0,
text: 'CF',
fontSize: 48,
fontWeight: 'bold',
},
],
createdAt: Date.now(),
updatedAt: Date.now(),
}
import { BrushEffect, CanvasFilter, FaviconElement, FaviconDesign } from './FaviconDesigner/types'
import { PRESET_SIZES, ELEMENT_TYPES, DEFAULT_DESIGN } from './FaviconDesigner/constants'
import { drawCanvas } from './FaviconDesigner/canvasUtils'
export function FaviconDesigner() {
const [designs, setDesigns] = useKV<FaviconDesign[]>('favicon-designs', [DEFAULT_DESIGN])
@@ -125,255 +45,11 @@ export function FaviconDesigner() {
const selectedElement = activeDesign.elements.find((e) => e.id === selectedElementId)
useEffect(() => {
drawCanvas()
}, [activeDesign])
const drawCanvas = () => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const size = activeDesign.size
canvas.width = size
canvas.height = size
ctx.fillStyle = activeDesign.backgroundColor
ctx.fillRect(0, 0, size, size)
activeDesign.elements.forEach((element) => {
ctx.save()
if (element.type === 'freehand' && element.paths && element.paths.length > 0) {
const effect = element.brushEffect || 'solid'
const strokeWidth = element.strokeWidth || 3
if (effect === 'glow') {
ctx.shadowColor = element.color
ctx.shadowBlur = element.glowIntensity || 10
}
if (effect === 'gradient' && element.gradientColor) {
const bounds = getPathBounds(element.paths)
const gradient = ctx.createLinearGradient(
bounds.minX,
bounds.minY,
bounds.maxX,
bounds.maxY
)
gradient.addColorStop(0, element.color)
gradient.addColorStop(1, element.gradientColor)
ctx.strokeStyle = gradient
} else {
ctx.strokeStyle = element.color
}
ctx.lineWidth = strokeWidth
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
if (effect === 'spray') {
element.paths.forEach((point, i) => {
if (i % 2 === 0) {
for (let j = 0; j < 3; j++) {
const offsetX = (Math.random() - 0.5) * strokeWidth * 2
const offsetY = (Math.random() - 0.5) * strokeWidth * 2
ctx.fillStyle = element.color
ctx.beginPath()
ctx.arc(point.x + offsetX, point.y + offsetY, strokeWidth / 3, 0, Math.PI * 2)
ctx.fill()
}
}
})
} else {
ctx.beginPath()
ctx.moveTo(element.paths[0].x, element.paths[0].y)
for (let i = 1; i < element.paths.length; i++) {
ctx.lineTo(element.paths[i].x, element.paths[i].y)
}
ctx.stroke()
}
ctx.shadowBlur = 0
} else {
ctx.translate(element.x, element.y)
ctx.rotate((element.rotation * Math.PI) / 180)
ctx.fillStyle = element.color
switch (element.type) {
case 'circle':
ctx.beginPath()
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2)
ctx.fill()
break
case 'square':
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height)
break
case 'triangle':
ctx.beginPath()
ctx.moveTo(0, -element.height / 2)
ctx.lineTo(element.width / 2, element.height / 2)
ctx.lineTo(-element.width / 2, element.height / 2)
ctx.closePath()
ctx.fill()
break
case 'star':
drawStar(ctx, 0, 0, 5, element.width / 2, element.width / 4)
break
case 'heart':
drawHeart(ctx, 0, 0, element.width)
break
case 'polygon':
drawPolygon(ctx, 0, 0, 6, element.width / 2)
break
case 'text':
ctx.fillStyle = element.color
ctx.font = `${element.fontWeight || 'bold'} ${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.text || '', 0, 0)
break
case 'emoji':
ctx.font = `${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.emoji || '😀', 0, 0)
break
}
}
ctx.restore()
})
if (activeDesign.filter && activeDesign.filter !== 'none') {
applyCanvasFilter(ctx, activeDesign.filter, activeDesign.filterIntensity || 50)
if (canvas) {
drawCanvas(canvas, activeDesign)
}
}
const getPathBounds = (paths: Array<{ x: number; y: number }>) => {
const xs = paths.map(p => p.x)
const ys = paths.map(p => p.y)
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys),
}
}
const applyCanvasFilter = (ctx: CanvasRenderingContext2D, filter: CanvasFilter, intensity: number) => {
const canvas = ctx.canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imageData.data
switch (filter) {
case 'blur':
ctx.filter = `blur(${intensity / 10}px)`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'brightness':
ctx.filter = `brightness(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'contrast':
ctx.filter = `contrast(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'grayscale':
ctx.filter = `grayscale(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'sepia':
ctx.filter = `sepia(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'invert':
ctx.filter = `invert(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'saturate':
ctx.filter = `saturate(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'hue-rotate':
ctx.filter = `hue-rotate(${intensity * 3.6}deg)`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'pixelate': {
const pixelSize = Math.max(1, Math.floor(intensity / 10))
const tempCanvas = document.createElement('canvas')
tempCanvas.width = canvas.width / pixelSize
tempCanvas.height = canvas.height / pixelSize
const tempCtx = tempCanvas.getContext('2d')
if (tempCtx) {
tempCtx.imageSmoothingEnabled = false
tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height)
ctx.imageSmoothingEnabled = false
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = true
}
break
}
}
}
const drawStar = (ctx: CanvasRenderingContext2D, cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) => {
let rot = (Math.PI / 2) * 3
let x = cx
let y = cy
const step = Math.PI / spikes
ctx.beginPath()
ctx.moveTo(cx, cy - outerRadius)
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius
y = cy + Math.sin(rot) * outerRadius
ctx.lineTo(x, y)
rot += step
x = cx + Math.cos(rot) * innerRadius
y = cy + Math.sin(rot) * innerRadius
ctx.lineTo(x, y)
rot += step
}
ctx.lineTo(cx, cy - outerRadius)
ctx.closePath()
ctx.fill()
}
const drawHeart = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => {
const topCurveHeight = size * 0.3
ctx.beginPath()
ctx.moveTo(x, y + topCurveHeight)
ctx.bezierCurveTo(x, y, x - size / 2, y - topCurveHeight, x - size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x - size / 2, y + (size + topCurveHeight) / 2, x, y + (size + topCurveHeight) / 1.2, x, y + size)
ctx.bezierCurveTo(x, y + (size + topCurveHeight) / 1.2, x + size / 2, y + (size + topCurveHeight) / 2, x + size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x + size / 2, y - topCurveHeight, x, y, x, y + topCurveHeight)
ctx.closePath()
ctx.fill()
}
const drawPolygon = (ctx: CanvasRenderingContext2D, x: number, y: number, sides: number, radius: number) => {
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const px = x + radius * Math.cos(angle)
const py = y + radius * Math.sin(angle)
if (i === 0) ctx.moveTo(px, py)
else ctx.lineTo(px, py)
}
ctx.closePath()
ctx.fill()
}
}, [activeDesign])
const handleAddElement = (type: FaviconElement['type']) => {
const newElement: FaviconElement = {
@@ -720,7 +396,10 @@ export function FaviconDesigner() {
}
setCurrentPath([])
drawCanvas()
const canvas = canvasRef.current
if (canvas) {
drawCanvas(canvas, activeDesign)
}
}
const handleCanvasMouseLeave = () => {

View File

@@ -0,0 +1,247 @@
import { FaviconElement, FaviconDesign, CanvasFilter } from './types'
export function getPathBounds(paths: Array<{ x: number; y: number }>) {
const xs = paths.map(p => p.x)
const ys = paths.map(p => p.y)
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys),
}
}
export function drawStar(ctx: CanvasRenderingContext2D, cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) {
let rot = (Math.PI / 2) * 3
let x = cx
let y = cy
const step = Math.PI / spikes
ctx.beginPath()
ctx.moveTo(cx, cy - outerRadius)
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius
y = cy + Math.sin(rot) * outerRadius
ctx.lineTo(x, y)
rot += step
x = cx + Math.cos(rot) * innerRadius
y = cy + Math.sin(rot) * innerRadius
ctx.lineTo(x, y)
rot += step
}
ctx.lineTo(cx, cy - outerRadius)
ctx.closePath()
ctx.fill()
}
export function drawHeart(ctx: CanvasRenderingContext2D, x: number, y: number, size: number) {
const topCurveHeight = size * 0.3
ctx.beginPath()
ctx.moveTo(x, y + topCurveHeight)
ctx.bezierCurveTo(x, y, x - size / 2, y - topCurveHeight, x - size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x - size / 2, y + (size + topCurveHeight) / 2, x, y + (size + topCurveHeight) / 1.2, x, y + size)
ctx.bezierCurveTo(x, y + (size + topCurveHeight) / 1.2, x + size / 2, y + (size + topCurveHeight) / 2, x + size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x + size / 2, y - topCurveHeight, x, y, x, y + topCurveHeight)
ctx.closePath()
ctx.fill()
}
export function drawPolygon(ctx: CanvasRenderingContext2D, x: number, y: number, sides: number, radius: number) {
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const px = x + radius * Math.cos(angle)
const py = y + radius * Math.sin(angle)
if (i === 0) ctx.moveTo(px, py)
else ctx.lineTo(px, py)
}
ctx.closePath()
ctx.fill()
}
export function applyCanvasFilter(ctx: CanvasRenderingContext2D, filter: CanvasFilter, intensity: number) {
const canvas = ctx.canvas
switch (filter) {
case 'blur':
ctx.filter = `blur(${intensity / 10}px)`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'brightness':
ctx.filter = `brightness(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'contrast':
ctx.filter = `contrast(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'grayscale':
ctx.filter = `grayscale(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'sepia':
ctx.filter = `sepia(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'invert':
ctx.filter = `invert(${intensity / 100})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'saturate':
ctx.filter = `saturate(${intensity / 50})`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'hue-rotate':
ctx.filter = `hue-rotate(${intensity * 3.6}deg)`
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none'
break
case 'pixelate': {
const pixelSize = Math.max(1, Math.floor(intensity / 10))
const tempCanvas = document.createElement('canvas')
tempCanvas.width = canvas.width / pixelSize
tempCanvas.height = canvas.height / pixelSize
const tempCtx = tempCanvas.getContext('2d')
if (tempCtx) {
tempCtx.imageSmoothingEnabled = false
tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height)
ctx.imageSmoothingEnabled = false
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = true
}
break
}
}
}
export function drawElement(ctx: CanvasRenderingContext2D, element: FaviconElement) {
ctx.save()
if (element.type === 'freehand' && element.paths && element.paths.length > 0) {
const effect = element.brushEffect || 'solid'
const strokeWidth = element.strokeWidth || 3
if (effect === 'glow') {
ctx.shadowColor = element.color
ctx.shadowBlur = element.glowIntensity || 10
}
if (effect === 'gradient' && element.gradientColor) {
const bounds = getPathBounds(element.paths)
const gradient = ctx.createLinearGradient(
bounds.minX,
bounds.minY,
bounds.maxX,
bounds.maxY
)
gradient.addColorStop(0, element.color)
gradient.addColorStop(1, element.gradientColor)
ctx.strokeStyle = gradient
} else {
ctx.strokeStyle = element.color
}
ctx.lineWidth = strokeWidth
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
if (effect === 'spray') {
element.paths.forEach((point, i) => {
if (i % 2 === 0) {
for (let j = 0; j < 3; j++) {
const offsetX = (Math.random() - 0.5) * strokeWidth * 2
const offsetY = (Math.random() - 0.5) * strokeWidth * 2
ctx.fillStyle = element.color
ctx.beginPath()
ctx.arc(point.x + offsetX, point.y + offsetY, strokeWidth / 3, 0, Math.PI * 2)
ctx.fill()
}
}
})
} else {
ctx.beginPath()
ctx.moveTo(element.paths[0].x, element.paths[0].y)
for (let i = 1; i < element.paths.length; i++) {
ctx.lineTo(element.paths[i].x, element.paths[i].y)
}
ctx.stroke()
}
ctx.shadowBlur = 0
} else {
ctx.translate(element.x, element.y)
ctx.rotate((element.rotation * Math.PI) / 180)
ctx.fillStyle = element.color
switch (element.type) {
case 'circle':
ctx.beginPath()
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2)
ctx.fill()
break
case 'square':
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height)
break
case 'triangle':
ctx.beginPath()
ctx.moveTo(0, -element.height / 2)
ctx.lineTo(element.width / 2, element.height / 2)
ctx.lineTo(-element.width / 2, element.height / 2)
ctx.closePath()
ctx.fill()
break
case 'star':
drawStar(ctx, 0, 0, 5, element.width / 2, element.width / 4)
break
case 'heart':
drawHeart(ctx, 0, 0, element.width)
break
case 'polygon':
drawPolygon(ctx, 0, 0, 6, element.width / 2)
break
case 'text':
ctx.fillStyle = element.color
ctx.font = `${element.fontWeight || 'bold'} ${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.text || '', 0, 0)
break
case 'emoji':
ctx.font = `${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.emoji || '😀', 0, 0)
break
}
}
ctx.restore()
}
export function drawCanvas(canvas: HTMLCanvasElement, design: FaviconDesign) {
const ctx = canvas.getContext('2d')
if (!ctx) return
const size = design.size
canvas.width = size
canvas.height = size
ctx.fillStyle = design.backgroundColor
ctx.fillRect(0, 0, size, size)
design.elements.forEach((element) => {
drawElement(ctx, element)
})
if (design.filter && design.filter !== 'none') {
applyCanvasFilter(ctx, design.filter, design.filterIntensity || 50)
}
}

View File

@@ -0,0 +1,48 @@
import {
CircleNotch,
Square,
Triangle,
Star,
Heart,
Polygon,
TextT,
Image as ImageIcon,
} from '@phosphor-icons/react'
import { FaviconDesign } from './types'
export const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512]
export const ELEMENT_TYPES = [
{ value: 'circle', label: 'Circle', icon: CircleNotch },
{ value: 'square', label: 'Square', icon: Square },
{ value: 'triangle', label: 'Triangle', icon: Triangle },
{ value: 'star', label: 'Star', icon: Star },
{ value: 'heart', label: 'Heart', icon: Heart },
{ value: 'polygon', label: 'Polygon', icon: Polygon },
{ value: 'text', label: 'Text', icon: TextT },
{ value: 'emoji', label: 'Emoji', icon: ImageIcon },
]
export const DEFAULT_DESIGN: FaviconDesign = {
id: 'default',
name: 'My Favicon',
size: 128,
backgroundColor: '#7c3aed',
elements: [
{
id: '1',
type: 'text',
x: 64,
y: 64,
width: 100,
height: 100,
color: '#ffffff',
rotation: 0,
text: 'CF',
fontSize: 48,
fontWeight: 'bold',
},
],
createdAt: Date.now(),
updatedAt: Date.now(),
}

View File

@@ -0,0 +1,34 @@
export type BrushEffect = 'solid' | 'gradient' | 'spray' | 'glow'
export type CanvasFilter = 'none' | 'blur' | 'brightness' | 'contrast' | 'grayscale' | 'sepia' | 'invert' | 'saturate' | 'hue-rotate' | 'pixelate'
export interface FaviconElement {
id: string
type: 'circle' | 'square' | 'triangle' | 'star' | 'heart' | 'polygon' | 'text' | 'emoji' | 'freehand'
x: number
y: number
width: number
height: number
color: string
rotation: number
text?: string
fontSize?: number
fontWeight?: string
emoji?: string
paths?: Array<{ x: number; y: number }>
strokeWidth?: number
brushEffect?: BrushEffect
gradientColor?: string
glowIntensity?: number
}
export interface FaviconDesign {
id: string
name: string
size: number
backgroundColor: string
elements: FaviconElement[]
createdAt: number
updatedAt: number
filter?: CanvasFilter
filterIntensity?: number
}

View File

@@ -1,6 +1,4 @@
/// <reference path="../global.d.ts" />
import { useState, useEffect, useCallback, useRef, ReactElement } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import { useKV } from '@/hooks/use-kv'
import ReactFlow, {
Node,
@@ -14,307 +12,20 @@ import ReactFlow, {
MarkerType,
ConnectionMode,
Panel,
NodeProps,
Handle,
Position,
reconnectEdge,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Textarea } from '@/components/ui/textarea'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Trash, Sparkle, DotsThree, Package } from '@phosphor-icons/react'
import { Plus, Trash, Sparkle, Package } from '@phosphor-icons/react'
import { toast } from 'sonner'
interface FeatureIdea {
id: string
title: string
description: string
category: string
priority: 'low' | 'medium' | 'high'
status: 'idea' | 'planned' | 'in-progress' | 'completed'
createdAt: number
parentGroup?: string
}
interface IdeaGroup {
id: string
label: string
color: string
createdAt: number
}
interface IdeaEdgeData {
label?: string
}
const SEED_IDEAS: FeatureIdea[] = [
{
id: 'idea-1',
title: 'AI Code Assistant',
description: 'Integrate an AI assistant that can suggest code improvements and answer questions',
category: 'AI/ML',
priority: 'high',
status: 'completed',
createdAt: Date.now() - 10000000,
},
{
id: 'idea-2',
title: 'Real-time Collaboration',
description: 'Allow multiple developers to work on the same project simultaneously',
category: 'Collaboration',
priority: 'high',
status: 'idea',
createdAt: Date.now() - 9000000,
},
{
id: 'idea-3',
title: 'Component Marketplace',
description: 'A marketplace where users can share and download pre-built components',
category: 'Community',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 8000000,
},
{
id: 'idea-4',
title: 'Visual Git Integration',
description: 'Git operations through a visual interface with branch visualization',
category: 'DevOps',
priority: 'high',
status: 'planned',
createdAt: Date.now() - 7000000,
},
{
id: 'idea-5',
title: 'API Mock Server',
description: 'Built-in mock server for testing API integrations',
category: 'Testing',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 6000000,
},
{
id: 'idea-6',
title: 'Performance Profiler',
description: 'Analyze and optimize application performance with visual metrics',
category: 'Performance',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 5000000,
},
{
id: 'idea-7',
title: 'Theme Presets',
description: 'Pre-designed theme templates for quick project setup',
category: 'Design',
priority: 'low',
status: 'completed',
createdAt: Date.now() - 4000000,
},
{
id: 'idea-8',
title: 'Database Schema Migrations',
description: 'Visual tool for creating and managing database migrations',
category: 'Database',
priority: 'high',
status: 'in-progress',
createdAt: Date.now() - 3000000,
},
{
id: 'idea-9',
title: 'Mobile App Preview',
description: 'Live preview on actual mobile devices or simulators',
category: 'Mobile',
priority: 'medium',
status: 'planned',
createdAt: Date.now() - 2000000,
},
{
id: 'idea-10',
title: 'Accessibility Checker',
description: 'Automated accessibility testing and suggestions',
category: 'Accessibility',
priority: 'high',
status: 'idea',
createdAt: Date.now() - 1000000,
},
]
const CATEGORIES = ['AI/ML', 'Collaboration', 'Community', 'DevOps', 'Testing', 'Performance', 'Design', 'Database', 'Mobile', 'Accessibility', 'Productivity', 'Security', 'Analytics', 'Other']
const PRIORITIES = ['low', 'medium', 'high'] as const
const STATUSES = ['idea', 'planned', 'in-progress', 'completed'] as const
const CONNECTION_STYLE = {
stroke: '#a78bfa',
strokeWidth: 2.5
}
const STATUS_COLORS = {
idea: 'bg-muted text-muted-foreground',
planned: 'bg-accent text-accent-foreground',
'in-progress': 'bg-primary text-primary-foreground',
completed: 'bg-green-600 text-white',
}
const PRIORITY_COLORS = {
low: 'border-blue-400/60 bg-blue-50/80 dark:bg-blue-950/40',
medium: 'border-amber-400/60 bg-amber-50/80 dark:bg-amber-950/40',
high: 'border-red-400/60 bg-red-50/80 dark:bg-red-950/40',
}
const GROUP_COLORS = [
{ name: 'Blue', value: '#3b82f6', bg: 'rgba(59, 130, 246, 0.08)', border: 'rgba(59, 130, 246, 0.3)' },
{ name: 'Purple', value: '#a855f7', bg: 'rgba(168, 85, 247, 0.08)', border: 'rgba(168, 85, 247, 0.3)' },
{ name: 'Green', value: '#10b981', bg: 'rgba(16, 185, 129, 0.08)', border: 'rgba(16, 185, 129, 0.3)' },
{ name: 'Red', value: '#ef4444', bg: 'rgba(239, 68, 68, 0.08)', border: 'rgba(239, 68, 68, 0.3)' },
{ name: 'Orange', value: '#f97316', bg: 'rgba(249, 115, 22, 0.08)', border: 'rgba(249, 115, 22, 0.3)' },
{ name: 'Pink', value: '#ec4899', bg: 'rgba(236, 72, 153, 0.08)', border: 'rgba(236, 72, 153, 0.3)' },
{ name: 'Cyan', value: '#06b6d4', bg: 'rgba(6, 182, 212, 0.08)', border: 'rgba(6, 182, 212, 0.3)' },
{ name: 'Amber', value: '#f59e0b', bg: 'rgba(245, 158, 11, 0.08)', border: 'rgba(245, 158, 11, 0.3)' },
]
function GroupNode({ data, selected }: NodeProps<IdeaGroup>) {
const colorScheme = GROUP_COLORS.find(c => c.value === data.color) || GROUP_COLORS[0]
return (
<div
className="rounded-2xl backdrop-blur-sm transition-all"
style={{
width: 450,
height: 350,
backgroundColor: colorScheme.bg,
border: `3px dashed ${colorScheme.border}`,
boxShadow: selected ? `0 0 0 2px ${colorScheme.value}` : 'none',
}}
>
<div
className="absolute -top-3 left-4 px-3 py-1 rounded-full text-xs font-semibold shadow-md"
style={{
backgroundColor: colorScheme.value,
color: 'white',
}}
>
{data.label}
</div>
<Button
size="icon"
variant="ghost"
className="absolute -top-2 -right-2 h-7 w-7 rounded-full shadow-md bg-background hover:bg-destructive hover:text-destructive-foreground"
onClick={(e) => {
e.stopPropagation()
const event = new CustomEvent('editGroup', { detail: data })
window.dispatchEvent(event)
}}
>
<DotsThree size={16} />
</Button>
</div>
)
}
function IdeaNode({ data, selected, id }: NodeProps<FeatureIdea> & { id: string }) {
const [connectionCounts, setConnectionCounts] = useState<Record<string, number>>({
left: 0,
right: 0,
top: 0,
bottom: 0,
})
useEffect(() => {
const updateConnectionCounts = (event: CustomEvent) => {
const { nodeId, counts } = event.detail
if (nodeId === id) {
setConnectionCounts(counts)
}
}
window.addEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener)
return () => {
window.removeEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener)
}
}, [id])
const generateHandles = (position: Position, type: 'source' | 'target', side: string) => {
const count = connectionCounts[side] || 0
const totalHandles = Math.max(2, count + 1)
const handles: ReactElement[] = []
for (let i = 0; i < totalHandles; i++) {
const handleId = `${side}-${i}`
const isVertical = position === Position.Top || position === Position.Bottom
const positionStyle = isVertical
? { left: `${((i + 1) / (totalHandles + 1)) * 100}%` }
: { top: `${((i + 1) / (totalHandles + 1)) * 100}%` }
handles.push(
<Handle
key={handleId}
type={type}
position={position}
id={handleId}
className="w-3 h-3 !bg-primary border-2 border-background transition-all hover:scale-125"
style={{
...positionStyle,
transform: 'translate(-50%, -50%)',
}}
/>
)
}
return handles
}
return (
<div className="relative">
{generateHandles(Position.Left, 'target', 'left')}
{generateHandles(Position.Right, 'source', 'right')}
{generateHandles(Position.Top, 'target', 'top')}
{generateHandles(Position.Bottom, 'source', 'bottom')}
<Card className={`p-4 shadow-xl hover:shadow-2xl transition-all border-2 ${PRIORITY_COLORS[data.priority]} w-[240px] ${selected ? 'ring-2 ring-primary' : ''}`}>
<div className="space-y-2">
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-sm line-clamp-2 flex-1">{data.title}</h3>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 shrink-0"
onClick={(e) => {
e.stopPropagation()
const event = new CustomEvent('editIdea', { detail: data })
window.dispatchEvent(event)
}}
>
<DotsThree size={16} />
</Button>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
{data.description}
</p>
<div className="flex flex-wrap gap-1">
<Badge variant="secondary" className="text-xs">
{data.category}
</Badge>
<Badge className={`text-xs ${STATUS_COLORS[data.status]}`}>
{data.status}
</Badge>
</div>
</div>
</Card>
</div>
)
}
const nodeTypes = {
ideaNode: IdeaNode,
groupNode: GroupNode,
}
import { FeatureIdea, IdeaGroup, IdeaEdgeData } from './FeatureIdeaCloud/types'
import { SEED_IDEAS, CATEGORIES, PRIORITIES, STATUSES, CONNECTION_STYLE, GROUP_COLORS } from './FeatureIdeaCloud/constants'
import { nodeTypes } from './FeatureIdeaCloud/nodes'
export function FeatureIdeaCloud() {
const [ideas, setIdeas] = useKV<FeatureIdea[]>('feature-ideas', SEED_IDEAS)

View File

@@ -1,3 +1,103 @@
import { FeatureIdea } from './types'
export const SEED_IDEAS: FeatureIdea[] = [
{
id: 'idea-1',
title: 'AI Code Assistant',
description: 'Integrate an AI assistant that can suggest code improvements and answer questions',
category: 'AI/ML',
priority: 'high',
status: 'completed',
createdAt: Date.now() - 10000000,
},
{
id: 'idea-2',
title: 'Real-time Collaboration',
description: 'Allow multiple developers to work on the same project simultaneously',
category: 'Collaboration',
priority: 'high',
status: 'idea',
createdAt: Date.now() - 9000000,
},
{
id: 'idea-3',
title: 'Component Marketplace',
description: 'A marketplace where users can share and download pre-built components',
category: 'Community',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 8000000,
},
{
id: 'idea-4',
title: 'Visual Git Integration',
description: 'Git operations through a visual interface with branch visualization',
category: 'DevOps',
priority: 'high',
status: 'planned',
createdAt: Date.now() - 7000000,
},
{
id: 'idea-5',
title: 'API Mock Server',
description: 'Built-in mock server for testing API integrations',
category: 'Testing',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 6000000,
},
{
id: 'idea-6',
title: 'Performance Profiler',
description: 'Analyze and optimize application performance with visual metrics',
category: 'Performance',
priority: 'medium',
status: 'idea',
createdAt: Date.now() - 5000000,
},
{
id: 'idea-7',
title: 'Theme Presets',
description: 'Pre-designed theme templates for quick project setup',
category: 'Design',
priority: 'low',
status: 'completed',
createdAt: Date.now() - 4000000,
},
{
id: 'idea-8',
title: 'Database Schema Migrations',
description: 'Visual tool for creating and managing database migrations',
category: 'Database',
priority: 'high',
status: 'in-progress',
createdAt: Date.now() - 3000000,
},
{
id: 'idea-9',
title: 'Mobile App Preview',
description: 'Live preview on actual mobile devices or simulators',
category: 'Mobile',
priority: 'medium',
status: 'planned',
createdAt: Date.now() - 2000000,
},
{
id: 'idea-10',
title: 'Accessibility Checker',
description: 'Automated accessibility testing and suggestions',
category: 'Accessibility',
priority: 'high',
status: 'idea',
createdAt: Date.now() - 1000000,
},
]
export const CONNECTION_STYLE = {
stroke: '#a78bfa',
strokeWidth: 2.5
}
export const CATEGORIES = [
'AI/ML',
'Collaboration',

View File

@@ -0,0 +1,145 @@
import { useState, useEffect, ReactElement } from 'react'
import { NodeProps, Handle, Position } from 'reactflow'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { DotsThree } from '@phosphor-icons/react'
import { FeatureIdea, IdeaGroup } from './types'
import { PRIORITY_COLORS, STATUS_COLORS, GROUP_COLORS } from './constants'
export function GroupNode({ data, selected }: NodeProps<IdeaGroup>) {
const colorScheme = GROUP_COLORS.find(c => c.value === data.color) || GROUP_COLORS[0]
return (
<div
className="rounded-2xl backdrop-blur-sm transition-all"
style={{
width: 450,
height: 350,
backgroundColor: colorScheme.bg,
border: `3px dashed ${colorScheme.border}`,
boxShadow: selected ? `0 0 0 2px ${colorScheme.value}` : 'none',
}}
>
<div
className="absolute -top-3 left-4 px-3 py-1 rounded-full text-xs font-semibold shadow-md"
style={{
backgroundColor: colorScheme.value,
color: 'white',
}}
>
{data.label}
</div>
<Button
size="icon"
variant="ghost"
className="absolute -top-2 -right-2 h-7 w-7 rounded-full shadow-md bg-background hover:bg-destructive hover:text-destructive-foreground"
onClick={(e) => {
e.stopPropagation()
const event = new CustomEvent('editGroup', { detail: data })
window.dispatchEvent(event)
}}
>
<DotsThree size={16} />
</Button>
</div>
)
}
export function IdeaNode({ data, selected, id }: NodeProps<FeatureIdea> & { id: string }) {
const [connectionCounts, setConnectionCounts] = useState<Record<string, number>>({
left: 0,
right: 0,
top: 0,
bottom: 0,
})
useEffect(() => {
const updateConnectionCounts = (event: CustomEvent) => {
const { nodeId, counts } = event.detail
if (nodeId === id) {
setConnectionCounts(counts)
}
}
window.addEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener)
return () => {
window.removeEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener)
}
}, [id])
const generateHandles = (position: Position, type: 'source' | 'target', side: string) => {
const count = connectionCounts[side] || 0
const totalHandles = Math.max(2, count + 1)
const handles: ReactElement[] = []
for (let i = 0; i < totalHandles; i++) {
const handleId = `${side}-${i}`
const isVertical = position === Position.Top || position === Position.Bottom
const positionStyle = isVertical
? { left: `${((i + 1) / (totalHandles + 1)) * 100}%` }
: { top: `${((i + 1) / (totalHandles + 1)) * 100}%` }
handles.push(
<Handle
key={handleId}
type={type}
position={position}
id={handleId}
className="w-3 h-3 !bg-primary border-2 border-background transition-all hover:scale-125"
style={{
...positionStyle,
transform: 'translate(-50%, -50%)',
}}
/>
)
}
return handles
}
return (
<div className="relative">
{generateHandles(Position.Left, 'target', 'left')}
{generateHandles(Position.Right, 'source', 'right')}
{generateHandles(Position.Top, 'target', 'top')}
{generateHandles(Position.Bottom, 'source', 'bottom')}
<Card className={`p-4 shadow-xl hover:shadow-2xl transition-all border-2 ${PRIORITY_COLORS[data.priority]} w-[240px] ${selected ? 'ring-2 ring-primary' : ''}`}>
<div className="space-y-2">
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-sm line-clamp-2 flex-1">{data.title}</h3>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 shrink-0"
onClick={(e) => {
e.stopPropagation()
const event = new CustomEvent('editIdea', { detail: data })
window.dispatchEvent(event)
}}
>
<DotsThree size={16} />
</Button>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
{data.description}
</p>
<div className="flex flex-wrap gap-1">
<Badge variant="secondary" className="text-xs">
{data.category}
</Badge>
<Badge className={`text-xs ${STATUS_COLORS[data.status]}`}>
{data.status}
</Badge>
</div>
</div>
</Card>
</div>
)
}
export const nodeTypes = {
ideaNode: IdeaNode,
groupNode: GroupNode,
}

View File

@@ -0,0 +1,21 @@
export interface FeatureIdea {
id: string
title: string
description: string
category: string
priority: 'low' | 'medium' | 'high'
status: 'idea' | 'planned' | 'in-progress' | 'completed'
createdAt: number
parentGroup?: string
}
export interface IdeaGroup {
id: string
label: string
color: string
createdAt: number
}
export interface IdeaEdgeData {
label?: string
}