Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/react": "^0.3.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

dnd-kit npm packages @dnd-kit/core @dnd-kit/sortable @dnd-kit/react compatibility 2025 2026

💡 Result:

Compatibility (2025–2026)

1) “Legacy” dnd-kit (stable)

  • Use @dnd-kit/core + @dnd-kit/sortable together (Sortable is built on top of Core). [1][2]
  • Latest visible package versions (as of the sources crawled): @dnd-kit/core 6.3.1 and @dnd-kit/sortable 10.0.0. [3][2]

2) “New” dnd-kit (rewrite)

  • @dnd-kit/react is the new React API layer (published as 0.x, e.g. 0.1.21 in the npm listing snippet). [4]
  • The official migration guide says to migrate from @dnd-kit/core/@dnd-kit/sortable/@dnd-kit/utilities to @dnd-kit/react (and @dnd-kit/helpers), i.e. it’s a replacement track, not an add-on. [5]
  • The legacy docs explicitly note there’s a new version and recommend using the latest instead of the legacy install path. [6]

Practical takeaway

  • Don’t mix @dnd-kit/react (new 0.x line) with the legacy @dnd-kit/core / @dnd-kit/sortable stack in the same implementation—pick one track:
    • Legacy: @dnd-kit/core + @dnd-kit/sortable [1][2]
    • New: @dnd-kit/react (+ @dnd-kit/helpers, plus other new-scope packages as needed) [5][4]

Sources: [1] @dnd-kit Sortable docs [2] Snyk package info for @dnd-kit/sortable [3] npm listing snippet for @dnd-kit/core [4] npm listing snippet for @dnd-kit/react [5] official migration guide (core → react) [6] legacy installation page noting a new version is available.


🏁 Script executed:

# Find and examine sortable.tsx to see which APIs are actually imported
fd -e tsx -e ts | xargs rg -l "sortable.tsx|useSortable|SortableContext" | head -5

Repository: getdokan/plugin-ui

Length of output: 92


🏁 Script executed:

# Also check the actual package.json to see the full context
cat -n package.json | sed -n '50,75p'

Repository: getdokan/plugin-ui

Length of output: 1084


🏁 Script executed:

cat -n src/components/ui/sortable.tsx | head -50

Repository: getdokan/plugin-ui

Length of output: 1696


🏁 Script executed:

# Search for imports from dnd-kit packages
rg "from ['\"]@dnd-kit" src/components/ui/sortable.tsx -A 1 -B 1

Repository: getdokan/plugin-ui

Length of output: 270


🏁 Script executed:

rg "@dnd-kit/react" --type ts --type tsx

Repository: getdokan/plugin-ui

Length of output: 89


🏁 Script executed:

rg "@dnd-kit/react" --type typescript

Repository: getdokan/plugin-ui

Length of output: 96


🏁 Script executed:

# Also try searching without type restriction
rg "@dnd-kit/react" src/

Repository: getdokan/plugin-ui

Length of output: 44


Remove unused @dnd-kit/react dependency — incompatible with the legacy dnd-kit stack in use.

The codebase uses the legacy dnd-kit track (@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities), which is explicitly incompatible with @dnd-kit/react. The latter is a replacement API from a complete dnd-kit rewrite (available as 0.x pre-release) and should not be mixed with the legacy packages. A search of the entire codebase confirms @dnd-kit/react is never imported or used anywhere—only the legacy packages are referenced in sortable.tsx. Remove @dnd-kit/react@^0.3.0 from dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 60 - 64, Remove the unused and incompatible
dependency entry "@dnd-kit/react": "^0.3.0" from package.json (it's not imported
anywhere and conflicts with the legacy dnd-kit packages like "@dnd-kit/core" and
"@dnd-kit/sortable"); delete that line from the dependencies block, then update
your lockfile by reinstalling (run npm install or yarn install) so the lockfile
and node_modules are consistent.

"@wordpress/dataviews": "^11.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down Expand Up @@ -103,4 +108,4 @@
"type": "git",
"url": "https://github.com/getdokan/plugin-ui.git"
}
}
}
362 changes: 362 additions & 0 deletions src/components/ui/Sortable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { GripVertical, GripHorizontal, X, Pencil, Check } from "lucide-react";
import {
verticalListSortingStrategy,
horizontalListSortingStrategy,
rectSortingStrategy,
} from "@dnd-kit/sortable";

