638 lines
21 KiB
TypeScript
638 lines
21 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react'
|
||
import Editor from '@monaco-editor/react'
|
||
import { ComponentDefinition } from '../../@types/componentInfo'
|
||
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
||
import { Check, Code, Loader, MousePointer, Save, Settings, X } from 'lucide-react'
|
||
|
||
interface CodeEditorProps {
|
||
code: string
|
||
onChange: (code: string) => void
|
||
onApplyCodeChanges: (code: string) => void
|
||
onResetCodeChanges: () => void
|
||
language?: string
|
||
theme?: 'vs-dark' | 'light'
|
||
parseError?: string | null
|
||
onCursorChange?: (componentId: string | null) => void
|
||
onDrop?: (componentDef: ComponentDefinition, position: { line: number; column: number }) => void
|
||
onComponentAdded?: (componentDef: ComponentDefinition) => void
|
||
onComponentSave: () => void
|
||
}
|
||
|
||
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||
code,
|
||
onChange,
|
||
onApplyCodeChanges,
|
||
language = 'typescript',
|
||
theme = 'vs-dark',
|
||
onCursorChange,
|
||
onComponentAdded,
|
||
onComponentSave,
|
||
}) => {
|
||
const [localCode, setLocalCode] = useState(code)
|
||
const [hasChanges, setHasChanges] = useState(false)
|
||
const [isFormatting, setIsFormatting] = useState(false)
|
||
const [showSettings, setShowSettings] = useState(false)
|
||
const [editorTheme, setEditorTheme] = useState(theme)
|
||
const [fontSize, setFontSize] = useState(14)
|
||
const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on')
|
||
const [minimap, setMinimap] = useState(true)
|
||
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
|
||
const [isDragOver, setIsDragOver] = useState(false)
|
||
const [dropIndicator, setDropIndicator] = useState<{
|
||
show: boolean
|
||
line: number
|
||
column: number
|
||
}>({ show: false, line: 0, column: 0 })
|
||
|
||
const editorRef = useRef<any>(null)
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
const cursorChangeTimeout = useRef<number | null>(null)
|
||
|
||
useEffect(() => {
|
||
setLocalCode(code)
|
||
setHasChanges(false)
|
||
}, [code])
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (cursorChangeTimeout.current) {
|
||
clearTimeout(cursorChangeTimeout.current)
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
const findComponentIdAtPosition = (
|
||
code: string,
|
||
position: { lineNumber: number; column: number },
|
||
): string | null => {
|
||
const lines = code.split('\n')
|
||
const currentLineIndex = position.lineNumber - 1
|
||
|
||
// Önce bulunduğu satırı kontrol et
|
||
const line = lines[currentLineIndex] || ''
|
||
const idMatch = line.match(/id=["']([\w-]+)["']/)
|
||
if (idMatch) return idMatch[1]
|
||
|
||
// Eğer bulunduğu satırda yoksa, yakındaki satırları kontrol et
|
||
// Önce yukarı doğru ara, sonra aşağı doğru ara
|
||
for (let distance = 1; distance <= 3; distance++) {
|
||
// Yukarı ara
|
||
if (currentLineIndex - distance >= 0) {
|
||
const upLine = lines[currentLineIndex - distance] || ''
|
||
// Component tag başlangıcını kontrol et
|
||
const componentMatch = upLine.match(/<([A-Z][a-zA-Z]*)/)
|
||
if (componentMatch) {
|
||
// Bu component tag'inin ID'sini ara (aynı satırda veya sonraki satırlarda)
|
||
for (
|
||
let j = currentLineIndex - distance;
|
||
j <= Math.min(lines.length - 1, currentLineIndex + 2);
|
||
j++
|
||
) {
|
||
const searchLine = lines[j] || ''
|
||
const foundId = searchLine.match(/id=["']([\w-]+)["']/)
|
||
if (foundId) return foundId[1]
|
||
}
|
||
}
|
||
}
|
||
|
||
// Aşağı ara
|
||
if (currentLineIndex + distance < lines.length) {
|
||
const downLine = lines[currentLineIndex + distance] || ''
|
||
const componentMatch = downLine.match(/<([A-Z][a-zA-Z]*)/)
|
||
if (componentMatch) {
|
||
// Bu component tag'inin ID'sini ara
|
||
for (
|
||
let j = currentLineIndex + distance;
|
||
j <= Math.min(lines.length - 1, currentLineIndex + distance + 2);
|
||
j++
|
||
) {
|
||
const searchLine = lines[j] || ''
|
||
const foundId = searchLine.match(/id=["']([\w-]+)["']/)
|
||
if (foundId) return foundId[1]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Hiçbir şey bulunamadıysa, cursor'ın bulunduğu satırda herhangi bir JSX tag var mı kontrol et
|
||
const jsxMatch = line.match(/<\/?([A-Z][a-zA-Z]*)/)
|
||
if (jsxMatch) {
|
||
// Bu tag'e ait ID'yi bulabilir miyiz?
|
||
const componentName = jsxMatch[1]
|
||
for (
|
||
let i = Math.max(0, currentLineIndex - 5);
|
||
i <= Math.min(lines.length - 1, currentLineIndex + 5);
|
||
i++
|
||
) {
|
||
const searchLine = lines[i] || ''
|
||
if (searchLine.includes(`<${componentName}`) && searchLine.includes('id=')) {
|
||
const foundId = searchLine.match(/id=["']([\w-]+)["']/)
|
||
if (foundId) return foundId[1]
|
||
}
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const handleEditorCursorChange = (_event: any) => {
|
||
if (!editorRef.current) return
|
||
const position = editorRef.current.getPosition()
|
||
if (!position) return
|
||
|
||
// Throttle the cursor change to avoid too many calls
|
||
if (cursorChangeTimeout.current) {
|
||
clearTimeout(cursorChangeTimeout.current)
|
||
}
|
||
const id = findComponentIdAtPosition(localCode, position)
|
||
if (onCursorChange) onCursorChange(id)
|
||
}
|
||
|
||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||
editorRef.current = editor
|
||
|
||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||
allowNonTsExtensions: true,
|
||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||
noEmit: true,
|
||
esModuleInterop: true,
|
||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||
reactNamespace: 'React',
|
||
allowJs: true,
|
||
typeRoots: ['node_modules/@types'],
|
||
})
|
||
|
||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||
handleApplyChanges()
|
||
})
|
||
|
||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, () => {
|
||
handleFormatCode()
|
||
})
|
||
|
||
editor.onDidChangeCursorPosition(handleEditorCursorChange)
|
||
}
|
||
|
||
const handleCodeChange = (value: string | undefined) => {
|
||
if (value !== undefined) {
|
||
setLocalCode(value)
|
||
setHasChanges(value !== code)
|
||
onChange(value)
|
||
}
|
||
}
|
||
|
||
const handleApplyChanges = () => {
|
||
onApplyCodeChanges(localCode)
|
||
setHasChanges(false)
|
||
setShowSuccessMessage(true)
|
||
handleFormatCode()
|
||
setTimeout(() => setShowSuccessMessage(false), 3000)
|
||
}
|
||
|
||
const handleFormatCode = async () => {
|
||
if (editorRef.current) {
|
||
setIsFormatting(true)
|
||
try {
|
||
await editorRef.current.getAction('editor.action.formatDocument').run()
|
||
} catch (error) {
|
||
console.error('Kod formatlama hatası:', error)
|
||
} finally {
|
||
setIsFormatting(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleResetChanges = () => {
|
||
setLocalCode(code)
|
||
setHasChanges(false)
|
||
}
|
||
|
||
// Drag & Drop handlers for Code Editor
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
if (!editorRef.current || !containerRef.current) return
|
||
|
||
try {
|
||
const hasComponentData = e.dataTransfer.types.some(
|
||
(type) => type === 'application/json' || type === 'text/plain' || type === 'text',
|
||
)
|
||
|
||
if (hasComponentData) {
|
||
e.dataTransfer.dropEffect = 'copy'
|
||
setIsDragOver(true)
|
||
|
||
// Get cursor position from mouse coordinates
|
||
const rect = containerRef.current.getBoundingClientRect()
|
||
const x = e.clientX - rect.left
|
||
const y = e.clientY - rect.top
|
||
|
||
// Convert to editor position
|
||
const position = editorRef.current.getTargetAtClientPoint(x, y)
|
||
if (position) {
|
||
setDropIndicator({
|
||
show: true,
|
||
line: position.position.lineNumber,
|
||
column: position.position.column,
|
||
})
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Error handling drag over:', err)
|
||
}
|
||
}
|
||
|
||
const handleDragLeave = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
if (!containerRef.current) return
|
||
|
||
const rect = containerRef.current.getBoundingClientRect()
|
||
const x = e.clientX
|
||
const y = e.clientY
|
||
|
||
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
|
||
setIsDragOver(false)
|
||
setDropIndicator({ show: false, line: 0, column: 0 })
|
||
}
|
||
}
|
||
|
||
const handleDrop = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
setIsDragOver(false)
|
||
setDropIndicator({ show: false, line: 0, column: 0 })
|
||
|
||
if (!editorRef.current || !containerRef.current) return
|
||
|
||
try {
|
||
let componentDefData
|
||
const formats = ['application/json', 'text/plain', 'text']
|
||
|
||
// Sürüklenen bileşenin verisini al
|
||
for (const format of formats) {
|
||
const data = e.dataTransfer.getData(format)
|
||
|
||
if (data) {
|
||
componentDefData = data
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!componentDefData) return
|
||
|
||
let componentDef
|
||
try {
|
||
componentDef = JSON.parse(componentDefData)
|
||
} catch (error) {
|
||
console.error('Component verisi çözümlenemedi:', error)
|
||
return
|
||
}
|
||
|
||
// 1. Component ID'sini oluştur
|
||
const componentId = generateUniqueId()
|
||
|
||
// 2. Props'ları adapt et ve ID'yi dahil et
|
||
const adaptedProps = {
|
||
...(componentDef.properties || []).reduce(
|
||
(acc: any, prop: any) => {
|
||
acc[prop.name] = {
|
||
type: prop.type,
|
||
value: prop.value,
|
||
...(prop.options ? { options: prop.options } : {}),
|
||
}
|
||
return acc
|
||
},
|
||
{} as Record<string, any>,
|
||
),
|
||
id: {
|
||
type: 'string',
|
||
value: componentId,
|
||
},
|
||
}
|
||
|
||
// 3. JSX oluştur
|
||
const componentJSX = generateSingleComponentJSX(componentDef.name, adaptedProps)
|
||
|
||
// 4. Component tanımını ID ile genişlet
|
||
const componentDefWithId = {
|
||
...componentDef,
|
||
id: componentId,
|
||
properties: [
|
||
...(componentDef.properties || []).filter((p: any) => p.name !== 'id'),
|
||
{
|
||
name: 'id',
|
||
type: 'string',
|
||
value: componentId,
|
||
category: 'properties',
|
||
},
|
||
],
|
||
}
|
||
|
||
// 5. Pozisyonu al
|
||
let position = editorRef.current.getPosition()
|
||
if (!position) {
|
||
console.error('Geçersiz pozisyon tespit edildi. Yedek pozisyon kullanılıyor.')
|
||
position = { lineNumber: 0, column: 0 }
|
||
}
|
||
|
||
// 6. JSX kodunu pozisyonda ekle
|
||
const newCode = insertJSXAtPosition(localCode, componentJSX, position)
|
||
setLocalCode(newCode)
|
||
onChange(newCode)
|
||
|
||
// 7. Parent'a bildirim gönder
|
||
if (onComponentAdded) {
|
||
onComponentAdded(componentDefWithId)
|
||
}
|
||
|
||
// 8. Uygulamayı tetikle
|
||
setTimeout(() => {
|
||
onApplyCodeChanges(newCode)
|
||
}, 100)
|
||
} catch (error) {
|
||
console.error('Bileşen bırakılırken hata oluştu:', error)
|
||
}
|
||
}
|
||
|
||
const insertJSXAtPosition = (
|
||
code: string,
|
||
jsx: string,
|
||
position: { lineNumber: number; column: number },
|
||
): string => {
|
||
const updatedCode = code
|
||
const lines = updatedCode.split('\n')
|
||
|
||
// Pozisyonu doğrula (import eklendiyse satır numaraları değişmiş olabilir)
|
||
if (position.lineNumber < 1 || position.lineNumber > lines.length) {
|
||
console.error('Geçersiz satır numarası:', position.lineNumber)
|
||
position.lineNumber = lines.length // Geçersiz satırda son satıra ekle
|
||
}
|
||
|
||
const line = lines[position.lineNumber - 1] // 1 tabanlı indeks, 0 tabanlıya çevrilir
|
||
|
||
if (!line) {
|
||
console.error('Satır bulunamadı:', position.lineNumber)
|
||
return updatedCode // Eğer satır yoksa, kodu değiştirmeden döndür
|
||
}
|
||
|
||
// Satırın indentasyonunu belirle
|
||
const indentMatch = line.match(/^(\s*)/)
|
||
const indent = indentMatch ? indentMatch[1] : ' ' // Default 6 boşluk
|
||
|
||
// Component'i yeni satıra ekle (mevcut satırın altına)
|
||
const formattedJSX = `${indent}${jsx}`
|
||
lines.splice(position.lineNumber, 0, formattedJSX)
|
||
|
||
return lines.join('\n') // Tüm satırları birleştir ve yeni kodu döndür
|
||
}
|
||
|
||
const handleDragEnter = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
}
|
||
|
||
return (
|
||
<div
|
||
ref={containerRef}
|
||
className={`w-full h-full flex flex-col min-h-0 max-h-full bg-gray-900 text-white relative ${
|
||
isDragOver ? 'ring-2 ring-blue-500 ring-offset-2' : ''
|
||
}`}
|
||
style={{ flex: 1, minHeight: 0, maxHeight: '100vh' }}
|
||
onDragOver={handleDragOver}
|
||
onDragEnter={handleDragEnter}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
>
|
||
{/* Drop Indicator */}
|
||
{dropIndicator.show && (
|
||
<div className="absolute inset-0 bg-blue-500 bg-opacity-10 border-2 border-dashed border-blue-500 rounded-lg flex items-center justify-center z-50 pointer-events-none">
|
||
<div className="bg-blue-500 text-white px-4 py-2 rounded-lg font-medium shadow-lg">
|
||
Bileşeni Line {dropIndicator.line}, Column {dropIndicator.column} konumuna bırak
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Header */}
|
||
<div className="bg-gray-800 border-b border-gray-700 p-4 flex items-end justify-between shrink-0">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={handleFormatCode}
|
||
disabled={isFormatting}
|
||
className="px-3 py-2 bg-gray-700 text-gray-300 rounded-lg text-xs font-medium hover:bg-gray-600 transition-colors flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
{isFormatting ? (
|
||
<Loader className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<Code className="w-4 h-4" />
|
||
)}
|
||
Formatla
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowSettings(!showSettings)}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 ${
|
||
showSettings
|
||
? 'bg-blue-600 text-white'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
>
|
||
<Settings className="w-4 h-4" />
|
||
Ayarlar
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleResetChanges}
|
||
className="px-3 py-2 bg-red-600 text-white rounded-lg text-xs font-medium hover:bg-red-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
Sıfırla
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleApplyChanges}
|
||
className="px-3 py-2 bg-green-600 text-white rounded-lg text-xs font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
Uygula
|
||
</button>
|
||
</div>
|
||
<div className="col-span-2 flex items-center justify-end">
|
||
<button
|
||
onClick={onComponentSave}
|
||
className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||
>
|
||
<Save className="w-4 h-4" />
|
||
Kaydet
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Settings Panel */}
|
||
{showSettings && (
|
||
<div className="bg-gray-800 border-b border-gray-700 p-4 shrink-0">
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">Tema</label>
|
||
<select
|
||
value={editorTheme}
|
||
onChange={(e) => setEditorTheme(e.target.value as 'vs-dark' | 'light')}
|
||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="vs-dark">Koyu</option>
|
||
<option value="light">Açık</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">Font Boyutu</label>
|
||
<input
|
||
type="number"
|
||
min="10"
|
||
max="24"
|
||
value={fontSize}
|
||
onChange={(e) => setFontSize(parseInt(e.target.value))}
|
||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Kelime Kaydırma
|
||
</label>
|
||
<select
|
||
value={wordWrap}
|
||
onChange={(e) => setWordWrap(e.target.value as 'on' | 'off')}
|
||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="on">Açık</option>
|
||
<option value="off">Kapalı</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">Mini Harita</label>
|
||
<button
|
||
onClick={() => setMinimap(!minimap)}
|
||
className={`w-full px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||
minimap ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
>
|
||
{minimap ? 'Etkin' : 'Devre Dışı'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Editor */}
|
||
<div className="flex-1 h-full">
|
||
<Editor
|
||
height="100%"
|
||
width="100%"
|
||
language={language}
|
||
theme={editorTheme}
|
||
value={localCode}
|
||
onChange={handleCodeChange}
|
||
onMount={handleEditorDidMount}
|
||
options={{
|
||
fontSize: fontSize,
|
||
wordWrap: wordWrap,
|
||
minimap: { enabled: minimap },
|
||
automaticLayout: true,
|
||
scrollBeyondLastLine: false,
|
||
renderWhitespace: 'selection',
|
||
bracketPairColorization: { enabled: true },
|
||
guides: {
|
||
bracketPairs: true,
|
||
indentation: true,
|
||
},
|
||
suggest: {
|
||
showKeywords: true,
|
||
showSnippets: true,
|
||
showFunctions: true,
|
||
showConstructors: true,
|
||
showFields: true,
|
||
showVariables: true,
|
||
showClasses: true,
|
||
showStructs: true,
|
||
showInterfaces: true,
|
||
showModules: true,
|
||
showProperties: true,
|
||
showEvents: true,
|
||
showOperators: true,
|
||
showUnits: true,
|
||
showValues: true,
|
||
showConstants: true,
|
||
showEnums: true,
|
||
showEnumMembers: true,
|
||
showColors: true,
|
||
showFiles: true,
|
||
showReferences: true,
|
||
showFolders: true,
|
||
showTypeParameters: true,
|
||
showUsers: true,
|
||
showIssues: true,
|
||
},
|
||
quickSuggestions: {
|
||
other: true,
|
||
comments: true,
|
||
strings: true,
|
||
},
|
||
parameterHints: {
|
||
enabled: true,
|
||
},
|
||
hover: {
|
||
enabled: true,
|
||
},
|
||
contextmenu: true,
|
||
mouseWheelZoom: true,
|
||
cursorBlinking: 'smooth',
|
||
cursorSmoothCaretAnimation: 'on',
|
||
smoothScrolling: true,
|
||
folding: true,
|
||
foldingStrategy: 'indentation',
|
||
showFoldingControls: 'always',
|
||
unfoldOnClickAfterEndOfLine: false,
|
||
tabSize: 2,
|
||
insertSpaces: true,
|
||
detectIndentation: true,
|
||
trimAutoWhitespace: true,
|
||
formatOnPaste: true,
|
||
formatOnType: true,
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Status Bar */}
|
||
<div className="bg-gray-800 border-t border-gray-700 px-4 py-2 flex items-center justify-between text-sm text-gray-400 shrink-0">
|
||
<div className="flex items-center gap-4">
|
||
<span>TypeScript React</span>
|
||
<span>UTF-8</span>
|
||
<span>LF</span>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<span>Satır 1, Sütun 1</span>
|
||
<span>Boşluklar: 2</span>
|
||
{hasChanges && <span className="text-orange-400">● Kaydedilmemiş değişiklikler</span>}
|
||
{showSuccessMessage && (
|
||
<span className="text-green-400 flex items-center gap-1">
|
||
<Check className="w-4 h-4" />
|
||
</span>
|
||
)}
|
||
{isDragOver && (
|
||
<span className="text-blue-400 flex items-center gap-1">
|
||
<MousePointer className="w-4 h-4" />
|
||
Bileşeni bırakmaya hazır
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|