mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-26 04:04:22 +01:00
618 lines
23 KiB
TypeScript
618 lines
23 KiB
TypeScript
import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
|
|
import { Tabs } from "@/ui/tabs"
|
|
import FileTree from "@/components/file-tree"
|
|
import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
import { useLocal, useSDK } from "@/context"
|
|
import { Code } from "@/components/code"
|
|
import {
|
|
DragDropProvider,
|
|
DragDropSensors,
|
|
DragOverlay,
|
|
SortableProvider,
|
|
createSortable,
|
|
closestCenter,
|
|
useDragDropContext,
|
|
} from "@thisbeyond/solid-dnd"
|
|
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
|
import type { LocalFile } from "@/context/local"
|
|
import SessionList from "@/components/session-list"
|
|
import SessionTimeline from "@/components/session-timeline"
|
|
|
|
export default function Page() {
|
|
const sdk = useSDK()
|
|
const local = useLocal()
|
|
const [clickTimer, setClickTimer] = createSignal<number | undefined>()
|
|
const [activeItem, setActiveItem] = createSignal<string | undefined>(undefined)
|
|
const [inputValue, setInputValue] = createSignal("")
|
|
const [isDragging, setIsDragging] = createSignal<"left" | "right" | undefined>(undefined)
|
|
const [leftScrolled, setLeftScrolled] = createSignal(false)
|
|
|
|
// TODO: remove
|
|
local.model.set({ providerID: "opencode", modelID: "grok-code" })
|
|
|
|
let inputRef: HTMLInputElement | undefined = undefined
|
|
|
|
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
|
|
|
|
onMount(() => {
|
|
document.addEventListener("keydown", handleKeyDown)
|
|
})
|
|
|
|
onCleanup(() => {
|
|
document.removeEventListener("keydown", handleKeyDown)
|
|
})
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
const inputFocused = document.activeElement === inputRef
|
|
if (inputFocused) {
|
|
if (e.key === "Escape") {
|
|
inputRef?.blur()
|
|
}
|
|
return
|
|
}
|
|
|
|
if (local.file.active()) {
|
|
if (e.getModifierState(MOD)) {
|
|
if (e.key.toLowerCase() === "a") {
|
|
return
|
|
}
|
|
if (e.key.toLowerCase() === "c") {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.key.length === 1 && e.key !== "Unidentified") {
|
|
inputRef?.focus()
|
|
}
|
|
}
|
|
|
|
const navigateChange = (dir: 1 | -1) => {
|
|
const active = local.file.active()
|
|
if (!active) return
|
|
const current = local.file.changeIndex(active.path)
|
|
const next = current == undefined ? (dir === 1 ? 0 : -1) : current + dir
|
|
local.file.setChangeIndex(active.path, next)
|
|
}
|
|
|
|
const resetClickTimer = () => {
|
|
if (!clickTimer()) return
|
|
clearTimeout(clickTimer())
|
|
setClickTimer(undefined)
|
|
}
|
|
|
|
const startClickTimer = () => {
|
|
const newClickTimer = setTimeout(() => {
|
|
setClickTimer(undefined)
|
|
}, 300)
|
|
setClickTimer(newClickTimer as unknown as number)
|
|
}
|
|
|
|
const handleFileClick = async (file: LocalFile) => {
|
|
if (clickTimer()) {
|
|
resetClickTimer()
|
|
local.file.update(file.path, { ...file, pinned: true })
|
|
} else {
|
|
local.file.open(file.path)
|
|
startClickTimer()
|
|
}
|
|
}
|
|
|
|
const handleTabChange = (path: string) => {
|
|
local.file.open(path)
|
|
}
|
|
|
|
const handleTabClose = (file: LocalFile) => {
|
|
local.file.close(file.path)
|
|
}
|
|
|
|
const onDragStart = (event: any) => {
|
|
setActiveItem(event.draggable.id as string)
|
|
}
|
|
|
|
const onDragOver = (event: DragEvent) => {
|
|
const { draggable, droppable } = event
|
|
if (draggable && droppable) {
|
|
const currentFiles = local.file.opened().map((f) => f.path)
|
|
const fromIndex = currentFiles.indexOf(draggable.id.toString())
|
|
const toIndex = currentFiles.indexOf(droppable.id.toString())
|
|
if (fromIndex !== toIndex) {
|
|
local.file.move(draggable.id.toString(), toIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
const onDragEnd = () => {
|
|
setActiveItem(undefined)
|
|
}
|
|
|
|
const handleLeftDragStart = (e: MouseEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging("left")
|
|
const startX = e.clientX
|
|
const startWidth = local.layout.leftWidth()
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const deltaX = e.clientX - startX
|
|
const newWidth = startWidth + deltaX
|
|
local.layout.setLeftWidth(newWidth)
|
|
}
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(undefined)
|
|
document.removeEventListener("mousemove", handleMouseMove)
|
|
document.removeEventListener("mouseup", handleMouseUp)
|
|
}
|
|
|
|
document.addEventListener("mousemove", handleMouseMove)
|
|
document.addEventListener("mouseup", handleMouseUp)
|
|
}
|
|
|
|
const handleRightDragStart = (e: MouseEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging("right")
|
|
const startX = e.clientX
|
|
const startWidth = local.layout.rightWidth()
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const deltaX = startX - e.clientX
|
|
const newWidth = startWidth + deltaX
|
|
local.layout.setRightWidth(newWidth)
|
|
}
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(undefined)
|
|
document.removeEventListener("mousemove", handleMouseMove)
|
|
document.removeEventListener("mouseup", handleMouseUp)
|
|
}
|
|
|
|
document.addEventListener("mousemove", handleMouseMove)
|
|
document.addEventListener("mouseup", handleMouseUp)
|
|
}
|
|
|
|
const handleSubmit = async (e: SubmitEvent) => {
|
|
e.preventDefault()
|
|
const prompt = inputValue()
|
|
setInputValue("")
|
|
inputRef?.blur()
|
|
|
|
const session =
|
|
(local.layout.rightPane() ? local.session.active() : undefined) ??
|
|
(await sdk.session.create().then((x) => x.data!))
|
|
local.session.setActive(session!.id)
|
|
local.layout.openRightPane()
|
|
|
|
const response = await sdk.session.prompt({
|
|
path: { id: session!.id },
|
|
body: {
|
|
agent: local.agent.current()!.name,
|
|
model: local.model.current(),
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: prompt,
|
|
},
|
|
...local.file
|
|
.opened()
|
|
.filter((f) => f.selection || local.file.active()?.path === f.path)
|
|
.flatMap((f) => [
|
|
{
|
|
type: "file" as const,
|
|
mime: "text/plain",
|
|
url: `file://${f.absolute}${f.selection ? `?start=${f.selection.startLine}&end=${f.selection.endLine}` : ""}`,
|
|
filename: f.name,
|
|
source: {
|
|
type: "file" as const,
|
|
text: {
|
|
value: "@" + f.name,
|
|
start: 0, // f.start,
|
|
end: 0, // f.end,
|
|
},
|
|
path: f.absolute,
|
|
},
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
})
|
|
|
|
console.log("response", response)
|
|
}
|
|
|
|
return (
|
|
<div class="relative">
|
|
<div
|
|
class="fixed top-0 left-0 h-full border-r border-border-subtle/30 flex flex-col overflow-hidden"
|
|
style={`width: ${local.layout.leftWidth()}px`}
|
|
>
|
|
<Tabs class="relative flex flex-col h-full" defaultValue="files">
|
|
<div class="sticky top-0 shrink-0 flex">
|
|
<Tabs.List class="grow w-full after:hidden">
|
|
<Tabs.Trigger value="files" class="flex-1 justify-center">
|
|
Files
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="changes" class="flex-1 justify-center">
|
|
Changes
|
|
</Tabs.Trigger>
|
|
</Tabs.List>
|
|
</div>
|
|
<Tabs.Content
|
|
value="files"
|
|
class="grow min-h-0 py-2 bg-background"
|
|
onScroll={(e: Event & { currentTarget: HTMLDivElement }) => setLeftScrolled(e.currentTarget.scrollTop > 0)}
|
|
>
|
|
<FileTree path="" onFileClick={handleFileClick} />
|
|
<Show when={leftScrolled()}>
|
|
<div
|
|
class="pointer-events-none sticky top-20 left-px h-4
|
|
bg-gradient-to-t from-transparent to-background"
|
|
style={`width: ${local.layout.leftWidth() - 2}px`}
|
|
/>
|
|
</Show>
|
|
<div
|
|
class="pointer-events-none fixed bottom-0 left-px h-4
|
|
bg-gradient-to-b from-transparent to-background"
|
|
style={`width: ${local.layout.leftWidth() - 2}px`}
|
|
/>
|
|
</Tabs.Content>
|
|
<Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
|
|
<div class="px-2 text-sm text-text-muted">No changes yet</div>
|
|
</Tabs.Content>
|
|
</Tabs>
|
|
</div>
|
|
<div
|
|
class="fixed top-0 h-full w-1.5 bg-transparent cursor-col-resize z-50 group"
|
|
style={`left: ${local.layout.leftWidth()}px`}
|
|
onMouseDown={(e) => handleLeftDragStart(e)}
|
|
>
|
|
<div
|
|
class="w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors"
|
|
classList={{
|
|
"bg-border-active": isDragging() === "left",
|
|
}}
|
|
/>
|
|
</div>
|
|
<Show when={local.layout.rightPane()}>
|
|
<div
|
|
class="fixed top-0 right-0 h-full border-l border-border-subtle/30 flex flex-col overflow-hidden"
|
|
style={`width: ${local.layout.rightWidth()}px`}
|
|
>
|
|
<div class="relative flex-1 min-h-0 overflow-y-auto no-scrollbar">
|
|
<Show when={local.session.active()} fallback={<SessionList />}>
|
|
{(activeSession) => (
|
|
<div class="relative">
|
|
<div class="sticky top-0 bg-background z-50 p-2 h-9 border-b border-border-subtle/30">
|
|
<div class="flex items-center gap-2">
|
|
<IconButton
|
|
size="xs"
|
|
variant="ghost"
|
|
onClick={() => local.session.clearActive()}
|
|
class="text-text-muted hover:text-text"
|
|
>
|
|
<Icon name="arrow-left" size={14} />
|
|
</IconButton>
|
|
<h2 class="text-sm font-medium text-text truncate">
|
|
{activeSession().title || "Untitled Session"}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
<SessionTimeline session={activeSession().id} />
|
|
</div>
|
|
)}
|
|
</Show>
|
|
<div
|
|
class="pointer-events-none fixed top-0 right-px h-4
|
|
bg-gradient-to-t from-transparent to-background"
|
|
style={`width: ${local.layout.rightWidth() - 2}px`}
|
|
/>
|
|
<div
|
|
class="pointer-events-none fixed bottom-0 right-px h-4
|
|
bg-gradient-to-b from-transparent to-background"
|
|
style={`width: ${local.layout.rightWidth() - 2}px`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="fixed top-0 h-full w-1.5 bg-transparent cursor-col-resize z-50 group flex justify-end"
|
|
style={`right: ${local.layout.rightWidth()}px`}
|
|
onMouseDown={(e) => handleRightDragStart(e)}
|
|
>
|
|
<div
|
|
class="w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors"
|
|
classList={{ "bg-border-active": isDragging() === "right" }}
|
|
/>
|
|
</div>
|
|
</Show>
|
|
<div
|
|
class="relative"
|
|
style={`margin-left: ${local.layout.leftWidth()}px; margin-right: ${local.layout.rightPane() ? local.layout.rightWidth() : 0}px`}
|
|
>
|
|
<Logo
|
|
size={64}
|
|
variant="ornate"
|
|
class="absolute top-2/5 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
|
/>
|
|
<DragDropProvider
|
|
onDragStart={onDragStart}
|
|
onDragEnd={onDragEnd}
|
|
onDragOver={onDragOver}
|
|
collisionDetector={closestCenter}
|
|
>
|
|
<DragDropSensors />
|
|
<ConstrainDragYAxis />
|
|
<Tabs
|
|
class="relative grow w-full flex flex-col h-screen"
|
|
value={local.file.active()?.path}
|
|
onChange={handleTabChange}
|
|
>
|
|
<div class="sticky top-0 shrink-0 flex">
|
|
<Tabs.List class="grow">
|
|
<SortableProvider ids={local.file.opened().map((f) => f.path)}>
|
|
<For each={local.file.opened()}>
|
|
{(file) => <SortableTab file={file} onTabClick={handleFileClick} onTabClose={handleTabClose} />}
|
|
</For>
|
|
</SortableProvider>
|
|
</Tabs.List>
|
|
<div class="shrink-0 h-full flex items-center gap-1 px-2 border-b border-border-subtle/40">
|
|
<Show when={local.file.active() && local.file.active()!.content?.diff}>
|
|
{(() => {
|
|
const f = local.file.active()!
|
|
const view = local.file.view(f.path)
|
|
return (
|
|
<div class="flex items-center gap-1">
|
|
<Show when={view !== "raw"}>
|
|
<div class="mr-1 flex items-center gap-1">
|
|
<Tooltip value="Previous change" placement="bottom">
|
|
<IconButton size="xs" variant="ghost" onClick={() => navigateChange(-1)}>
|
|
<Icon name="arrow-up" size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip value="Next change" placement="bottom">
|
|
<IconButton size="xs" variant="ghost" onClick={() => navigateChange(1)}>
|
|
<Icon name="arrow-down" size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
</Show>
|
|
<Tooltip value="Raw" placement="bottom">
|
|
<IconButton
|
|
size="xs"
|
|
variant="ghost"
|
|
classList={{
|
|
"text-text": view === "raw",
|
|
"text-text-muted/70": view !== "raw",
|
|
"bg-background-element": view === "raw",
|
|
}}
|
|
onClick={() => local.file.setView(f.path, "raw")}
|
|
>
|
|
<Icon name="file-text" size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip value="Unified diff" placement="bottom">
|
|
<IconButton
|
|
size="xs"
|
|
variant="ghost"
|
|
classList={{
|
|
"text-text": view === "diff-unified",
|
|
"text-text-muted/70": view !== "diff-unified",
|
|
"bg-background-element": view === "diff-unified",
|
|
}}
|
|
onClick={() => local.file.setView(f.path, "diff-unified")}
|
|
>
|
|
<Icon name="checklist" size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip value="Split diff" placement="bottom">
|
|
<IconButton
|
|
size="xs"
|
|
variant="ghost"
|
|
classList={{
|
|
"text-text": view === "diff-split",
|
|
"text-text-muted/70": view !== "diff-split",
|
|
"bg-background-element": view === "diff-split",
|
|
}}
|
|
onClick={() => local.file.setView(f.path, "diff-split")}
|
|
>
|
|
<Icon name="columns" size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
)
|
|
})()}
|
|
</Show>
|
|
<Tooltip value={local.layout.rightPane() ? "Close pane" : "Open pane"} placement="bottom">
|
|
<IconButton size="xs" variant="ghost" onClick={() => local.layout.toggleRightPane()}>
|
|
<Icon name={local.layout.rightPane() ? "close-pane" : "open-pane"} size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<For each={local.file.opened()}>
|
|
{(file) => (
|
|
<Tabs.Content value={file.path} class="grow h-full pt-1 select-text">
|
|
{(() => {
|
|
const view = local.file.view(file.path)
|
|
const showRaw = view === "raw" || !file.content?.diff
|
|
const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "")
|
|
return <Code path={file.path} code={code} />
|
|
})()}
|
|
</Tabs.Content>
|
|
)}
|
|
</For>
|
|
</Tabs>
|
|
<DragOverlay>
|
|
{activeItem() &&
|
|
(() => {
|
|
const draggedFile = local.file.node(activeItem()!)
|
|
return (
|
|
<div
|
|
class="relative px-3 h-9 flex items-center
|
|
text-sm font-medium text-text whitespace-nowrap
|
|
shrink-0 bg-background-panel
|
|
border-x border-border-subtle/40 border-b border-b-transparent"
|
|
>
|
|
<TabVisual file={draggedFile} />
|
|
</div>
|
|
)
|
|
})()}
|
|
</DragOverlay>
|
|
</DragDropProvider>
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
class="peer/editor absolute inset-x-4 z-50 flex items-center justify-center"
|
|
classList={{
|
|
"bottom-8": !!local.file.active(),
|
|
"bottom-2/5": local.file.active() === undefined,
|
|
}}
|
|
>
|
|
<div
|
|
class="w-full max-w-xl min-w-0 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
|
|
flex flex-col gap-1
|
|
bg-gradient-to-b from-background-panel/90 to-background/90
|
|
ring-1 ring-border-active/50 border border-transparent
|
|
shadow-[0_0_33px_rgba(0,0,0,0.8)]
|
|
focus-within:ring-2 focus-within:ring-primary/40 focus-within:border-primary"
|
|
>
|
|
<div class="flex flex-wrap gap-1">
|
|
<Show when={local.file.active()}>
|
|
<FileTag
|
|
default
|
|
file={local.file.active()!}
|
|
onClose={() => local.file.close(local.file.active()?.path ?? "")}
|
|
/>
|
|
</Show>
|
|
<For each={local.file.opened().filter((x) => x.selection)}>
|
|
{(file) => <FileTag file={file} onClose={() => local.file.select(file.path, undefined)} />}
|
|
</For>
|
|
</div>
|
|
<input
|
|
ref={(el) => (inputRef = el)}
|
|
type="text"
|
|
value={inputValue()}
|
|
onInput={(e) => setInputValue(e.currentTarget.value)}
|
|
placeholder="It all starts with a prompt..."
|
|
class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none"
|
|
/>
|
|
<div class="px-1 flex justify-between items-center text-xs text-text-muted">
|
|
<span>
|
|
<span class="text-primary uppercase">{local.agent.current()?.name ?? "unknown"}</span> /{" "}
|
|
{local.model.parsed().provider} / {local.model.parsed().model}
|
|
</span>
|
|
<div class="flex gap-1 items-center">
|
|
<IconButton class="text-text-muted" size="xs" variant="ghost">
|
|
<Icon name="photo" size={16} />
|
|
</IconButton>
|
|
<IconButton class="text-background-panel! bg-primary rounded-full!" size="xs" variant="ghost">
|
|
<Icon name="arrow-up" size={14} />
|
|
</IconButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const TabVisual = (props: { file: LocalFile }) => {
|
|
const local = useLocal()
|
|
return (
|
|
<div class="flex items-center gap-x-1.5">
|
|
<FileIcon node={props.file} class="" />
|
|
<span
|
|
classList={{ "text-xs": true, "text-primary": local.file.changed(props.file.path), italic: !props.file.pinned }}
|
|
>
|
|
{props.file.name}
|
|
</span>
|
|
<span class="text-xs opacity-70">
|
|
<Switch>
|
|
<Match when={local.file.status(props.file.path)?.status === "modified"}>
|
|
<span class="text-primary">M</span>
|
|
</Match>
|
|
<Match when={local.file.status(props.file.path)?.status === "added"}>
|
|
<span class="text-success">A</span>
|
|
</Match>
|
|
<Match when={local.file.status(props.file.path)?.status === "deleted"}>
|
|
<span class="text-error">D</span>
|
|
</Match>
|
|
</Switch>
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const SortableTab = (props: {
|
|
file: LocalFile
|
|
onTabClick: (file: LocalFile) => void
|
|
onTabClose: (file: LocalFile) => void
|
|
}) => {
|
|
const sortable = createSortable(props.file.path)
|
|
|
|
return (
|
|
// @ts-ignore
|
|
<div use:sortable classList={{ "opacity-0": sortable.isActiveDraggable }}>
|
|
<Tooltip value={props.file.path} placement="bottom">
|
|
<div class="relative">
|
|
<Tabs.Trigger value={props.file.path} class="peer/tab pr-7" onClick={() => props.onTabClick(props.file)}>
|
|
<TabVisual file={props.file} />
|
|
</Tabs.Trigger>
|
|
<IconButton
|
|
class="absolute right-1 top-2 opacity-0 text-text-muted/60
|
|
peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text
|
|
peer-data-[selected]/tab:hover:bg-border-subtle
|
|
hover:opacity-100 peer-hover/tab:opacity-100"
|
|
size="xs"
|
|
variant="ghost"
|
|
onClick={() => props.onTabClose(props.file)}
|
|
>
|
|
<Icon name="close" size={16} />
|
|
</IconButton>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const FileTag = (props: { file: LocalFile; default?: boolean; onClose: () => void }) => (
|
|
<div
|
|
class="flex items-center bg-background group/tag
|
|
border border-border-subtle/60 border-dashed
|
|
rounded-md text-xs text-text-muted"
|
|
>
|
|
<IconButton class="text-text-muted" size="xs" variant="ghost" onClick={props.onClose}>
|
|
<Switch fallback={<FileIcon node={props.file} class="group-hover/tag:hidden size-3!" />}>
|
|
<Match when={props.default}>
|
|
<Icon name="file" class="group-hover/tag:hidden" size={12} />
|
|
</Match>
|
|
</Switch>
|
|
<Icon name="close" class="hidden group-hover/tag:block" size={12} />
|
|
</IconButton>
|
|
<div class="pr-1 flex gap-1 items-center">
|
|
<span>{props.file.name}</span>
|
|
<Show when={!props.default && props.file.selection}>
|
|
<span class="">
|
|
({props.file.selection!.startLine}-{props.file.selection!.endLine})
|
|
</span>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const ConstrainDragYAxis = () => {
|
|
const context = useDragDropContext()
|
|
if (!context) return <></>
|
|
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
|
|
const transformer: Transformer = {
|
|
id: "constrain-y-axis",
|
|
order: 100,
|
|
callback: (transform) => ({ ...transform, y: 0 }),
|
|
}
|
|
onDragStart((event: any) => {
|
|
addTransformer("draggables", event.draggable.id, transformer)
|
|
})
|
|
onDragEnd((event: any) => {
|
|
removeTransformer("draggables", event.draggable.id, transformer.id)
|
|
})
|
|
return <></>
|
|
}
|