-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fd965b3
commit 3730d81
Showing
10 changed files
with
826 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
--- | ||
title: SortableList | ||
description: An animated sortable list | ||
component: true | ||
links: | ||
--- | ||
|
||
<ComponentPreview | ||
name="sortable-list-demo" | ||
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]" | ||
description="All variations" | ||
/> | ||
|
||
## Installation | ||
|
||
<Tabs defaultValue="manual"> | ||
|
||
<TabsList> | ||
<TabsTrigger value="manual">Manual</TabsTrigger> | ||
</TabsList> | ||
|
||
<TabsContent value="manual"> | ||
|
||
<Steps> | ||
|
||
<Step>Copy and paste the following code into your project.</Step> | ||
|
||
<ComponentSource name="sortable-list" /> | ||
|
||
<Step>Update the import paths to match your project setup.</Step> | ||
|
||
</Steps> | ||
|
||
</TabsContent> | ||
|
||
</Tabs> | ||
|
||
## Usage | ||
|
||
```tsx | ||
import { useState } from "react" | ||
|
||
import { | ||
Item, | ||
SortableList, | ||
SortableListItem, | ||
} from "@/components/ui/sortable-list" | ||
``` | ||
|
||
```tsx | ||
export default function Example() { | ||
const [items, setItems] = useState<Item[]>([ | ||
{ text: "Item 1", checked: false, id: 1, description: "Description 1" }, | ||
{ text: "Item 2", checked: false, id: 2, description: "Description 2" }, | ||
{ text: "Item 3", checked: false, id: 3, description: "Description 3" }, | ||
]) | ||
|
||
const handleAddItem = () => { | ||
const newItem: Item = { | ||
text: `Item ${items.length + 1}`, | ||
checked: false, | ||
id: items.length + 1, | ||
description: `Description ${items.length + 1}`, | ||
} | ||
setItems([...items, newItem]) | ||
} | ||
|
||
const handleResetItems = () => { | ||
setItems([]) | ||
} | ||
|
||
const handleCompleteItem = (id: number) => { | ||
setItems((prevItems) => | ||
prevItems.map((item) => | ||
item.id === id ? { ...item, checked: !item.checked } : item | ||
) | ||
) | ||
} | ||
|
||
const handleRemoveItem = (id: number) => { | ||
setItems((prevItems) => prevItems.filter((item) => item.id !== id)) | ||
} | ||
|
||
const renderItem = ( | ||
item: Item, | ||
onCompleteItem: (id: number) => void, | ||
onRemoveItem: (id: number) => void | ||
) => ( | ||
<SortableListItem | ||
key={item.id} | ||
item={item} | ||
onCompleteItem={onCompleteItem} | ||
onRemoveItem={onRemoveItem} | ||
handleDrag={() => {}} | ||
/> | ||
) | ||
|
||
return ( | ||
<div className="container mx-auto p-4"> | ||
<h1 className="text-2xl font-bold mb-4">Sortable List Example</h1> | ||
<SortableList | ||
items={items} | ||
setItems={setItems} | ||
onAddItem={handleAddItem} | ||
onResetItems={handleResetItems} | ||
onCompleteItem={handleCompleteItem} | ||
renderItem={renderItem} | ||
/> | ||
</div> | ||
) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
apps/www/public/registry/styles/default/sortable-list.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "sortable-list", | ||
"dependencies": [ | ||
"framer-motion, react-use-measure" | ||
], | ||
"files": [ | ||
{ | ||
"name": "sortable-list.tsx", | ||
"content": "\"use client\"\n\n// npx shadcn-ui@latest add checkbox\n// npm i react-use-measure\nimport { Dispatch, ReactNode, SetStateAction, useState } from \"react\"\nimport {\n AnimatePresence,\n LayoutGroup,\n Reorder,\n motion,\n useDragControls,\n} from \"framer-motion\"\nimport { Plus, RepeatIcon, Trash } from \"lucide-react\"\nimport useMeasure from \"react-use-measure\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\n\nexport type Item = {\n text: string\n checked: boolean\n id: number\n description: string\n}\n\ninterface SortableListItemProps {\n item: Item\n onCompleteItem: (id: number) => void\n onRemoveItem: (id: number) => void\n renderExtra?: (item: Item) => React.ReactNode\n isExpanded?: boolean\n className?: string\n handleDrag: () => void\n}\n\nfunction SortableListItem({\n item,\n onCompleteItem,\n onRemoveItem,\n renderExtra,\n handleDrag,\n isExpanded,\n className,\n}: SortableListItemProps) {\n let [ref, bounds] = useMeasure()\n const [isDragging, setIsDragging] = useState(false)\n const [isDraggable, setIsDraggable] = useState(true)\n const dragControls = useDragControls()\n\n const handleDragStart = (event: any) => {\n setIsDragging(true)\n dragControls.start(event, { snapToCursor: true })\n handleDrag()\n }\n\n const handleDragEnd = () => {\n setIsDragging(false)\n }\n\n return (\n <motion.div\n className={cn(\n \"flex w-full items-center justify-between gap-2\",\n className\n )}\n key={item.id}\n >\n <div className=\"flex w-full items-center justify-between gap-0\">\n <Reorder.Item\n value={item}\n className={cn(\n \"relative z-auto grow\",\n \"shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)]\",\n item.checked ? \"cursor-not-allowed\" : \"cursor-grab\",\n \"h-full rounded-2xl border border-white/10 bg-[#141712]\",\n item.checked && !isDragging ? \"w-7/10\" : \"w-full\"\n )}\n key={item.id}\n initial={{ opacity: 0 }}\n animate={{\n opacity: 1,\n height: bounds.height > 0 ? bounds.height : undefined,\n transition: { type: \"spring\", bounce: 0, duration: 0.3 },\n }}\n exit={{\n opacity: 0,\n transition: {\n delay: 0,\n duration: 0.05,\n type: \"spring\",\n bounce: 0,\n },\n }}\n layout\n layoutId={`item-${item.id}`}\n dragListener={!item.checked}\n dragControls={dragControls}\n onDragEnd={handleDragEnd}\n style={\n isExpanded\n ? {\n zIndex: 9999,\n marginTop: 10,\n marginBottom: 10,\n position: \"relative\",\n overflow: \"hidden\",\n }\n : {\n position: \"relative\",\n overflow: \"hidden\",\n }\n }\n whileDrag={{ zIndex: 9999 }}\n >\n <div ref={ref} className=\"z-20\">\n <motion.div layout=\"position\" className=\"flex items-center gap-4 \">\n <AnimatePresence>\n {!isExpanded ? (\n <motion.div\n initial={{ opacity: 0, filter: \"blur(4px)\" }}\n animate={{ opacity: 1, filter: \"blur(0px)\" }}\n exit={{ opacity: 0, filter: \"blur(4px)\" }}\n transition={{ duration: 0.001 }}\n className=\"flex w-full items-center gap-4 p-1\"\n >\n <Checkbox\n checked={item.checked}\n id={`checkbox-${item.id}`}\n aria-label=\"Mark as done\"\n onCheckedChange={() => onCompleteItem(item.id)}\n className=\"ml-3 h-5 w-5 rounded-md border-neutral-400/80 bg-black \"\n />\n <motion.span className=\"w-full px-1 text-lg tracking-tight text-neutral-300/90\">\n {item.text}\n </motion.span>\n </motion.div>\n ) : null}\n </AnimatePresence>\n {renderExtra && renderExtra(item)}\n </motion.div>\n </div>\n <div\n onPointerDown={isDraggable ? handleDragStart : undefined}\n style={{ touchAction: \"none\" }}\n />\n </Reorder.Item>\n <AnimatePresence mode=\"popLayout\">\n {item.checked ? (\n <motion.div\n layout\n initial={{ opacity: 0, x: -1 }}\n animate={{\n opacity: 1,\n x: 0,\n transition: {\n delay: 0.17,\n duration: 0.2,\n type: \"spring\",\n bounce: 0.3,\n },\n zIndex: 5,\n }}\n exit={{\n opacity: 0,\n x: -5,\n transition: {\n delay: 0,\n duration: 0.05,\n type: \"spring\",\n bounce: 0,\n },\n }}\n className=\"-ml-2 h-[3.1rem] w-4 rounded-l-sm rounded-r-md border-y border-r border-y-white/10 border-r-white/10 bg-[#141712] shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)]\"\n />\n ) : null}\n </AnimatePresence>\n <AnimatePresence mode=\"popLayout\">\n {item.checked ? (\n <motion.div\n layout\n initial={{ opacity: 0, x: -5, filter: \"blur(4px)\" }}\n animate={{\n opacity: 1,\n x: 0,\n filter: \"blur(0px)\",\n transition: {\n delay: 0.3,\n duration: 0.15,\n type: \"spring\",\n bounce: 0.9,\n },\n }}\n exit={{\n opacity: 0,\n filter: \"blur(4px)\",\n x: -10,\n transition: { delay: 0, duration: 0.12 },\n }}\n className=\"inset-0 z-0 border-spacing-1 rounded-r-xl border-y border-r-2 border-y-white/10 border-r-red-300 bg-[#141712]/80 shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)] dark:bg-[#141712]/50\"\n >\n <button\n className=\" group inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-3 text-sm font-medium ring-offset-background transition-colors duration-150 hover:bg-[#141712] hover:text-red-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\"\n onClick={() => onRemoveItem(item.id)}\n >\n <Trash className=\"h-4 w-4 text-red-400 transition-colors duration-150 group-hover:fill-red-400/60\" />\n </button>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n </motion.div>\n )\n}\n\nSortableListItem.displayName = \"SortableListItem\"\n\ninterface SortableListProps {\n items: Item[]\n setItems: Dispatch<SetStateAction<Item[]>>\n onAddItem: () => void\n onResetItems: () => void\n onCompleteItem: (id: number) => void\n renderItem: (\n item: Item,\n onCompleteItem: (id: number) => void,\n onRemoveItem: (id: number) => void\n ) => ReactNode\n}\n\nfunction SortableList({\n items,\n setItems,\n onAddItem,\n onResetItems,\n onCompleteItem,\n renderItem,\n}: SortableListProps) {\n if (items) {\n return (\n <div className=\"mb-9 rounded-2xl border border-black/5 p-2 shadow-sm md:p-6 \">\n <div className=\" overflow-auto p-4\">\n <div className=\"flex flex-col space-y-2\">\n <div className=\"\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"256\"\n height=\"260\"\n preserveAspectRatio=\"xMidYMid\"\n viewBox=\"0 0 256 260\"\n className=\"h-6 w-6\"\n >\n <path d=\"M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z\" />\n </svg>\n <h3>Agent workflow</h3>\n </div>\n <div className=\"flex items-center justify-between gap-4\">\n <button\n className=\"flex items-center gap-1 rounded-md border border-black/10 p-2 disabled:opacity-50\"\n disabled={items?.length > 5}\n onClick={onAddItem}\n >\n <Plus className=\"dark:text-netural-100 h-4 w-4 text-neutral-800\" />\n New stage\n </button>\n <div data-tip=\"Reset task list\">\n <button onClick={onResetItems}>\n <RepeatIcon className=\"dark:text-netural-100 h-4 w-4 text-neutral-800\" />\n </button>\n </div>\n </div>\n <LayoutGroup>\n <Reorder.Group\n axis=\"y\"\n values={items}\n onReorder={setItems}\n className=\"flex flex-col\"\n >\n <AnimatePresence>\n {items?.map((item) =>\n renderItem(item, onCompleteItem, (id: number) =>\n setItems((items) =>\n items.filter((item) => item.id !== id)\n )\n )\n )}\n </AnimatePresence>\n </Reorder.Group>\n </LayoutGroup>\n </div>\n </div>\n </div>\n )\n }\n return null\n}\n\nSortableList.displayName = \"SortableList\"\n\nexport { SortableList, SortableListItem }\nexport default SortableList\n" | ||
} | ||
], | ||
"type": "components:ui" | ||
} |
Oops, something went wrong.