import {
Sortable,
SortableItem,
SortableDragHandle,
SortableProvider,
} from "./sortable";
import { Button } from "./button";
import { Input } from "./input";
import { Switch } from "./switch";
import { cn } from "@/lib/utils";

const meta = {
title: "UI/Sortable",
component: Sortable,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof Sortable>;

export default meta;

type Story = StoryObj<typeof meta>;

const initialItems = [
{ id: "1", content: "Item 1" },
{ id: "2", content: "Item 2" },
{ id: "3", content: "Item 3" },
{ id: "4", content: "Item 4" },
{ id: "5", content: "Item 5" },
];

export const AdvancedCustomUI: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState([
{ id: "1", label: "Dashboard", checked: false },
{ id: "2", label: "Products", checked: true },
{ id: "3", label: "Address", checked: true },
{ id: "4", label: "Request Quotes", checked: true },
{ id: "5", label: "Coupons", checked: true },
{ id: "6", label: "Reports", checked: true },
{ id: "7", label: "Delivery Time", checked: true },
{ id: "8", label: "Reviews", checked: true },
{ id: "9", label: "Withdraw", checked: false },
]);
const [editingId, setEditingId] = useState<string | null>("3");

const toggleChecked = (id: string) => {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, checked: !item.checked } : item
)
);
};

