-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #65 from unovue/feat/record
add reorder component
- Loading branch information
Showing
27 changed files
with
1,113 additions
and
16 deletions.
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
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> |
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,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> |
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,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] | ||
} | ||
} |
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,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> |
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,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)) | ||
} |
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,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> |
Oops, something went wrong.