From a32cd7aabf7a7c20b60acd885cc90bac8cd49dbe Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Wed, 18 Feb 2026 22:35:13 +0800 Subject: [PATCH] refactor: rename PascalCase files to kebab-case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all component files to kebab-case per project convention (e.g. EditorLayout.tsx → editor-layout.tsx). Remove obsolete PascalCase originals that were replaced by kebab-case versions. --- src/canvas/FabricCanvas.tsx | 26 --- src/components/Header.tsx | 33 ++- src/components/editor/EditorLayout.tsx | 27 --- src/components/editor/ToolButton.tsx | 37 --- src/components/editor/Toolbar.tsx | 221 ++++++++++-------- src/components/panels/AppearanceSection.tsx | 30 --- src/components/panels/CornerRadiusSection.tsx | 35 --- src/components/panels/FillSection.tsx | 32 --- src/components/panels/LayerItem.tsx | 128 ---------- src/components/panels/LayerPanel.tsx | 91 -------- src/components/panels/PropertyPanel.tsx | 92 -------- src/components/panels/SizeSection.tsx | 68 ------ src/components/panels/StrokeSection.tsx | 61 ----- src/components/panels/TextSection.tsx | 86 ------- src/components/shared/ColorPicker.tsx | 66 ------ src/components/shared/DropdownSelect.tsx | 34 --- src/components/shared/IconButton.tsx | 34 --- src/components/shared/NumberInput.tsx | 111 --------- src/components/shared/SliderInput.tsx | 41 ---- 19 files changed, 135 insertions(+), 1118 deletions(-) delete mode 100644 src/canvas/FabricCanvas.tsx delete mode 100644 src/components/editor/EditorLayout.tsx delete mode 100644 src/components/editor/ToolButton.tsx delete mode 100644 src/components/panels/AppearanceSection.tsx delete mode 100644 src/components/panels/CornerRadiusSection.tsx delete mode 100644 src/components/panels/FillSection.tsx delete mode 100644 src/components/panels/LayerItem.tsx delete mode 100644 src/components/panels/LayerPanel.tsx delete mode 100644 src/components/panels/PropertyPanel.tsx delete mode 100644 src/components/panels/SizeSection.tsx delete mode 100644 src/components/panels/StrokeSection.tsx delete mode 100644 src/components/panels/TextSection.tsx delete mode 100644 src/components/shared/ColorPicker.tsx delete mode 100644 src/components/shared/DropdownSelect.tsx delete mode 100644 src/components/shared/IconButton.tsx delete mode 100644 src/components/shared/NumberInput.tsx delete mode 100644 src/components/shared/SliderInput.tsx diff --git a/src/canvas/FabricCanvas.tsx b/src/canvas/FabricCanvas.tsx deleted file mode 100644 index a8c7344fe..000000000 --- a/src/canvas/FabricCanvas.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useRef } from 'react' -import { useFabricCanvas } from './use-fabric-canvas' -import { useCanvasEvents } from './use-canvas-events' -import { useCanvasViewport } from './use-canvas-viewport' -import { useCanvasSelection } from './use-canvas-selection' -import { useCanvasSync } from './use-canvas-sync' - -export default function FabricCanvas() { - const canvasRef = useRef(null) - const containerRef = useRef(null) - - useFabricCanvas(canvasRef, containerRef) - useCanvasEvents() - useCanvasViewport() - useCanvasSelection() - useCanvasSync() - - return ( -
- -
- ) -} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 15aee61c4..0157488be 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,20 +2,22 @@ import { Link } from '@tanstack/react-router' import { useState } from 'react' import { Home, Menu, X } from 'lucide-react' +import { Button } from '@/components/ui/button' export default function Header() { const [isOpen, setIsOpen] = useState(false) return ( <> -
- + +

diff --git a/src/components/editor/EditorLayout.tsx b/src/components/editor/EditorLayout.tsx deleted file mode 100644 index 6dafad6a9..000000000 --- a/src/components/editor/EditorLayout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { lazy, Suspense } from 'react' -import Toolbar from './Toolbar' -import LayerPanel from '@/components/panels/LayerPanel' -import PropertyPanel from '@/components/panels/PropertyPanel' - -const FabricCanvas = lazy(() => import('@/canvas/FabricCanvas')) - -export default function EditorLayout() { - return ( -
- -
- - - Loading canvas... -
- } - > - - - -
- - ) -} diff --git a/src/components/editor/ToolButton.tsx b/src/components/editor/ToolButton.tsx deleted file mode 100644 index c58856a4b..000000000 --- a/src/components/editor/ToolButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { ReactNode } from 'react' -import type { ToolType } from '@/types/canvas' -import { useCanvasStore } from '@/stores/canvas-store' - -interface ToolButtonProps { - tool: ToolType - icon: ReactNode - label: string - shortcut?: string -} - -export default function ToolButton({ - tool, - icon, - label, - shortcut, -}: ToolButtonProps) { - const activeTool = useCanvasStore((s) => s.activeTool) - const setActiveTool = useCanvasStore((s) => s.setActiveTool) - const isActive = activeTool === tool - - return ( - - ) -} diff --git a/src/components/editor/Toolbar.tsx b/src/components/editor/Toolbar.tsx index 72b9b6d1f..fd939f2df 100644 --- a/src/components/editor/Toolbar.tsx +++ b/src/components/editor/Toolbar.tsx @@ -6,121 +6,138 @@ import { Type, Frame, Hand, - ZoomIn, - ZoomOut, + Undo2, + Redo2, } from 'lucide-react' -import ToolButton from './ToolButton' +import ToolButton from './tool-button' import { useCanvasStore } from '@/stores/canvas-store' -import { MIN_ZOOM, MAX_ZOOM } from '@/canvas/canvas-constants' +import { useDocumentStore } from '@/stores/document-store' +import { useHistoryStore } from '@/stores/history-store' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' export default function Toolbar() { - const zoom = useCanvasStore((s) => s.viewport.zoom) - const fabricCanvas = useCanvasStore((s) => s.fabricCanvas) + const canUndo = useHistoryStore((s) => s.undoStack.length > 0) + const canRedo = useHistoryStore((s) => s.redoStack.length > 0) - const handleZoomIn = () => { - if (!fabricCanvas) return - const newZoom = Math.min(MAX_ZOOM, zoom * 1.25) - const center = fabricCanvas.getCenterPoint() - fabricCanvas.zoomToPoint(center, newZoom) - useCanvasStore.getState().setZoom(newZoom) - fabricCanvas.requestRenderAll() + const handleUndo = () => { + const currentDoc = useDocumentStore.getState().document + const prev = useHistoryStore.getState().undo(currentDoc) + if (prev) { + useDocumentStore.getState().applyHistoryState(prev) + } + useCanvasStore.getState().clearSelection() + const canvas = useCanvasStore.getState().fabricCanvas + if (canvas) { + canvas.discardActiveObject() + canvas.requestRenderAll() + } } - const handleZoomOut = () => { - if (!fabricCanvas) return - const newZoom = Math.max(MIN_ZOOM, zoom / 1.25) - const center = fabricCanvas.getCenterPoint() - fabricCanvas.zoomToPoint(center, newZoom) - useCanvasStore.getState().setZoom(newZoom) - fabricCanvas.requestRenderAll() - } - - const handleZoomReset = () => { - if (!fabricCanvas) return - const center = fabricCanvas.getCenterPoint() - fabricCanvas.zoomToPoint(center, 1) - useCanvasStore.getState().setZoom(1) - fabricCanvas.requestRenderAll() + const handleRedo = () => { + const currentDoc = useDocumentStore.getState().document + const next = useHistoryStore.getState().redo(currentDoc) + if (next) { + useDocumentStore.getState().applyHistoryState(next) + } + useCanvasStore.getState().clearSelection() + const canvas = useCanvasStore.getState().fabricCanvas + if (canvas) { + canvas.discardActiveObject() + canvas.requestRenderAll() + } } return ( -
+
{/* Drawing Tools */} -
- } - label="Select" - shortcut="V" - /> - } - label="Frame" - shortcut="F" - /> - } - label="Rectangle" - shortcut="R" - /> - } - label="Ellipse" - shortcut="O" - /> - } - label="Line" - shortcut="L" - /> - } - label="Text" - shortcut="T" - /> - } - label="Hand" - shortcut="H" - /> -
+ } + label="Select" + shortcut="V" + /> + } + label="Rectangle" + shortcut="R" + /> + } + label="Ellipse" + shortcut="O" + /> + } + label="Line" + shortcut="L" + /> + } + label="Text" + shortcut="T" + /> + } + label="Frame" + shortcut="F" + /> + } + label="Hand" + shortcut="H" + /> - {/* Spacer */} -
+ - {/* Zoom Controls */} -
- - - -
+ {/* Undo / Redo */} + + + + + + Undo + + {'\u2318'}Z + + + + + + + + + Redo + + {'\u2318\u21e7'}Z + + +
) } diff --git a/src/components/panels/AppearanceSection.tsx b/src/components/panels/AppearanceSection.tsx deleted file mode 100644 index f3f4c507a..000000000 --- a/src/components/panels/AppearanceSection.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import SliderInput from '@/components/shared/SliderInput' -import type { PenNode } from '@/types/pen' - -interface AppearanceSectionProps { - node: PenNode - onUpdate: (updates: Partial) => void -} - -export default function AppearanceSection({ - node, - onUpdate, -}: AppearanceSectionProps) { - const opacity = - typeof node.opacity === 'number' ? node.opacity * 100 : 100 - - return ( -
-

- Appearance -

- onUpdate({ opacity: v / 100 })} - min={0} - max={100} - /> -
- ) -} diff --git a/src/components/panels/CornerRadiusSection.tsx b/src/components/panels/CornerRadiusSection.tsx deleted file mode 100644 index 65bed1e0f..000000000 --- a/src/components/panels/CornerRadiusSection.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import NumberInput from '@/components/shared/NumberInput' -import type { PenNode } from '@/types/pen' - -interface CornerRadiusSectionProps { - cornerRadius?: number | [number, number, number, number] - onUpdate: (updates: Partial) => void -} - -export default function CornerRadiusSection({ - cornerRadius, - onUpdate, -}: CornerRadiusSectionProps) { - const value = - typeof cornerRadius === 'number' - ? cornerRadius - : Array.isArray(cornerRadius) - ? cornerRadius[0] - : 0 - - return ( -
-

- Corner Radius -

- - onUpdate({ cornerRadius: v } as Partial) - } - min={0} - max={999} - /> -
- ) -} diff --git a/src/components/panels/FillSection.tsx b/src/components/panels/FillSection.tsx deleted file mode 100644 index 0c8f6135c..000000000 --- a/src/components/panels/FillSection.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import ColorPicker from '@/components/shared/ColorPicker' -import type { PenNode } from '@/types/pen' -import type { PenFill } from '@/types/styles' - -interface FillSectionProps { - fills?: PenFill[] - onUpdate: (updates: Partial) => void -} - -export default function FillSection({ - fills, - onUpdate, -}: FillSectionProps) { - const currentColor = - fills && fills.length > 0 && fills[0].type === 'solid' - ? fills[0].color - : '#d1d5db' - - const handleColorChange = (color: string) => { - const newFills: PenFill[] = [{ type: 'solid', color }] - onUpdate({ fill: newFills } as Partial) - } - - return ( -
-

- Fill -

- -
- ) -} diff --git a/src/components/panels/LayerItem.tsx b/src/components/panels/LayerItem.tsx deleted file mode 100644 index e71fbd396..000000000 --- a/src/components/panels/LayerItem.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState } from 'react' -import { - Square, - Circle, - Type, - Minus, - Frame, - Eye, - EyeOff, - Lock, - Unlock, - FolderOpen, - Hexagon, - Spline, - Link, -} from 'lucide-react' -import type { PenNodeType } from '@/types/pen' - -const TYPE_ICONS: Record = { - rectangle: Square, - ellipse: Circle, - text: Type, - line: Minus, - frame: Frame, - group: FolderOpen, - polygon: Hexagon, - path: Spline, - ref: Link, -} - -interface LayerItemProps { - id: string - name: string - type: PenNodeType - depth: number - selected: boolean - onSelect: (id: string) => void - onRename: (id: string, name: string) => void -} - -export default function LayerItem({ - id, - name, - type, - depth, - selected, - onSelect, - onRename, -}: LayerItemProps) { - const [isEditing, setIsEditing] = useState(false) - const [editName, setEditName] = useState(name) - const [visible, setVisible] = useState(true) - const [locked, setLocked] = useState(false) - - const Icon = TYPE_ICONS[type] ?? Square - - const handleDoubleClick = () => { - setEditName(name) - setIsEditing(true) - } - - const handleRenameBlur = () => { - setIsEditing(false) - if (editName.trim() && editName !== name) { - onRename(id, editName.trim()) - } - } - - const handleRenameKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleRenameBlur() - if (e.key === 'Escape') { - setIsEditing(false) - setEditName(name) - } - } - - return ( -
onSelect(id)} - onDoubleClick={handleDoubleClick} - > - - - {isEditing ? ( - setEditName(e.target.value)} - onBlur={handleRenameBlur} - onKeyDown={handleRenameKeyDown} - className="flex-1 bg-gray-700 text-white text-xs px-1 py-0.5 rounded border border-blue-500 focus:outline-none" - autoFocus - /> - ) : ( - {name} - )} - - - -
- ) -} diff --git a/src/components/panels/LayerPanel.tsx b/src/components/panels/LayerPanel.tsx deleted file mode 100644 index 7766fbd97..000000000 --- a/src/components/panels/LayerPanel.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useDocumentStore } from '@/stores/document-store' -import { useCanvasStore } from '@/stores/canvas-store' -import type { FabricObjectWithPenId } from '@/canvas/canvas-object-factory' -import type { PenNode } from '@/types/pen' -import LayerItem from './LayerItem' - -function renderLayerTree( - nodes: PenNode[], - depth: number, - selectedIds: string[], - onSelect: (id: string) => void, - onRename: (id: string, name: string) => void, -) { - // Render in reverse order so top items appear at top of panel - return [...nodes].reverse().map((node) => ( -
- - {'children' in node && - node.children && - node.children.length > 0 && - renderLayerTree( - node.children, - depth + 1, - selectedIds, - onSelect, - onRename, - )} -
- )) -} - -export default function LayerPanel() { - const children = useDocumentStore((s) => s.document.children) - const updateNode = useDocumentStore((s) => s.updateNode) - const selectedIds = useCanvasStore((s) => s.selection.selectedIds) - const setSelection = useCanvasStore((s) => s.setSelection) - const fabricCanvas = useCanvasStore((s) => s.fabricCanvas) - - const handleSelect = (id: string) => { - setSelection([id], id) - - // Also select the corresponding fabric object - if (fabricCanvas) { - const objects = fabricCanvas.getObjects() - const target = objects.find( - (o) => (o as FabricObjectWithPenId).penNodeId === id, - ) - if (target) { - fabricCanvas.setActiveObject(target) - fabricCanvas.requestRenderAll() - } - } - } - - const handleRename = (id: string, name: string) => { - updateNode(id, { name }) - } - - return ( -
-
- - Layers - -
-
- {children.length === 0 ? ( -

- No layers yet. Use the toolbar to draw shapes. -

- ) : ( - renderLayerTree( - children, - 0, - selectedIds, - handleSelect, - handleRename, - ) - )} -
-
- ) -} diff --git a/src/components/panels/PropertyPanel.tsx b/src/components/panels/PropertyPanel.tsx deleted file mode 100644 index d121d20ef..000000000 --- a/src/components/panels/PropertyPanel.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useCanvasStore } from '@/stores/canvas-store' -import { useDocumentStore } from '@/stores/document-store' -import type { PenNode } from '@/types/pen' -import SizeSection from './SizeSection' -import FillSection from './FillSection' -import StrokeSection from './StrokeSection' -import AppearanceSection from './AppearanceSection' -import CornerRadiusSection from './CornerRadiusSection' -import TextSection from './TextSection' - -export default function PropertyPanel() { - const activeId = useCanvasStore((s) => s.selection.activeId) - const children = useDocumentStore((s) => s.document.children) - const getNodeById = useDocumentStore((s) => s.getNodeById) - const updateNode = useDocumentStore((s) => s.updateNode) - - // Subscribe to `children` so we re-render when nodes change - void children - const node = activeId ? getNodeById(activeId) : undefined - - const handleUpdate = (updates: Partial) => { - if (activeId) { - updateNode(activeId, updates) - } - } - - if (!node) { - return ( -
-
- - Properties - -
-
-

- Select an element to view its properties. -

-
-
- ) - } - - const hasFill = - node.type !== 'line' && node.type !== 'ref' - const hasStroke = node.type !== 'ref' - const hasCornerRadius = - node.type === 'rectangle' || node.type === 'frame' - const isText = node.type === 'text' - - return ( -
-
- - {node.name ?? node.type} - -
-
- - - {hasFill && ( - - )} - - {hasStroke && ( - - )} - - {hasCornerRadius && ( - - )} - - - - {isText && node.type === 'text' && ( - - )} -
-
- ) -} diff --git a/src/components/panels/SizeSection.tsx b/src/components/panels/SizeSection.tsx deleted file mode 100644 index 7d4fe9128..000000000 --- a/src/components/panels/SizeSection.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import NumberInput from '@/components/shared/NumberInput' -import type { PenNode } from '@/types/pen' - -interface SizeSectionProps { - node: PenNode - onUpdate: (updates: Partial) => void -} - -export default function SizeSection({ node, onUpdate }: SizeSectionProps) { - const x = node.x ?? 0 - const y = node.y ?? 0 - const rotation = node.rotation ?? 0 - - const width = - 'width' in node && typeof node.width === 'number' - ? node.width - : undefined - const height = - 'height' in node && typeof node.height === 'number' - ? node.height - : undefined - - return ( -
-

- Transform -

-
- onUpdate({ x: v })} - /> - onUpdate({ y: v })} - /> - {width !== undefined && ( - - onUpdate({ width: v } as Partial) - } - min={1} - /> - )} - {height !== undefined && ( - - onUpdate({ height: v } as Partial) - } - min={1} - /> - )} -
- onUpdate({ rotation: v })} - suffix="°" - /> -
- ) -} diff --git a/src/components/panels/StrokeSection.tsx b/src/components/panels/StrokeSection.tsx deleted file mode 100644 index 15e53e054..000000000 --- a/src/components/panels/StrokeSection.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import ColorPicker from '@/components/shared/ColorPicker' -import NumberInput from '@/components/shared/NumberInput' -import type { PenNode } from '@/types/pen' -import type { PenStroke, PenFill } from '@/types/styles' - -interface StrokeSectionProps { - stroke?: PenStroke - onUpdate: (updates: Partial) => void -} - -export default function StrokeSection({ - stroke, - onUpdate, -}: StrokeSectionProps) { - const strokeColor = - stroke?.fill && stroke.fill.length > 0 && stroke.fill[0].type === 'solid' - ? stroke.fill[0].color - : '#374151' - - const strokeWidth = - stroke && typeof stroke.thickness === 'number' - ? stroke.thickness - : 0 - - const handleColorChange = (color: string) => { - const newFill: PenFill[] = [{ type: 'solid', color }] - const newStroke: PenStroke = { - ...(stroke ?? { thickness: 1 }), - fill: newFill, - } - onUpdate({ stroke: newStroke } as Partial) - } - - const handleWidthChange = (width: number) => { - const newStroke: PenStroke = { - ...(stroke ?? { - thickness: 1, - fill: [{ type: 'solid', color: strokeColor }], - }), - thickness: width, - } - onUpdate({ stroke: newStroke } as Partial) - } - - return ( -
-

- Stroke -

- - -
- ) -} diff --git a/src/components/panels/TextSection.tsx b/src/components/panels/TextSection.tsx deleted file mode 100644 index a72a6b806..000000000 --- a/src/components/panels/TextSection.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import NumberInput from '@/components/shared/NumberInput' -import DropdownSelect from '@/components/shared/DropdownSelect' -import type { PenNode, TextNode } from '@/types/pen' - -interface TextSectionProps { - node: TextNode - onUpdate: (updates: Partial) => void -} - -const FONT_OPTIONS = [ - { value: 'Inter, sans-serif', label: 'Inter' }, - { value: 'Arial, sans-serif', label: 'Arial' }, - { value: 'Helvetica, sans-serif', label: 'Helvetica' }, - { value: 'Georgia, serif', label: 'Georgia' }, - { value: 'Times New Roman, serif', label: 'Times' }, - { value: 'Courier New, monospace', label: 'Courier' }, -] - -const WEIGHT_OPTIONS = [ - { value: '100', label: 'Thin' }, - { value: '300', label: 'Light' }, - { value: '400', label: 'Regular' }, - { value: '500', label: 'Medium' }, - { value: '600', label: 'Semibold' }, - { value: '700', label: 'Bold' }, - { value: '900', label: 'Black' }, -] - -const ALIGN_OPTIONS = [ - { value: 'left', label: 'Left' }, - { value: 'center', label: 'Center' }, - { value: 'right', label: 'Right' }, - { value: 'justify', label: 'Justify' }, -] - -export default function TextSection({ - node, - onUpdate, -}: TextSectionProps) { - const fontFamily = node.fontFamily ?? 'Inter, sans-serif' - const fontSize = node.fontSize ?? 16 - const fontWeight = String(node.fontWeight ?? '400') - const textAlign = node.textAlign ?? 'left' - - return ( -
-

- Typography -

- onUpdate({ fontFamily: v } as Partial)} - /> -
- - onUpdate({ fontSize: v } as Partial) - } - min={1} - max={999} - /> - - onUpdate({ fontWeight: Number(v) } as Partial) - } - /> -
- - onUpdate({ - textAlign: v as TextNode['textAlign'], - } as Partial) - } - /> -
- ) -} diff --git a/src/components/shared/ColorPicker.tsx b/src/components/shared/ColorPicker.tsx deleted file mode 100644 index 337cd74a9..000000000 --- a/src/components/shared/ColorPicker.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState, useRef, useEffect } from 'react' - -interface ColorPickerProps { - value: string - onChange: (color: string) => void - label?: string -} - -export default function ColorPicker({ - value, - onChange, - label, -}: ColorPickerProps) { - const [hexInput, setHexInput] = useState(value) - const inputRef = useRef(null) - - useEffect(() => { - setHexInput(value) - }, [value]) - - const handleHexChange = (e: React.ChangeEvent) => { - const v = e.target.value - setHexInput(v) - if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(v)) { - onChange(v) - } - } - - const handleNativeChange = ( - e: React.ChangeEvent, - ) => { - onChange(e.target.value) - setHexInput(e.target.value) - } - - const handleBlur = () => { - if (!/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(hexInput)) { - setHexInput(value) - } - } - - return ( -
- {label && ( - {label} - )} -
- -
- -
- ) -} diff --git a/src/components/shared/DropdownSelect.tsx b/src/components/shared/DropdownSelect.tsx deleted file mode 100644 index 51aed1cd1..000000000 --- a/src/components/shared/DropdownSelect.tsx +++ /dev/null @@ -1,34 +0,0 @@ -interface DropdownSelectProps { - value: string - options: { value: string; label: string }[] - onChange: (value: string) => void - label?: string - className?: string -} - -export default function DropdownSelect({ - value, - options, - onChange, - label, - className = '', -}: DropdownSelectProps) { - return ( -
- {label && ( - {label} - )} - -
- ) -} diff --git a/src/components/shared/IconButton.tsx b/src/components/shared/IconButton.tsx deleted file mode 100644 index 90cc59d39..000000000 --- a/src/components/shared/IconButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { ReactNode, ButtonHTMLAttributes } from 'react' - -interface IconButtonProps extends ButtonHTMLAttributes { - icon: ReactNode - label: string - active?: boolean - size?: 'sm' | 'md' -} - -export default function IconButton({ - icon, - label, - active = false, - size = 'md', - className = '', - ...props -}: IconButtonProps) { - const sizeClass = size === 'sm' ? 'p-1' : 'p-1.5' - const activeClass = active - ? 'bg-blue-500 text-white' - : 'text-gray-400 hover:bg-gray-700 hover:text-white' - - return ( - - ) -} diff --git a/src/components/shared/NumberInput.tsx b/src/components/shared/NumberInput.tsx deleted file mode 100644 index 7365dbc37..000000000 --- a/src/components/shared/NumberInput.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useState, useRef, useCallback, useEffect } from 'react' - -interface NumberInputProps { - value: number - onChange: (value: number) => void - min?: number - max?: number - step?: number - label?: string - suffix?: string - className?: string -} - -export default function NumberInput({ - value, - onChange, - min, - max, - step = 1, - label, - suffix, - className = '', -}: NumberInputProps) { - const [localValue, setLocalValue] = useState(String(value)) - const [isDragging, setIsDragging] = useState(false) - const dragStartY = useRef(0) - const dragStartValue = useRef(0) - - useEffect(() => { - if (!isDragging) { - setLocalValue(String(Math.round(value * 100) / 100)) - } - }, [value, isDragging]) - - const clamp = useCallback( - (v: number) => { - let result = v - if (min !== undefined) result = Math.max(min, result) - if (max !== undefined) result = Math.min(max, result) - return result - }, - [min, max], - ) - - const handleBlur = () => { - const parsed = parseFloat(localValue) - if (!isNaN(parsed)) { - onChange(clamp(parsed)) - } else { - setLocalValue(String(value)) - } - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleBlur() - } else if (e.key === 'ArrowUp') { - e.preventDefault() - onChange(clamp(value + step)) - } else if (e.key === 'ArrowDown') { - e.preventDefault() - onChange(clamp(value - step)) - } - } - - const handleMouseDown = (e: React.MouseEvent) => { - if (e.target instanceof HTMLInputElement) return - setIsDragging(true) - dragStartY.current = e.clientY - dragStartValue.current = value - - const handleMouseMove = (ev: MouseEvent) => { - const delta = dragStartY.current - ev.clientY - const newValue = clamp(dragStartValue.current + delta * step) - onChange(newValue) - } - - const handleMouseUp = () => { - setIsDragging(false) - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - } - - return ( -
- {label && ( - - {label} - - )} - setLocalValue(e.target.value)} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - className="w-full bg-gray-700 text-white text-xs px-1.5 py-1 rounded border border-gray-600 focus:border-blue-500 focus:outline-none" - /> - {suffix && ( - {suffix} - )} -
- ) -} diff --git a/src/components/shared/SliderInput.tsx b/src/components/shared/SliderInput.tsx deleted file mode 100644 index d40cb1d53..000000000 --- a/src/components/shared/SliderInput.tsx +++ /dev/null @@ -1,41 +0,0 @@ -interface SliderInputProps { - value: number - onChange: (value: number) => void - min?: number - max?: number - step?: number - label?: string - className?: string -} - -export default function SliderInput({ - value, - onChange, - min = 0, - max = 100, - step = 1, - label, - className = '', -}: SliderInputProps) { - return ( -
- {label && ( - - {label} - - )} - onChange(Number(e.target.value))} - className="flex-1 h-1 accent-blue-500 cursor-pointer" - /> - - {Math.round(value)} - -
- ) -}