Files
sori.studio/components/admin/AdminRowMoreMenu.vue
zenn 02d33996c5 관리자 목록 more vert 메뉴 통일 및 태그 메뉴 정렬 수정
AdminRowMoreMenu 공통 컴포넌트로 글·태그·페이지·미디어·네비게이션 행 액션을 ⋮ 팝오버로 통일.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 14:14:28 +09:00

141 lines
3.8 KiB
Vue

<script setup>
const openMenuId = defineModel('openMenuId', {
type: String,
default: ''
})
const props = defineProps({
/** 이 메뉴 인스턴스의 고유 id */
itemId: {
type: String,
required: true
},
/** 트리거 비활성화 */
disabled: {
type: Boolean,
default: false
},
/** 처리 중(… 표시) */
busy: {
type: Boolean,
default: false
},
/** 접근성 라벨 접두사 */
menuLabel: {
type: String,
default: '메뉴'
},
/** 트리거 크기: md(테이블) | sm(배지·사이드) */
size: {
type: String,
default: 'md'
},
/** 어두운 배경 위 트리거(미디어 폴더 선택 행 등) */
inverse: {
type: Boolean,
default: false
}
})
/** @type {import('vue').ComputedRef<boolean>} */
const isOpen = computed(() => openMenuId.value === props.itemId)
/** @type {import('vue').ComputedRef<string>} */
const triggerSizeClass = computed(() => (props.size === 'sm' ? 'size-7' : 'size-9'))
/** @type {import('vue').ComputedRef<string>} */
const iconSizeClass = computed(() => (props.size === 'sm' ? 'size-5' : 'size-6'))
/** @type {import('vue').ComputedRef<string>} */
const triggerToneClass = computed(() => (
props.inverse
? 'text-white hover:bg-white/15 focus-visible:ring-white/40'
: 'text-[#394047] hover:bg-[#eceff2]'
))
/**
* 메뉴 열기/닫기
* @returns {void}
*/
const toggleMenu = () => {
if (props.disabled) {
return
}
openMenuId.value = isOpen.value ? '' : props.itemId
}
</script>
<template>
<div
class="admin-row-more-menu relative inline-flex justify-end"
data-admin-row-menu
@mousedown.stop
>
<button
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"
:disabled="disabled"
:aria-expanded="isOpen"
aria-haspopup="menu"
:aria-label="isOpen ? `${menuLabel} 닫기` : menuLabel"
@mousedown.stop
@click.stop="toggleMenu"
>
<span
v-if="busy"
class="text-[10px] font-semibold text-muted"
aria-hidden="true"
></span>
<svg
v-else
class="admin-row-more-menu__icon shrink-0"
:class="iconSizeClass"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor"
aria-hidden="true"
>
<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>
</div>
</template>
<style scoped>
.admin-row-more-menu__popover :deep(.admin-row-more-menu__item) {
display: block;
width: 100%;
padding: 0.625rem 1rem;
text-align: left;
font-size: 0.875rem;
line-height: 1.25rem;
color: #3f4650;
}
.admin-row-more-menu__popover :deep(.admin-row-more-menu__item:hover) {
background: #f3f5f7;
}
.admin-row-more-menu__popover :deep(.admin-row-more-menu__item:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
.admin-row-more-menu__popover :deep(.admin-row-more-menu__item--danger) {
color: #c0392b;
}
.admin-row-more-menu__popover :deep(.admin-row-more-menu__item--danger:hover) {
background: #fef2f2;
}
</style>