first commit

This commit is contained in:
sajjadtalkhabi
2026-05-13 01:43:05 +03:30
commit d2a577f254
297 changed files with 36274 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
<template>
<Teleport to="body">
<Transition name="dropdown">
<div v-if="modelValue" ref="floatingEl" class="dropdown-menu" :style="floatingStyle">
<ul class="dropdown-menu__list">
<li
v-for="(item, i) in items"
:key="item.key ?? item.label ?? i"
class="dropdown-menu__item-wrap"
>
<button
type="button"
class="dropdown-menu__item"
:class="{ 'dropdown-menu__item--danger': item.danger }"
:disabled="item.disabled"
@click="handleClick(item)"
>
<SvgIcon v-if="item.icon" :name="item.icon" :size="16" class="dropdown-menu__icon" />
<span class="dropdown-menu__label">{{ item.label }}</span>
</button>
</li>
</ul>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { onBeforeUnmount, ref, watch } from 'vue'
import {
autoUpdate,
computePosition,
flip,
offset as offsetMiddleware,
shift,
} from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
reference: { type: [Object, null], default: null },
items: { type: Array, default: () => [] },
placement: { type: String, default: 'bottom-end' },
offset: { type: Number, default: 8 },
minWidth: { type: String, default: '12rem' },
})
const emit = defineEmits(['update:modelValue', 'select'])
const floatingEl = ref(null)
const floatingStyle = ref({ position: 'fixed', top: '0', left: '0', minWidth: props.minWidth })
let cleanup = null
const close = () => emit('update:modelValue', false)
const mountFloating = async () => {
await Promise.resolve()
const referenceEl = props.reference
const floating = floatingEl.value
if (!referenceEl || !floating) return
cleanup = autoUpdate(referenceEl, floating, () => {
computePosition(referenceEl, floating, {
placement: props.placement,
strategy: 'fixed',
middleware: [offsetMiddleware(props.offset), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
floatingStyle.value = {
position: 'fixed',
top: `${y}px`,
left: `${x}px`,
minWidth: props.minWidth,
}
})
})
document.addEventListener('mousedown', handleOutsideClick)
document.addEventListener('keydown', handleEsc)
}
const unmountFloating = () => {
cleanup?.()
cleanup = null
document.removeEventListener('mousedown', handleOutsideClick)
document.removeEventListener('keydown', handleEsc)
}
const handleOutsideClick = (event) => {
const referenceEl = props.reference
const floating = floatingEl.value
if (referenceEl?.contains(event.target)) return
if (floating?.contains(event.target)) return
close()
}
const handleEsc = (event) => {
if (event.key === 'Escape') close()
}
const handleClick = (item) => {
if (item.disabled) return
emit('select', item)
item.onClick?.(item)
close()
}
watch(
() => props.modelValue,
(open) => {
if (open) {
setTimeout(mountFloating, 0)
} else {
unmountFloating()
}
}
)
onBeforeUnmount(unmountFloating)
</script>
<style lang="scss" scoped>
.dropdown-menu {
z-index: 9999;
background: #fff;
border-radius: 0.875rem;
box-shadow: 0 10px 25px -8px rgba(0, 0, 0, 12%), 0 4px 10px -4px rgba(0, 0, 0, 8%);
padding: 0.25rem;
max-height: 70vh;
overflow-y: auto;
overscroll-behavior: contain;
&__list {
list-style: none;
margin: 0;
padding: 0;
}
&__item-wrap {
& + & {
border-top: 1px solid #ececec;
}
}
&__item {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.75rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 0.625rem;
background: transparent;
cursor: pointer;
font-family: var(--font-family-fa);
font-size: 0.85rem;
color: var(--color-sec-gray, #363636);
transition: background-color 0.15s ease;
&:hover {
background-color: #f7f7f7;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--danger {
color: var(--color-error);
}
}
&__label {
flex: 1;
text-align: right;
white-space: nowrap;
}
&__icon {
flex-shrink: 0;
}
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>