관리자 태그와 목록 메뉴 개선 v1.5.0
This commit is contained in:
@@ -65,6 +65,9 @@ const isLoadingMedia = ref(false)
|
||||
const isUploadingFeaturedImage = ref(false)
|
||||
const isSettingsOpen = ref(true)
|
||||
const tagInput = ref('')
|
||||
const tagInputRef = ref(null)
|
||||
const isTagSuggestionsOpen = ref(false)
|
||||
const activeTagSuggestionIndex = ref(0)
|
||||
const isTagInputComposing = ref(false)
|
||||
const isTitleInputComposing = ref(false)
|
||||
const activeMediaPickerTab = ref('upload')
|
||||
@@ -77,6 +80,12 @@ const publishTiming = ref('now')
|
||||
const scheduledPublishAt = ref('')
|
||||
const publishModalExpandedSection = ref(null)
|
||||
|
||||
const { data: adminTags } = useFetch('/admin/api/tags', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const defaultTagColor = '#15171a'
|
||||
|
||||
/**
|
||||
* ISO 날짜를 datetime-local 입력값으로 변환
|
||||
* @param {string} value - ISO 날짜 문자열
|
||||
@@ -364,6 +373,32 @@ const parseTags = (value) => {
|
||||
|
||||
const selectedTags = computed(() => parseTags(form.tagsText))
|
||||
|
||||
const selectedTagKeys = computed(() => new Set(selectedTags.value.map((tag) => tag.toLowerCase())))
|
||||
|
||||
const availableAdminTags = computed(() => Array.isArray(adminTags.value) ? adminTags.value : [])
|
||||
|
||||
const managedTagOptions = computed(() => availableAdminTags.value.filter((tag) => tag.tagType === 'managed'))
|
||||
|
||||
const tagSuggestionOptions = computed(() => {
|
||||
const keyword = normalizeTagToken(tagInput.value)
|
||||
const sourceTags = keyword ? availableAdminTags.value : managedTagOptions.value
|
||||
|
||||
return sourceTags
|
||||
.filter((tag) => {
|
||||
if (!tag?.slug || selectedTagKeys.value.has(tag.slug.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
return tag.name.toLowerCase().includes(keyword) || tag.slug.toLowerCase().includes(keyword)
|
||||
})
|
||||
.slice(0, 8)
|
||||
})
|
||||
|
||||
const hasTagSuggestions = computed(() => isTagSuggestionsOpen.value && tagSuggestionOptions.value.length > 0)
|
||||
|
||||
/**
|
||||
* 예약 발행 여부 확인
|
||||
* @returns {boolean} 예약 발행 여부
|
||||
@@ -656,16 +691,41 @@ const removeFeaturedImage = () => {
|
||||
* 태그 입력값을 배지 목록에 추가
|
||||
* @returns {void}
|
||||
*/
|
||||
const addTagFromInput = () => {
|
||||
const nextTag = normalizeTagToken(tagInput.value)
|
||||
const addTagToken = (value) => {
|
||||
const nextTag = normalizeTagToken(value)
|
||||
|
||||
if (!nextTag) {
|
||||
tagInput.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
form.tagsText = [...new Set([...selectedTags.value, nextTag])].join(', ')
|
||||
const nextTags = [...selectedTags.value]
|
||||
if (!selectedTagKeys.value.has(nextTag.toLowerCase())) {
|
||||
nextTags.push(nextTag)
|
||||
}
|
||||
|
||||
form.tagsText = nextTags.join(', ')
|
||||
tagInput.value = ''
|
||||
activeTagSuggestionIndex.value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 입력값을 배지 목록에 추가
|
||||
* @returns {void}
|
||||
*/
|
||||
const addTagFromInput = () => {
|
||||
addTagToken(tagInput.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 태그 추천 항목을 배지 목록에 추가
|
||||
* @param {Object} tag - 태그 항목
|
||||
* @returns {void}
|
||||
*/
|
||||
const selectTagSuggestion = (tag) => {
|
||||
addTagToken(tag.slug)
|
||||
isTagSuggestionsOpen.value = false
|
||||
tagInputRef.value?.focus()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -677,6 +737,75 @@ const removeTag = (tag) => {
|
||||
form.tagsText = selectedTags.value.filter((item) => item !== tag).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 슬러그의 화면 표시 이름을 반환한다.
|
||||
* @param {string} slug - 태그 슬러그
|
||||
* @returns {string} 태그 표시 이름
|
||||
*/
|
||||
const getTagDisplayName = (slug) => availableAdminTags.value.find((tag) => tag.slug === slug)?.name || slug
|
||||
|
||||
/**
|
||||
* 태그 슬러그의 고유 색상을 반환한다.
|
||||
* @param {string} slug - 태그 슬러그
|
||||
* @returns {string} 태그 색상
|
||||
*/
|
||||
const getTagColor = (slug) => availableAdminTags.value.find((tag) => tag.slug === slug)?.color || defaultTagColor
|
||||
|
||||
/**
|
||||
* 태그 배지 스타일을 생성한다.
|
||||
* @param {string} color - 태그 색상
|
||||
* @returns {Object} 배지 인라인 스타일
|
||||
*/
|
||||
const createTagBadgeStyle = (color) => ({
|
||||
backgroundColor: `color-mix(in srgb, ${color} 14%, white)`,
|
||||
borderColor: `color-mix(in srgb, ${color} 34%, white)`,
|
||||
color
|
||||
})
|
||||
|
||||
/**
|
||||
* 태그 추천 목록의 보조 라벨을 반환한다.
|
||||
* @param {Object} tag - 태그 항목
|
||||
* @returns {string} 보조 라벨
|
||||
*/
|
||||
const getTagSuggestionMeta = (tag) => {
|
||||
const typeLabel = tag.tagType === 'managed' ? '메인' : '일반'
|
||||
return tag.name.toLowerCase() === tag.slug.toLowerCase() ? typeLabel : `${typeLabel} · ${tag.slug}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 추천 목록을 열고 입력에 포커스한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const openTagSuggestions = () => {
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = 0
|
||||
tagInputRef.value?.focus()
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 추천 목록을 토글한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleTagSuggestions = () => {
|
||||
if (isTagSuggestionsOpen.value) {
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
openTagSuggestions()
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 입력 포커스 해제 처리
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleTagBlur = () => {
|
||||
window.setTimeout(() => {
|
||||
addTagFromInput()
|
||||
isTagSuggestionsOpen.value = false
|
||||
}, 80)
|
||||
}
|
||||
|
||||
/**
|
||||
* 태그 입력 키 처리
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
@@ -687,8 +816,44 @@ const handleTagKeydown = (event) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown' && tagSuggestionOptions.value.length) {
|
||||
event.preventDefault()
|
||||
if (!isTagSuggestionsOpen.value) {
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = 0
|
||||
return
|
||||
}
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = (activeTagSuggestionIndex.value + 1) % tagSuggestionOptions.value.length
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && tagSuggestionOptions.value.length) {
|
||||
event.preventDefault()
|
||||
if (!isTagSuggestionsOpen.value) {
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = tagSuggestionOptions.value.length - 1
|
||||
return
|
||||
}
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = activeTagSuggestionIndex.value === 0
|
||||
? tagSuggestionOptions.value.length - 1
|
||||
: activeTagSuggestionIndex.value - 1
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Escape' && isTagSuggestionsOpen.value) {
|
||||
event.preventDefault()
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ',') {
|
||||
event.preventDefault()
|
||||
if (event.key === 'Enter' && hasTagSuggestions.value) {
|
||||
selectTagSuggestion(tagSuggestionOptions.value[activeTagSuggestionIndex.value] || tagSuggestionOptions.value[0])
|
||||
return
|
||||
}
|
||||
addTagFromInput()
|
||||
return
|
||||
}
|
||||
@@ -1393,8 +1558,10 @@ defineExpose({
|
||||
<h2 class="admin-post-form__settings-title text-xl font-bold text-black">
|
||||
게시물 설정
|
||||
</h2>
|
||||
<button class="admin-post-form__settings-close grid size-8 place-items-center rounded text-[#394047] transition-colors hover:bg-[#eff1f2] hover:text-black" type="button" aria-label="게시물 설정 닫기" @click="toggleSettingsPanel">
|
||||
<span aria-hidden="true">x</span>
|
||||
<button class="admin-post-form__settings-close grid size-8 place-items-center rounded text-neutral-900 transition-colors hover:bg-[#eff1f2] hover:text-neutral-500" type="button" aria-label="게시물 설정 닫기" @click="toggleSettingsPanel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="16" width="16" class="h-[1.1rem] w-[1.1rem]" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-width="0.4" fill="currentColor" stroke="#000000" stroke-linejoin="round" d="M.44,21.44a1.49,1.49,0,0,0,0,2.12,1.5,1.5,0,0,0,2.12,0l9.26-9.26a.25.25,0,0,1,.36,0l9.26,9.26a1.5,1.5,0,0,0,2.12,0,1.49,1.49,0,0,0,0-2.12L14.3,12.18a.25.25,0,0,1,0-.36l9.26-9.26A1.5,1.5,0,0,0,21.44.44L12.18,9.7a.25.25,0,0,1-.36,0L2.56.44A1.5,1.5,0,0,0,.44,2.56L9.7,11.82a.25.25,0,0,1,0,.36Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1443,10 +1610,16 @@ defineExpose({
|
||||
|
||||
<label class="admin-post-form__field grid gap-2 text-sm">
|
||||
<span class="admin-post-form__label font-medium">상태</span>
|
||||
<select v-model="form.status" class="admin-post-form__select h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none">
|
||||
<option value="draft">초안</option>
|
||||
<option value="published">발행</option>
|
||||
</select>
|
||||
<span class="admin-post-form__select-wrap relative block">
|
||||
<select v-model="form.status" class="admin-post-form__select h-[38px] w-full appearance-none rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 pr-10 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none">
|
||||
<option value="draft">초안</option>
|
||||
<option value="published">발행</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-down pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-[#15171a]" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div v-if="form.status === 'published'" class="admin-post-form__field grid gap-2 text-sm">
|
||||
@@ -1504,36 +1677,76 @@ defineExpose({
|
||||
|
||||
<div class="admin-post-form__field grid gap-1 text-sm">
|
||||
<span class="admin-post-form__label h-[22px] font-bold text-[#15171a]">Tags</span>
|
||||
<label class="admin-post-form__tag-editor flex min-h-[38px] w-full items-center gap-2 rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-1 transition-colors hover:border-[#c8ced3] focus-within:border-[#8e9cac]">
|
||||
<span
|
||||
v-for="tag in selectedTags"
|
||||
:key="tag"
|
||||
class="admin-post-form__tag-badge inline-flex h-6 shrink-0 items-center gap-1.5 rounded-[3px] bg-[#ecd2de] px-2 text-sm text-[#e04e87]"
|
||||
>
|
||||
<span>{{ tag }}</span>
|
||||
<button
|
||||
class="admin-post-form__tag-remove inline-flex size-4 shrink-0 items-center justify-center rounded text-[#e04e87] transition-colors hover:bg-[#e7c3d2]"
|
||||
type="button"
|
||||
:aria-label="`${tag} 태그 삭제`"
|
||||
@click="removeTag(tag)"
|
||||
<div class="admin-post-form__tag-combobox relative">
|
||||
<div class="admin-post-form__tag-editor flex min-h-[38px] w-full flex-wrap items-center gap-2 rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-1 transition-colors hover:border-[#c8ced3] focus-within:border-[#8e9cac]">
|
||||
<span
|
||||
v-for="tag in selectedTags"
|
||||
:key="tag"
|
||||
class="admin-post-form__tag-badge inline-flex h-6 shrink-0 items-center gap-1.5 rounded-[3px] border px-2 text-sm font-semibold"
|
||||
:style="createTagBadgeStyle(getTagColor(tag))"
|
||||
>
|
||||
<svg class="size-2.5" version="1" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.707 12L23.854.854a.5.5 0 00-.707-.707L12 11.293.854.146a.5.5 0 00-.707.707L11.293 12 .146 23.146a.5.5 0 00.708.708L12 12.707l11.146 11.146a.5.5 0 10.708-.706L12.707 12z" />
|
||||
<span>{{ getTagDisplayName(tag) }}</span>
|
||||
<button
|
||||
class="admin-post-form__tag-remove inline-flex size-4 shrink-0 items-center justify-center rounded text-current transition-colors hover:bg-white/45"
|
||||
type="button"
|
||||
:aria-label="`${getTagDisplayName(tag)} 태그 삭제`"
|
||||
@click="removeTag(tag)"
|
||||
>
|
||||
<svg class="size-2.5" version="1" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.707 12L23.854.854a.5.5 0 00-.707-.707L12 11.293.854.146a.5.5 0 00-.707.707L11.293 12 .146 23.146a.5.5 0 00.708.708L12 12.707l11.146 11.146a.5.5 0 10.708-.706L12.707 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
ref="tagInputRef"
|
||||
v-model="tagInput"
|
||||
class="admin-post-form__tag-input min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-black outline-none placeholder:text-[#8e9cac]"
|
||||
type="text"
|
||||
placeholder="태그 입력"
|
||||
role="combobox"
|
||||
:aria-expanded="hasTagSuggestions"
|
||||
aria-autocomplete="list"
|
||||
@focus="openTagSuggestions"
|
||||
@input="openTagSuggestions"
|
||||
@blur="handleTagBlur"
|
||||
@keydown="handleTagKeydown"
|
||||
@compositionstart="isTagInputComposing = true"
|
||||
@compositionend="isTagInputComposing = false"
|
||||
>
|
||||
<button
|
||||
class="admin-post-form__tag-dropdown-trigger ml-auto inline-flex size-8 shrink-0 items-center justify-center rounded text-[#15171a] transition-colors hover:bg-[#e3e6e8] focus-visible:outline focus-visible:ring-2 focus-visible:ring-[#8e9cac]"
|
||||
type="button"
|
||||
:aria-expanded="hasTagSuggestions"
|
||||
aria-label="메인 태그 목록 열기"
|
||||
@mousedown.prevent="toggleTagSuggestions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-down size-5" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
v-model="tagInput"
|
||||
class="admin-post-form__tag-input min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-black outline-none placeholder:text-[#8e9cac]"
|
||||
type="text"
|
||||
placeholder="태그 입력"
|
||||
@blur="addTagFromInput"
|
||||
@keydown="handleTagKeydown"
|
||||
@compositionstart="isTagInputComposing = true"
|
||||
@compositionend="isTagInputComposing = false"
|
||||
</div>
|
||||
<div
|
||||
v-if="hasTagSuggestions"
|
||||
class="admin-post-form__tag-suggestions absolute left-0 right-0 top-full z-40 mt-1 max-h-64 overflow-y-auto rounded-lg border border-[#d7dce0] bg-white py-1 text-sm shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
|
||||
role="listbox"
|
||||
>
|
||||
<span class="admin-post-form__tag-chevron text-xs text-[#394047]" aria-hidden="true">⌄</span>
|
||||
</label>
|
||||
<button
|
||||
v-for="(tag, index) in tagSuggestionOptions"
|
||||
:key="tag.id || tag.slug"
|
||||
class="admin-post-form__tag-suggestion flex w-full items-center justify-between gap-3 px-3 py-2 text-left transition-colors"
|
||||
:class="index === activeTagSuggestionIndex ? 'bg-[#f3f5f7]' : 'hover:bg-[#f7f8f9]'"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="index === activeTagSuggestionIndex"
|
||||
@mouseenter="activeTagSuggestionIndex = index"
|
||||
@mousedown.prevent="selectTagSuggestion(tag)"
|
||||
>
|
||||
<span class="admin-post-form__tag-suggestion-name min-w-0 truncate font-semibold text-[#15171a]">{{ tag.name }}</span>
|
||||
<span class="admin-post-form__tag-suggestion-meta shrink-0 text-xs text-muted">{{ getTagSuggestionMeta(tag) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="admin-post-form__featured-toggle flex items-center justify-between gap-4 border-t border-[#e3e6e8] pt-5 text-sm">
|
||||
|
||||
@@ -37,6 +37,9 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const triggerRef = ref(null)
|
||||
const popoverStyle = ref({})
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const isOpen = computed(() => openMenuId.value === props.itemId)
|
||||
|
||||
@@ -53,6 +56,32 @@ const triggerToneClass = computed(() => (
|
||||
: 'text-[#394047] hover:bg-[#eceff2]'
|
||||
))
|
||||
|
||||
/**
|
||||
* 행 메뉴 위치를 화면 기준으로 계산한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updatePopoverPosition = () => {
|
||||
if (!import.meta.client || !triggerRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = triggerRef.value.getBoundingClientRect()
|
||||
const menuWidth = props.size === 'sm' ? 176 : 176
|
||||
const estimatedHeight = 112
|
||||
const margin = 8
|
||||
const left = Math.max(margin, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - margin))
|
||||
const opensUp = rect.bottom + estimatedHeight + margin > window.innerHeight
|
||||
const top = opensUp
|
||||
? Math.max(margin, rect.top - estimatedHeight - 4)
|
||||
: rect.bottom + 4
|
||||
|
||||
popoverStyle.value = {
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${menuWidth}px`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 열기/닫기
|
||||
* @returns {void}
|
||||
@@ -64,6 +93,25 @@ const toggleMenu = () => {
|
||||
|
||||
openMenuId.value = isOpen.value ? '' : props.itemId
|
||||
}
|
||||
|
||||
watch(isOpen, async (open) => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
updatePopoverPosition()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updatePopoverPosition)
|
||||
window.addEventListener('scroll', updatePopoverPosition, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updatePopoverPosition)
|
||||
window.removeEventListener('scroll', updatePopoverPosition, true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,6 +121,7 @@ const toggleMenu = () => {
|
||||
@mousedown.stop
|
||||
>
|
||||
<button
|
||||
ref="triggerRef"
|
||||
class="admin-row-more-menu__trigger inline-flex items-center justify-center rounded transition-colors focus-visible:outline focus-visible:ring-2 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-40"
|
||||
:class="[triggerSizeClass, triggerToneClass]"
|
||||
type="button"
|
||||
@@ -100,13 +149,17 @@ const toggleMenu = () => {
|
||||
<path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="admin-row-more-menu__popover absolute right-0 top-full z-30 mt-1 min-w-[11rem] overflow-hidden rounded-xl border border-[#e2e5e9] bg-white py-2 text-sm text-[#3f4650] shadow-[0_18px_50px_rgba(15,23,42,0.16)]"
|
||||
role="menu"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="admin-row-more-menu__popover fixed z-[80] overflow-hidden rounded-xl border border-[#e2e5e9] bg-white py-2 text-sm text-[#3f4650] shadow-[0_18px_50px_rgba(15,23,42,0.16)]"
|
||||
:style="popoverStyle"
|
||||
role="menu"
|
||||
data-admin-row-menu
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user