|
|
|
|
@@ -90,6 +90,7 @@ let editorLoadToken = 0
|
|
|
|
|
const boardEl = ref(null)
|
|
|
|
|
const exportBoardEl = ref(null)
|
|
|
|
|
const groupListEl = ref(null)
|
|
|
|
|
const sidebarEl = ref(null)
|
|
|
|
|
const poolEl = ref(null)
|
|
|
|
|
const groupDropEls = ref({})
|
|
|
|
|
const fileEl = ref(null)
|
|
|
|
|
@@ -97,6 +98,8 @@ const thumbnailFileEl = ref(null)
|
|
|
|
|
const groupSortable = ref(null)
|
|
|
|
|
const poolSortable = ref(null)
|
|
|
|
|
const dropSortables = ref([])
|
|
|
|
|
const editorSidebarMaxHeight = ref('')
|
|
|
|
|
let editorSidebarMeasureFrame = 0
|
|
|
|
|
|
|
|
|
|
const isNewTierList = computed(() => tierListId.value === 'new')
|
|
|
|
|
const isOwnTierList = computed(() => !!auth.user && !!ownerId.value && ownerId.value === auth.user.id)
|
|
|
|
|
@@ -359,6 +362,30 @@ function closeItemContextMenu() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scrollWorkspaceBodyToTop() {
|
|
|
|
|
const workspaceBody = document.querySelector('.workspaceBody')
|
|
|
|
|
workspaceBody?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateEditorSidebarMaxHeight() {
|
|
|
|
|
if (typeof window === 'undefined' || !sidebarEl.value) return
|
|
|
|
|
const bottomGap = 14
|
|
|
|
|
const stickyTop = 14
|
|
|
|
|
const minHeight = 260
|
|
|
|
|
const sidebarTop = Math.max(sidebarEl.value.getBoundingClientRect().top, stickyTop)
|
|
|
|
|
const nextHeight = Math.max(minHeight, Math.floor(window.innerHeight - sidebarTop - bottomGap))
|
|
|
|
|
editorSidebarMaxHeight.value = `${nextHeight}px`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleEditorSidebarMeasure() {
|
|
|
|
|
if (typeof window === 'undefined') return
|
|
|
|
|
if (editorSidebarMeasureFrame) return
|
|
|
|
|
editorSidebarMeasureFrame = window.requestAnimationFrame(() => {
|
|
|
|
|
editorSidebarMeasureFrame = 0
|
|
|
|
|
updateEditorSidebarMaxHeight()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openItemContextMenu(itemId, event) {
|
|
|
|
|
if (!canEdit.value || !itemId || !itemsById.value[itemId] || shouldIgnoreItemClick()) return
|
|
|
|
|
selectedItemId.value = itemId
|
|
|
|
|
@@ -1350,6 +1377,7 @@ async function loadEditorState() {
|
|
|
|
|
if (loadToken !== editorLoadToken) return
|
|
|
|
|
|
|
|
|
|
syncSavedEditorSnapshot()
|
|
|
|
|
scheduleEditorSidebarMeasure()
|
|
|
|
|
if (canEdit.value) {
|
|
|
|
|
await initSortables()
|
|
|
|
|
}
|
|
|
|
|
@@ -1369,6 +1397,9 @@ onMounted(() => {
|
|
|
|
|
window.addEventListener('contextmenu', handleGlobalContextMenu, true)
|
|
|
|
|
window.addEventListener('blur', closeItemContextMenu)
|
|
|
|
|
window.addEventListener('scroll', closeItemContextMenu, true)
|
|
|
|
|
window.addEventListener('resize', scheduleEditorSidebarMeasure)
|
|
|
|
|
window.addEventListener('scroll', scheduleEditorSidebarMeasure, true)
|
|
|
|
|
nextTick(() => scheduleEditorSidebarMeasure())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
@@ -1377,6 +1408,12 @@ onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('contextmenu', handleGlobalContextMenu, true)
|
|
|
|
|
window.removeEventListener('blur', closeItemContextMenu)
|
|
|
|
|
window.removeEventListener('scroll', closeItemContextMenu, true)
|
|
|
|
|
window.removeEventListener('resize', scheduleEditorSidebarMeasure)
|
|
|
|
|
window.removeEventListener('scroll', scheduleEditorSidebarMeasure, true)
|
|
|
|
|
if (editorSidebarMeasureFrame) {
|
|
|
|
|
window.cancelAnimationFrame(editorSidebarMeasureFrame)
|
|
|
|
|
editorSidebarMeasureFrame = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
|
|
|
|
destroySortables()
|
|
|
|
|
@@ -1582,7 +1619,15 @@ onUnmounted(() => {
|
|
|
|
|
<div class="editorMain">
|
|
|
|
|
<section class="head">
|
|
|
|
|
<div class="editorMain__headCopy">
|
|
|
|
|
<div class="editorMain__title">{{ templateName || templateId }}</div>
|
|
|
|
|
<button
|
|
|
|
|
class="editorMain__title editorMain__titleButton"
|
|
|
|
|
type="button"
|
|
|
|
|
title="본문을 화면 위로 이동"
|
|
|
|
|
@click="scrollWorkspaceBodyToTop"
|
|
|
|
|
@keydown.space.prevent="scrollWorkspaceBodyToTop"
|
|
|
|
|
>
|
|
|
|
|
{{ templateName || templateId }}
|
|
|
|
|
</button>
|
|
|
|
|
<div class="editorMain__subtitle">
|
|
|
|
|
<template v-if="canEdit">
|
|
|
|
|
행/열 이름과 순서를 바꾸고 아이템을 드래그해서 배치할 수 있어요.
|
|
|
|
|
@@ -1746,9 +1791,17 @@ onUnmounted(() => {
|
|
|
|
|
<button class="btn btn--ghost btn--small dropzone__button" @click="openFile">파일 선택</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="editorTips">
|
|
|
|
|
<div class="editorTips__title">작업 팁</div>
|
|
|
|
|
<ul class="editorTips__list">
|
|
|
|
|
<li>마우스 오른쪽 클릭으로 아이템을 복수 사용하거나 커스텀 이미지를 빠르게 정리할 수 있어요.</li>
|
|
|
|
|
<li>미사용 아이템은 미리보기와 이미지 저장 결과에 표시되지 않으니, 필요한 것만 골라 배치해도 괜찮아요.</li>
|
|
|
|
|
<li>아이템이 많아 한 번에 보기 어렵다면 브라우저 확대/축소(`Ctrl +`, `Ctrl -`)로 화면 밀도를 조절해보세요.</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sidebar">
|
|
|
|
|
<div ref="sidebarEl" class="sidebar" :style="{ '--editor-sidebar-max-height': editorSidebarMaxHeight || undefined }">
|
|
|
|
|
<div class="sidebar__titleRow">
|
|
|
|
|
<div class="sidebar__title">아이템</div>
|
|
|
|
|
<div class="sidebar__count">{{ visiblePoolCount }} / {{ pool.length }}</div>
|
|
|
|
|
@@ -1966,6 +2019,24 @@ onUnmounted(() => {
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
letter-spacing: -0.04em;
|
|
|
|
|
}
|
|
|
|
|
.editorMain__titleButton {
|
|
|
|
|
width: fit-content;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
border: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: inherit;
|
|
|
|
|
text-align: left;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.editorMain__titleButton:hover {
|
|
|
|
|
color: var(--theme-text-strong);
|
|
|
|
|
}
|
|
|
|
|
.editorMain__titleButton:focus-visible {
|
|
|
|
|
outline: 2px solid color-mix(in srgb, var(--theme-accent) 70%, white);
|
|
|
|
|
outline-offset: 4px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
.editorMain__subtitle {
|
|
|
|
|
color: var(--theme-text-soft);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
@@ -2736,6 +2807,8 @@ onUnmounted(() => {
|
|
|
|
|
}
|
|
|
|
|
.sidebar {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
border: 1px solid var(--theme-border);
|
|
|
|
|
background: linear-gradient(180deg, color-mix(in srgb, var(--theme-card-bg) 94%, transparent), color-mix(in srgb, var(--theme-card-bg-hover) 88%, transparent));
|
|
|
|
|
border-radius: 22px;
|
|
|
|
|
@@ -2743,6 +2816,9 @@ onUnmounted(() => {
|
|
|
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 14px;
|
|
|
|
|
max-height: var(--editor-sidebar-max-height, calc(100dvh - 136px));
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dropzone--board {
|
|
|
|
|
@@ -3057,6 +3133,9 @@ onUnmounted(() => {
|
|
|
|
|
color: var(--theme-text-faint);
|
|
|
|
|
}
|
|
|
|
|
.pool {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
|
|
|
gap: 10px;
|
|
|
|
|
@@ -3065,6 +3144,30 @@ onUnmounted(() => {
|
|
|
|
|
.pool--clickTarget {
|
|
|
|
|
cursor: copy;
|
|
|
|
|
}
|
|
|
|
|
.editorTips {
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
background: color-mix(in srgb, var(--theme-card-bg) 82%, transparent);
|
|
|
|
|
}
|
|
|
|
|
.editorTips__title {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--theme-text-soft);
|
|
|
|
|
}
|
|
|
|
|
.editorTips__list {
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
color: var(--theme-text-faint);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
}
|
|
|
|
|
.editorTips__list li + li {
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
}
|
|
|
|
|
.poolItem {
|
|
|
|
|
position: relative;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
@@ -3227,8 +3330,11 @@ onUnmounted(() => {
|
|
|
|
|
}
|
|
|
|
|
.sidebar {
|
|
|
|
|
position: static;
|
|
|
|
|
max-height: none;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
}
|
|
|
|
|
.pool {
|
|
|
|
|
overflow: visible;
|
|
|
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|