Skip to content

Commit

Permalink
Merge pull request #65 from unovue/feat/record
Browse files Browse the repository at this point in the history
add reorder component
  • Loading branch information
rick-hup authored Feb 2, 2025
2 parents 6921c5b + 4a2d7cb commit d14305b
Show file tree
Hide file tree
Showing 27 changed files with 1,113 additions and 16 deletions.
22 changes: 22 additions & 0 deletions docs/components/demo/reorder-layout/AddIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 20 20"
:style="{ transform: 'rotate(45deg)', stroke: 'black' }"
>
<path
d="M 3 3 L 17 17"
fill="transparent"
stroke-width="3"
stroke-linecap="round"
/>
<path
d="M 17 3 L 3 17"
fill="transparent"
stroke-width="3"
stroke-linecap="round"
/>
</svg>
</template>
49 changes: 49 additions & 0 deletions docs/components/demo/reorder-layout/Tab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { Ingredient } from './ingredients'
import { Cross2Icon } from '@radix-icons/vue'
import { ReorderItem, motion } from 'motion-v'
const { item, isSelected } = defineProps<{
item: Ingredient
isSelected: boolean
}>()
defineEmits<{
(e: 'click'): void
(e: 'remove'): void
}>()
</script>

<template>
<ReorderItem
:id="item.label"
:value="item"
:initial="{ opacity: 0, y: 30 }"
:animate="{
opacity: 1,
backgroundColor: isSelected ? '#f3f3f3' : '#fff',
y: 0,
transition: { duration: 0.15 },
}"
:exit="{ opacity: 0, y: 20, transition: { duration: 0.3 } }"
:while-drag="{ backgroundColor: '#e3e3e3' }"
:class="{ selected: isSelected }"
@pointerdown="$emit('click')"
>
<motion.span layout="position">
{{ item.icon }} {{ item.label }}
</motion.span>
<motion.div
layout
class="close"
>
<motion.button
:initial="false"
:animate="{ backgroundColor: isSelected ? '#e3e3e3' : '#fff' }"
@pointerdown.stop="$emit('remove')"
>
<Cross2Icon class="w-4 h-4" />
</motion.button>
</motion.div>
</ReorderItem>
</template>
18 changes: 18 additions & 0 deletions docs/components/demo/reorder-layout/array-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function removeItem<T>([...arr]: T[], item: T) {
const index = arr.indexOf(item)
index > -1 && arr.splice(index, 1)
return arr
}

export function closestItem<T>(arr: T[], item: T) {
const index = arr.indexOf(item)
if (index === -1) {
return arr[0]
}
else if (index === arr.length - 1) {
return arr[arr.length - 2]
}
else {
return arr[index + 1]
}
}
213 changes: 213 additions & 0 deletions docs/components/demo/reorder-layout/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<script setup lang="ts">
import { ref } from 'vue'
import Tab from './Tab.vue'
import AddIcon from './AddIcon.vue'
import {
type Ingredient,
allIngredients,
getNextIngredient,
initialTabs,
} from './ingredients'
import { closestItem, removeItem } from './array-utils'
import { AnimatePresence, ReorderGroup, motion } from 'motion-v'
const tabs = ref(initialTabs)
const selectedTab = ref(tabs.value[0])
function remove(item: Ingredient) {
if (item === selectedTab.value) {
selectedTab.value = closestItem(tabs.value, item)
}
tabs.value = [...removeItem(tabs.value, item)]
}
function add() {
const nextItem = getNextIngredient(tabs.value)
if (nextItem) {
tabs.value = [...tabs.value, nextItem]
selectedTab.value = nextItem
}
}
</script>

<template>
<div class="mx-auto w-[480px] h-[360px] rounded-lg bg-white overflow-hidden shadow-[0_1px_1px_hsl(0deg_0%_0%_/_0.075),0_2px_2px_hsl(0deg_0%_0%_/_0.075),0_4px_4px_hsl(0deg_0%_0%_/_0.075),0_8px_8px_hsl(0deg_0%_0%_/_0.075),0_16px_16px_hsl(0deg_0%_0%_/_0.075)] flex flex-col">
<LayoutGroup>
<nav>
<ReorderGroup
v-model:values="tabs"
tag="ul"
axis="x"
class="tabs"
>
<AnimatePresence
multiple
:initial="false"
>
<Tab
v-for="item in tabs"
:key="item.label"
:item="item"
:data-size="tabs.length"
:is-selected="selectedTab === item"
@click="selectedTab = item"
@remove="remove(item)"
/>
</AnimatePresence>
</ReorderGroup>
<motion.button
class="add-item flex-shrink-0 flex items-center justify-center"
:disabled="tabs.length === allIngredients.length"
:initial="{ scale: 1 }"
:press="{ scale: 0.9 }"
@click="add"
>
<AddIcon />
</motion.button>
</nav>
</LayoutGroup>
<main>
<AnimatePresence
mode="wait"
:initial="false"
>
<motion.div
:key="selectedTab ? selectedTab.label : 'empty'"
:initial="{ opacity: 1, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: -20 }"
:transition="{ duration: 0.15 }"
>
{{ selectedTab ? selectedTab.icon : 'πŸ˜‹' }}
</motion.div>
</AnimatePresence>
</main>
</div>
</template>