return (
<div className="w-[600px] overflow-hidden rounded-lg border bg-background shadow-sm">
<Sortable items={items} onValueChange={setItems}>
<div className="flex flex-col">
{items.map((item) => (
<SortableItem
key={item.id}
id={item.id}
useHandle
className="group border-b last:border-0"
>
<div className="flex h-14 items-center gap-4 px-4 py-2 transition-colors hover:bg-muted/30">
<SortableDragHandle className="text-muted-foreground/50 hover:text-foreground">
<GripVertical className="size-5" />
</SortableDragHandle>

<div className="flex flex-1 items-center">
{editingId === item.id ? (
<div className="relative flex-1">
<Input
defaultValue={item.label}
className="h-9 border-primary/50 ring-primary/20 focus-visible:border-primary focus-visible:ring-primary/30"
autoFocus
/>
<button
onClick={() => setEditingId(null)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
Comment on lines +85 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Icon-only <button> is missing an aria-label.

The native <button> wrapping the X icon (cancel editing) has no accessible name. Screen readers will announce it without meaningful context.

♿ Proposed fix
-            <button
-              onClick={() => setEditingId(null)}
-              className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
-            >
+            <button
+              onClick={() => setEditingId(null)}
+              aria-label="Cancel editing"
+              className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+            >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setEditingId(null)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
<button
onClick={() => setEditingId(null)}
aria-label="Cancel editing"
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 85 - 90, The icon-only
<button> that calls setEditingId(null) and renders the X component lacks an
accessible name; update that button to include an appropriate aria-label (e.g.,
aria-label="Cancel editing" or "Close") so screen readers get meaningful context
for the action triggered by the X icon, and ensure the X icon itself remains
decorative (aria-hidden) if you add the label.

</div>
) : (
<span className="text-sm font-medium">{item.label}</span>
)}
</div>

<div className="flex items-center gap-3">
{editingId === item.id ? (
<Button
variant="outline"
size="icon-sm"
className="h-8 w-8 border-muted"
onClick={() => setEditingId(null)}
>
<Check className="size-4 text-muted-foreground" />
</Button>
Comment on lines +78 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edits in AdvancedCustomUI are silently discarded — demo implies saving but never updates state.

The Input uses defaultValue (uncontrolled), so its value is never read back. The "save" action (Check button, line 99 and the X button, line 86) only calls setEditingId(null) without committing the new label to items state. Users interacting with the demo will type a label change, click save, and see it revert — which misrepresents the editing capability.

Either make it a read-only view-mode toggle (remove the Input entirely), or wire up a controlled flow:

🐛 Proposed fix to persist edits
+      const [editValues, setEditValues] = useState<Record<string, string>>(
+        Object.fromEntries(items.map((i) => [i.id, i.label]))
+      );
+
+      const saveEdit = (id: string) => {
+        setItems((prev) =>
+          prev.map((item) =>
+            item.id === id ? { ...item, label: editValues[id] } : item
+          )
+        );
+        setEditingId(null);
+      };

         {editingId === item.id ? (
           <div className="relative flex-1">
             <Input
-              defaultValue={item.label}
+              value={editValues[item.id]}
+              onChange={(e) =>
+                setEditValues((prev) => ({ ...prev, [item.id]: e.target.value }))
+              }
               className="..."
               autoFocus
             />
             <button
-              onClick={() => setEditingId(null)}
+              onClick={() => saveEdit(item.id)}
               ...
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 78 - 106, The edit UI is
currently uncontrolled and never persists changes: the Input uses defaultValue
and the save/cancel handlers (setEditingId in the X and Check button handlers)
only close edit mode without updating the items state. Change to a controlled
edit flow by adding a temporary edit value (e.g., editingValue) when entering
edit mode for the specific item (set editingValue = item.label in the handler
that sets editingId), bind that editingValue to the Input (onChange updates
editingValue), and on the Check button click update the items state (use
setItems to map the item with matching item.id and replace its label with
editingValue) then setEditingId(null); keep the X button to discard changes by
just clearing editingId (and optionally clearing editingValue).

) : (
<Button
variant="ghost"
size="icon-sm"
className="h-8 w-8 text-muted-foreground/50 hover:bg-muted hover:text-foreground"
onClick={() => setEditingId(item.id)}
>
<Pencil className="size-4" />
</Button>
)}
<Switch
checked={item.checked}
onCheckedChange={() => toggleChecked(item.id)}
/>
</div>
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const Vertical: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState(initialItems);
return (
<div className="w-80">
<Sortable items={items} onValueChange={setItems}>
<div className="flex flex-col gap-2">
{items.map((item) => (
<SortableItem key={item.id} id={item.id}>
<div className="flex items-center justify-between rounded-md border bg-card p-4 shadow-sm">
<span>{item.content}</span>
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const WithDragHandle: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState(initialItems);
return (
<div className="w-80">
<Sortable items={items} onValueChange={setItems}>
<div className="flex flex-col gap-2">
{items.map((item) => (
<SortableItem key={item.id} id={item.id} useHandle>
<div className="flex items-center gap-3 rounded-md border bg-card p-3 shadow-sm">
<SortableDragHandle className="text-muted-foreground hover:text-foreground">
<GripVertical className="size-5" />
</SortableDragHandle>
<span className="flex-1 font-medium">{item.content}</span>
<Button variant="ghost" size="icon-sm">
<X className="size-4" />
</Button>
Comment on lines +173 to +175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove/action Button in WithDragHandle has no onClick handler.

The X icon button appears interactive but does nothing when clicked. In a demo, this creates confusion about whether removal is supported. Either wire up a remove handler or replace it with a non-interactive element.

🐛 Proposed fix to add a remove handler
-                    <Button variant="ghost" size="icon-sm">
+                    <Button
+                      variant="ghost"
+                      size="icon-sm"
+                      aria-label="Remove item"
+                      onClick={() =>
+                        setItems((prev) => prev.filter((i) => i.id !== item.id))
+                      }
+                    >
                       <X className="size-4" />
                     </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button variant="ghost" size="icon-sm">
<X className="size-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Remove item"
onClick={() =>
setItems((prev) => prev.filter((i) => i.id !== item.id))
}
>
<X className="size-4" />
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 173 - 175, In the
WithDragHandle story in Sortable.stories.tsx the remove/action Button (the X
icon) is missing an onClick handler; either wire that Button up to call the
story's remove function (e.g., call an existing handleRemove or onRemove item
callback used by the Sortable list) or add a local handler that updates the
story's items state to remove the corresponding item id when clicked; if removal
isn't supported in this story, replace the interactive Button with a
non-interactive element (e.g., a span) to avoid implying interactivity. Ensure
you modify the Button inside the WithDragHandle render so it receives
onClick={() => removeItem(id)} (or similar) and that removeItem is defined and
wired to the same state/props managing the list.

</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const Horizontal: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState(initialItems);
return (
<div className="w-full max-w-2xl overflow-x-auto p-4">
<Sortable
items={items}
onValueChange={setItems}
strategy={horizontalListSortingStrategy}
>
<div className="flex gap-3">
{items.map((item) => (
<SortableItem key={item.id} id={item.id} className="shrink-0">
<div className="flex h-20 w-32 items-center justify-center rounded-lg border bg-card shadow-sm">
{item.content}
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const Grid: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState([
...initialItems,
{ id: "6", content: "Item 6" },
{ id: "7", content: "Item 7" },
{ id: "8", content: "Item 8" },
{ id: "9", content: "Item 9" },
]);
return (
<div className="w-[500px] p-4">
<Sortable
items={items}
onValueChange={setItems}
strategy={rectSortingStrategy}
>
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<SortableItem key={item.id} id={item.id}>
<div className="flex aspect-square items-center justify-center rounded-xl border-2 bg-card text-lg font-bold shadow-sm transition-colors hover:border-primary/50">
{item.content.split(" ")[1]}
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const CustomGapsAndPadding: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState(initialItems);
return (
<div className="rounded-xl border bg-muted/30 p-8 shadow-inner">
<Sortable items={items} onValueChange={setItems}>
<div className="flex flex-col gap-6">
{items.map((item) => (
<SortableItem key={item.id} id={item.id} useHandle>
<div className="group flex items-center gap-4 rounded-2xl bg-background p-6 shadow-md transition-all hover:shadow-lg">
<SortableDragHandle className="rounded-lg bg-muted p-2 text-muted-foreground transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<GripVertical className="size-6" />
</SortableDragHandle>
<div className="flex-1">
<h4 className="font-bold">{item.content}</h4>
<p className="text-sm text-muted-foreground">Customizable padding and gaps</p>
</div>
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const DragOverlayExample: Story = {
render: () => {
function Demo() {
const [items, setItems] = useState(initialItems);
return (
<div className="w-80">
<Sortable
items={items}
onValueChange={setItems}
overlay={
<div className="flex items-center gap-3 rounded-md border border-primary bg-primary/10 p-3 shadow-xl backdrop-blur-sm">
<GripVertical className="size-5 text-primary" />
<span className="font-bold text-primary">Dragging...</span>
</div>
}
>
<div className="flex flex-col gap-2">
{items.map((item) => (
<SortableItem key={item.id} id={item.id} useHandle>
<div className="flex items-center gap-3 rounded-md border bg-card p-3 shadow-sm">
<SortableDragHandle className="text-muted-foreground">
<GripVertical className="size-5" />
</SortableDragHandle>
<span className="flex-1">{item.content}</span>
</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
);
}
return <Demo />;
},
};

export const MultipleLists: Story = {
render: () => {
function Demo() {
const [itemsA, setItemsA] = useState([
{ id: "A1", content: "A1" },
{ id: "A2", content: "A2" },
]);
const [itemsB, setItemsB] = useState([
{ id: "B1", content: "B1" },
{ id: "B2", content: "B2" },
]);

return (
<div className="flex gap-8 p-4">
<div className="w-48 space-y-4">
<h3 className="font-bold">List A</h3>
<Sortable items={itemsA} onValueChange={setItemsA}>
<div className="min-h-[100px] rounded-lg border bg-muted/50 p-2 space-y-2">
{itemsA.map(item => (
<SortableItem key={item.id} id={item.id}>
<div className="rounded border bg-card p-2 shadow-sm text-center">{item.content}</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
<div className="w-48 space-y-4">
<h3 className="font-bold">List B</h3>
<Sortable items={itemsB} onValueChange={setItemsB}>
<div className="min-h-[100px] rounded-lg border bg-muted/50 p-2 space-y-2">
{itemsB.map(item => (
<SortableItem key={item.id} id={item.id}>
<div className="rounded border bg-card p-2 shadow-sm text-center">{item.content}</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
</div>
);
}
return <Demo />;
},
};
Comment on lines +316 to +359

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

MultipleLists story doesn't demonstrate cross-list dragging.

Each list has its own independent <Sortable> (and thus its own DndContext), so items can only be reordered within each list, not dragged between them. If cross-list DnD is intended, both lists would need to share a single SortableProvider. If the intent is just "two independent sortable lists on one page," consider renaming to something like IndependentLists or SideBySideLists to set correct expectations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 319 - 362, The
MultipleLists story uses two independent Sortable components (each with its own
DndContext) so items cannot be dragged between lists; either change the
implementation to share a single SortableProvider/DndContext around both lists
to enable cross-list dragging (wrap both lists with a common SortableProvider or
move a single DndContext to encompass both Sortable instances and coordinate on
a shared onValueChange handler for itemsA/itemsB and SortableItem ids), or if
cross-list dragging is not desired, rename MultipleLists to IndependentLists or
SideBySideLists to make behavior clear; update the story accordingly
(referencing MultipleLists, Sortable, SortableProvider/DndContext, SortableItem,
itemsA/itemsB, setItemsA/setItemsB).

Loading
Loading