<style scoped>
nav {
background: #fdfdfd;
padding: 5px 5px 0;
border-radius: 10px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px solid #eeeeee;
height: 44px;
display: flex;
max-width: 100%;
overflow: hidden;
}
.tabs {
display: flex;
justify-content: flex-start;
align-items: flex-end;
flex-wrap: nowrap;
padding-right: 10px;
flex:1;
overflow: hidden;
}
main {
display: flex;
justify-content: center;
align-items: center;
font-size: 128px;
flex-grow: 1;
user-select: none;
}
:deep(ul),
:deep(li) {
list-style: none;
padding: 0;
margin: 0;
font-family: "Poppins", sans-serif;
font-weight: 500;
font-size: 14px;
}
:deep(li) {
border-radius: 5px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
width: 100%;
padding: 10px 15px;
position: relative;
background: white;
cursor: pointer;
height: 44px;
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
min-width: 0;
overflow: hidden;
position: relative;
user-select: none;
}
:deep(li span) {
flex-shrink: 1;
flex-grow: 1;
line-height: 18px;
white-space: nowrap;
display: block;
min-width: 0;
padding-right: 30px;
mask-image: linear-gradient(to left, transparent 20px, #fff 40px);
-webkit-mask-image: linear-gradient(to left, transparent 20px, #fff 40px);
}
:deep(li .close) {
position: absolute;
top: 0;
bottom: 0;
right: 10px;
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}
:deep(li button) {
width: 20px;
height: 20px;
border: 0;
background: #fff;
border-radius: 3px;
display: flex;
justify-content: center;
align-items: center;
stroke: #000;
margin-left: 10px;
cursor: pointer;
flex-shrink: 0;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 300px;
background: #fff;
}
.add-item {
width: 30px;
height: 30px;
background: #eee;
border-radius: 50%;
border: 0;
cursor: pointer;
align-self: center;
}
.add-item:disabled {
opacity: 0.4;
cursor: default;
pointer-events: none;
}
</style>
24 changes: 24 additions & 0 deletions docs/components/demo/reorder-layout/ingredients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Ingredient {
icon: string
label: string
}

export const allIngredients = [
{ icon: 'πŸ…', label: 'Tomato' },
{ icon: 'πŸ₯¬', label: 'Lettuce' },
{ icon: 'πŸ§€', label: 'Cheese' },
{ icon: 'πŸ₯•', label: 'Carrot' },
{ icon: '🍌', label: 'Banana' },
{ icon: '🫐', label: 'Blueberries' },
{ icon: 'πŸ₯‚', label: 'Champers?' },
]

const [tomato, lettuce, cheese] = allIngredients
export const initialTabs = [tomato, lettuce, cheese]

export function getNextIngredient(
ingredients: Ingredient[],
): Ingredient | undefined {
const existing = new Set(ingredients.map(ingredient => ingredient.label))
return allIngredients.find(ingredient => !existing.has(ingredient.label))
}
32 changes: 32 additions & 0 deletions docs/components/demo/reorder/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ReorderGroup, ReorderItem } from 'motion-v'
const initialItems = ['πŸ… Tomato', 'πŸ₯’ Cucumber', 'πŸ§€ Cheese', 'πŸ₯¬ Lettuce']
const items = ref(initialItems)
function setItems(newItems: string[]) {
items.value = newItems
}
</script>

<template>
<ReorderGroup
v-model:values="items"
axis="y"
class="relative w-[300px]"
>
<ReorderItem
v-for="item in items"
:key="item"
:value="item"
drag
class="rounded-lg select-none list-none mb-2 cursor-grab w-full py-4 px-6 bg-purple-500 justify-between flex flex-shrink-0"
>
{{ item }}
</ReorderItem>
</ReorderGroup>
</template>

<style scoped>
</style>
Loading

0 comments on commit d14305b

Please sign in to comment.