Compare commits

...

1 Commits

Author SHA1 Message Date
4fbd4a2845 초기 로딩 스켈레톤 정리 2026-04-07 16:29:54 +09:00
7 changed files with 356 additions and 5 deletions

View File

@@ -1,5 +1,11 @@
# 의사결정 이력
## 2026-04-07 v1.1.25
- 이번 로딩 문제는 “매우 짧은 대기시간 + 미완성 실제 화면 노출”이 핵심이므로, 풀스크린 로딩 화면을 계속 번쩍 띄우는 것보다 `초기 인증 게이트 + 페이지 스켈레톤` 조합이 더 맞다고 정리했다.
- 로그인 상태가 아직 hydrate 되지 않았을 때는 비로그인 UI를 잠깐 먼저 보여주지 않고, 최소 시간 있는 부트 게이트로 가려 두는 편이 완성도가 높다고 판단했다.
- 티어표 화면도 실제 데이터가 오기 전 내부 ID나 빈 보드가 먼저 보이면 사이트가 덜 다듬어진 인상을 주므로, “미완성 진짜 화면”보다 “완성형 뼈대 화면”을 먼저 보여주는 쪽으로 정리했다.
- 에디터 내부에서 인증 복원을 매번 다시 치는 구조는 최초 진입 외에는 체감 지연만 늘릴 수 있으므로, 앱 셸에서 이미 hydrate 된 경우에는 중복 `refresh()`를 피하는 편이 맞다고 정리했다.
## 2026-04-07 v1.1.18
- 설정 화면은 자주 바꾸지 않는 계정 정보를 상시 입력 폼으로 펼쳐두기보다, 현재 상태를 먼저 보여주고 필요할 때만 모달로 수정하는 편이 더 차분하고 완성도 높게 보인다고 정리했다.
- 닉네임은 공개 작성자 이름에 직접 반영되는 정보라 악용 가능성을 줄이기 위해 2주 제한을 두는 편이 맞다고 판단했다. 초기 가입 시점의 닉네임도 같은 규칙에 포함되도록 가입 시각을 기본 기준선으로 삼는다.

View File

@@ -17,7 +17,7 @@
## `/editor/:topicId/new`, `/editor/:topicId/:tierListId`
- 화면 파일: `frontend/src/views/TierEditorView.vue`
- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출
- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, 데이터가 준비되기 전에는 편집 모드/프리뷰 모드 모두 전용 스켈레톤 레이아웃을 먼저 보여주고, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출
- 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/:id`, `GET /api/tierlists/:id/comments`, `POST /api/tierlists/:id/comments`, `DELETE /api/tierlists/:id/comments/:commentId`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request`
## `/comments`
@@ -67,7 +67,7 @@
## 공통 레이아웃
- 앱 셸 파일: `frontend/src/App.vue`
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, `홈 / 템플릿 / 댓글 관리` 네비게이션과 화면별 검색 placeholder 전환, `preview=1` 공유 프리뷰에서도 같은 좌우 레일과 중앙 헤더 유지, 로그인 상태 반영, 최근 즐겨찾기 바로가기와 전역 검색 입력, 댓글 알림 unread dot, 관리자 메뉴 노출 제어, 실제 SVG 에셋과 선형 SVG 아이콘이 혼합된 레일 UI, 전역 우측 상단 토스트 렌더링, 관리자/에디터 화면이 Teleport로 사용하는 `#local-right-rail-root` 대상 DOM을 상시 유지해 라우트 전환 중 우측 레일 언마운트 순서를 안정화, `S/ㄴ`, `G/ㅎ`, `L/ㅣ`, `A/ㅁ` 같은 전역 단축키 처리, 설정 가이드 모달 단계 이동/높이 안정화
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, `홈 / 템플릿 / 댓글 관리` 네비게이션과 화면별 검색 placeholder 전환, `preview=1` 공유 프리뷰에서도 같은 좌우 레일과 중앙 헤더 유지, 로그인 상태 반영, 최근 즐겨찾기 바로가기와 전역 검색 입력, 댓글 알림 unread dot, 관리자 메뉴 노출 제어, 실제 SVG 에셋과 선형 SVG 아이콘이 혼합된 레일 UI, 전역 우측 상단 토스트 렌더링, 앱 최초 진입 시 `auth.refresh()`와 최소 140ms를 묶은 `bootGate` 초기 스켈레톤 표시, 관리자/에디터 화면이 Teleport로 사용하는 `#local-right-rail-root` 대상 DOM을 상시 유지해 라우트 전환 중 우측 레일 언마운트 순서를 안정화, `S/ㄴ`, `G/ㅎ`, `L/ㅣ`, `A/ㅁ` 같은 전역 단축키 처리, 설정 가이드 모달 단계 이동/높이 안정화
- 세부: 좌측 패널은 `248px` 기준 폭을 사용하되 축소 시 아이콘 중심의 좁은 레일로 전환하고, 우측 패널은 `320px` 기준 폭을 사용한다. 모바일(`860px` 이하)에서는 좌우 패널 모두 오버레이로 뜨며, 중앙 헤더 오른쪽 버튼으로 각각 열고 닫는다. 중앙 헤더의 브랜드 `Tier Maker`는 홈(`/`)으로 이동하는 터치 타겟으로 유지한다. 좌우 레일의 주요 액션은 각각 하단 `56px` 푸터 안에서 항상 보이도록 유지하면서 아래쪽 패딩으로 여백을 확보한다.
## 백엔드 진입점

View File

@@ -13,6 +13,7 @@
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
- 좌측 패널은 필요 시 축소형 레일로 접을 수 있으며, 접힌 상태에서는 아이콘 중심 내비게이션과 축약된 바로가기만 유지한다.
- 모바일(`860px` 이하)에서는 좌측 패널도 고정 열을 차지하지 않고, 우측 패널과 같은 오버레이 방식으로 띄운다.
- 앱 최초 진입 시에는 인증 상태 복원과 최소 140ms 대기 시간을 함께 묶은 `bootGate` 초기 게이트를 먼저 보여주고, 그 뒤에만 실제 앱 셸을 렌더링한다.
- 이 3단 셸 구조는 홈, 게임 허브, 에디터, 관리자 등 일반 페이지 전반의 공통 뼈대로 유지하고, 페이지별 차이는 중앙/우측에 어떤 콘텐츠를 넣는지만 달라지도록 관리한다.
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
- 공통 셸의 좌측 내비, 우측 패널, 빠른 점프 버튼은 간단한 선형 SVG 아이콘과 두꺼운 카드형 버튼 문법을 공유한다.
@@ -93,6 +94,7 @@
- 공통 우측 패널 대신 전용 로컬 편집 패널을 사용한다.
- 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측 `320px` 편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다.
- 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다.
- 편집 모드와 `preview=1` 뷰어 모드 모두 제목/보드/우측 패널 데이터가 준비되기 전까지는 실제 화면 대신 전용 스켈레톤 레이아웃을 먼저 보여준다.
- 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다. 템플릿 등록/업데이트 요청 버튼은 저장된 티어표가 있을 때만 노출하며, 제목이 비어 있는 상태에서 저장하면 랜덤 고유 제목을 먼저 부여해 저장본을 만든다.
- 상단 템플릿 제목은 해당 주제 허브로 이동하는 액션으로 사용하며, 미저장 변경이 있으면 이동 전에 확인 모달을 띄운다.
- 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다.

View File

@@ -1,6 +1,9 @@
# 할 일 및 이슈
## 단기 확인
- `v1.1.25` 이후 첫 진입 시 로그인 상태가 비로그인처럼 잠깐 보였다가 바뀌는 깜빡임이 실제로 줄었는지, 느린 네트워크/빠른 네트워크 양쪽에서 확인한다.
- `v1.1.25` 이후 티어표 화면 진입 시 제목 자리에 내부 ID가 먼저 노출되지 않고, 편집 모드/프리뷰 모드 둘 다 스켈레톤 뒤에 실제 데이터가 자연스럽게 붙는지 확인한다.
- 초기 인증 게이트는 최소 140ms를 기다리므로, 초고속 응답에서도 오히려 부트 스켈레톤이 과하게 눈에 띄지 않는지 확인한다.
- `v1.1.18` 이후 설정 화면이 데스크톱/태블릿/모바일에서 너무 넓게 퍼지지 않고, 요약 카드 간 간격과 제목/설명 밀도가 다른 대시보드 화면과 자연스럽게 맞는지 확인한다.
- 프로필 이미지 변경 후 저장, 이미지 제거 후 저장, 저장하지 않고 페이지 이탈 세 경우가 모두 의도대로 동작하는지 확인한다.
- 프로필 이미지 자동 저장으로 바뀐 뒤, 파일 선택 직후와 삭제 직후에 즉시 반영되고 연속 클릭 시 중복 저장 요청이 과도하게 쌓이지 않는지 확인한다.

View File

@@ -1,5 +1,12 @@
# 업데이트 로그
## 2026-04-07 v1.1.25
- 앱 최초 진입 시 로그인 상태가 잠깐 비로그인처럼 보였다가 다시 로그인 상태로 바뀌는 깜빡임을 줄이기 위해, 공통 앱 셸에 `bootGate` 초기 게이트를 추가했다. `App.vue``auth.refresh()`와 최소 140ms 대기 시간을 함께 기다린 뒤 셸을 렌더링해, 아주 짧은 인증 복원에서도 화면이 번쩍 바뀌지 않게 한다.
- `TierEditorView`는 제목/보드/우측 아이템 풀이 준비되기 전까지 실제 편집 화면 대신 같은 서비스 톤의 스켈레톤 레이아웃을 먼저 보여주도록 바꿨다. 이로써 티어표 진입 직후 내부 ID나 빈 보드가 먼저 드러나는 현상을 줄였다.
- 에디터 라우트는 앱 전체에서 이미 인증 복원이 끝난 뒤에는 다시 `auth.refresh()`를 중복 호출하지 않도록 보정했다. 최초 진입이 아니면 현재 인증 상태를 그대로 사용해 편집 화면 초기 로딩 지연을 줄인다.
- 스켈레톤은 `preview=1` 뷰어 모드와 편집 모드 모두에 대응하며, 제목/행/셀/우측 패널이 실제 화면 구조와 비슷한 밀도로 먼저 자리잡은 뒤 데이터가 준비되면 한 번에 교체된다.
- 확인: `npm run build`
## 2026-04-07 v1.1.18
- 설정(`/profile`) 화면을 전면 재구성했다. 기존처럼 넓은 2단 입력 폼을 상시 노출하지 않고, `settingsThemePanel` 톤을 참고한 요약 카드 레이아웃으로 바꿔 더 차분하고 통일된 계정 화면으로 정리했다.
- 프로필 영역은 `닉네임 / 이메일 / 프로필 이미지`의 현재 상태를 먼저 보여주고, 자주 바꾸지 않는 정보는 필요할 때만 모달을 열어 변경하도록 바꿨다. 비밀번호 변경도 별도 카드 전체를 차지하지 않고 작은 액션으로 열리는 모달 흐름으로 정리했다.

View File

@@ -51,6 +51,7 @@ const backendState = ref('online')
const backendMessage = ref('')
const isFullscreenActive = ref(false)
const unreadCommentCount = ref(0)
const isBootReady = ref(false)
provide('rightRailOpen', rightRailOpen)
provide('localRightRailTarget', '#local-right-rail-root')
@@ -378,7 +379,11 @@ onMounted(async () => {
if (savedTheme === 'light' || savedTheme === 'dark') applyTheme(savedTheme)
else applyTheme('dark')
}
await auth.refresh()
await Promise.all([
auth.refresh(),
new Promise((resolve) => setTimeout(resolve, 140)),
])
isBootReady.value = true
if (typeof window !== 'undefined') {
syncViewportWidth()
syncFullscreenState()
@@ -691,6 +696,24 @@ function reloadApp() {
</section>
</main>
</template>
<template v-else-if="!isBootReady">
<main class="bootGate">
<section class="bootGate__shell">
<div class="bootGate__brand">
<div class="bootGate__logo"></div>
<div class="bootGate__copy">
<div class="bootGate__title">Tier Maker</div>
<div class="bootGate__desc">계정 상태와 화면 구성을 준비하고 있어요.</div>
</div>
</div>
<div class="bootGate__panels">
<div class="bootGate__panel bootGate__panel--nav"></div>
<div class="bootGate__panel bootGate__panel--main"></div>
<div class="bootGate__panel bootGate__panel--side"></div>
</div>
</section>
</main>
</template>
<template v-else>
<button v-if="isMobileLayout && mobileLeftNavOpen" class="leftRailBackdrop" type="button" aria-label="왼쪽 패널 닫기" @click="toggleLeftRail"></button>
@@ -1004,6 +1027,95 @@ function reloadApp() {
cursor: pointer;
}
.bootGate {
min-width: 100dvw;
min-height: 100dvh;
display: grid;
place-items: center;
padding: 24px;
background: var(--theme-shell-bg);
}
.bootGate__shell {
width: min(100%, 1180px);
display: grid;
gap: 20px;
}
.bootGate__brand {
display: flex;
align-items: center;
gap: 16px;
padding: 18px 20px;
border-radius: 24px;
border: 1px solid var(--theme-border);
background: var(--theme-rail-bg);
}
.bootGate__logo,
.bootGate__panel {
position: relative;
overflow: hidden;
}
.bootGate__logo::after,
.bootGate__panel::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
animation: bootGateShimmer 1.4s ease-in-out infinite;
}
.bootGate__logo {
width: 52px;
height: 52px;
border-radius: 18px;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.bootGate__copy {
display: grid;
gap: 6px;
}
.bootGate__title {
font-size: 20px;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--theme-text-strong);
}
.bootGate__desc {
color: var(--theme-text-muted);
line-height: 1.6;
}
.bootGate__panels {
display: grid;
grid-template-columns: 248px minmax(0, 1fr) 320px;
gap: 20px;
}
.bootGate__panel {
border-radius: 24px;
border: 1px solid var(--theme-border);
background: var(--theme-rail-bg);
min-height: 72dvh;
}
.bootGate__panel--main {
background: var(--theme-shell-bg);
}
@keyframes bootGateShimmer {
100% {
transform: translateX(100%);
}
}
.leftRail,
.rightRail {
min-height: 100dvh;
@@ -2179,6 +2291,32 @@ function reloadApp() {
}
@media (max-width: 860px) {
.bootGate {
padding: 16px;
}
.bootGate__shell {
gap: 16px;
}
.bootGate__brand {
padding: 16px;
border-radius: 20px;
}
.bootGate__panels {
grid-template-columns: 1fr;
}
.bootGate__panel--nav,
.bootGate__panel--side {
display: none;
}
.bootGate__panel--main {
min-height: 72dvh;
}
.guideModal {
padding: 20px 12px;
}

View File

@@ -86,6 +86,7 @@ const itemContextMenu = ref({
y: 0,
itemId: '',
})
const isEditorLoading = ref(true)
let editorLoadToken = 0
const boardEl = ref(null)
@@ -1302,6 +1303,7 @@ function resetEditorStateForRoute() {
selectedItemId.value = ''
recentDragFinishedAt.value = 0
savedEditorSnapshot.value = ''
isEditorLoading.value = true
closeItemContextMenu()
resetTemplateRequestDrafts()
}
@@ -1309,7 +1311,9 @@ function resetEditorStateForRoute() {
async function loadEditorState() {
const loadToken = ++editorLoadToken
resetEditorStateForRoute()
await auth.refresh()
if (!auth.hydrated) {
await auth.refresh()
}
if (loadToken !== editorLoadToken) return
authorName.value = (auth.user?.nickname || '').trim()
@@ -1391,6 +1395,7 @@ async function loadEditorState() {
} catch (e) {
if (loadToken !== editorLoadToken) return
error.value = '티어표를 불러오지 못했어요.'
isEditorLoading.value = false
}
}
@@ -1402,6 +1407,8 @@ async function loadEditorState() {
if (canEdit.value) {
await initSortables()
}
if (loadToken !== editorLoadToken) return
isEditorLoading.value = false
}
watch(
@@ -1444,7 +1451,27 @@ onUnmounted(() => {
</script>
<template>
<section v-if="previewMode" class="previewOnly" :style="{ '--thumb-size': `${iconSize}px` }">
<section v-if="previewMode && isEditorLoading" class="editorSkeleton editorSkeleton--preview">
<header class="pageHead">
<div class="pageHead__main">
<div class="editorSkeleton__line editorSkeleton__line--eyebrow"></div>
<div class="editorSkeleton__line editorSkeleton__line--title"></div>
<div class="editorSkeleton__line editorSkeleton__line--desc"></div>
</div>
</header>
<div class="editorSkeleton__previewCard">
<div class="editorSkeleton__board">
<div class="editorSkeleton__row" v-for="index in 4" :key="`preview-row-${index}`">
<div class="editorSkeleton__label"></div>
<div class="editorSkeleton__cells">
<div class="editorSkeleton__cell" v-for="cellIndex in 3" :key="`preview-cell-${index}-${cellIndex}`"></div>
</div>
</div>
</div>
</div>
</section>
<section v-else-if="previewMode" class="previewOnly" :style="{ '--thumb-size': `${iconSize}px` }">
<header class="pageHead">
<div class="pageHead__main">
<div class="pageHead__eyebrow">Preview</div>
@@ -1521,6 +1548,29 @@ onUnmounted(() => {
</Teleport>
</section>
<template v-else-if="isEditorLoading">
<section class="editorSkeleton" :style="{ '--thumb-size': `${iconSize}px` }">
<div class="editorSkeleton__head">
<div class="editorSkeleton__line editorSkeleton__line--title"></div>
<div class="editorSkeleton__line editorSkeleton__line--desc"></div>
</div>
<div class="editorSkeleton__body">
<div class="editorSkeleton__board">
<div class="editorSkeleton__toolbar">
<div class="editorSkeleton__chip" v-for="index in 3" :key="`toolbar-${index}`"></div>
</div>
<div class="editorSkeleton__row" v-for="index in 4" :key="`editor-row-${index}`">
<div class="editorSkeleton__label"></div>
<div class="editorSkeleton__cells">
<div class="editorSkeleton__cell" v-for="cellIndex in 3" :key="`editor-cell-${index}-${cellIndex}`"></div>
</div>
</div>
</div>
<div class="editorSkeleton__side"></div>
</div>
</section>
</template>
<template v-else>
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
@@ -2023,6 +2073,127 @@ onUnmounted(() => {
</template>
<style scoped>
.editorSkeleton {
display: grid;
gap: 18px;
}
.editorSkeleton__head {
display: grid;
gap: 10px;
}
.editorSkeleton__body {
display: grid;
grid-template-columns: minmax(0, clamp(680px, 58vw, 960px)) minmax(280px, 1fr);
gap: 16px;
align-items: start;
}
.editorSkeleton__previewCard,
.editorSkeleton__board,
.editorSkeleton__side,
.editorSkeleton__line,
.editorSkeleton__label,
.editorSkeleton__cell,
.editorSkeleton__chip {
position: relative;
overflow: hidden;
}
.editorSkeleton__previewCard,
.editorSkeleton__board,
.editorSkeleton__side {
border-radius: 22px;
border: 1px solid var(--theme-border);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--theme-card-bg) 96%, transparent),
color-mix(in srgb, var(--theme-card-bg-hover) 92%, transparent)
);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.editorSkeleton__previewCard,
.editorSkeleton__board {
padding: 20px;
}
.editorSkeleton__side {
min-height: 540px;
}
.editorSkeleton__previewCard::after,
.editorSkeleton__board::after,
.editorSkeleton__side::after,
.editorSkeleton__line::after,
.editorSkeleton__label::after,
.editorSkeleton__cell::after,
.editorSkeleton__chip::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.08) 48%, transparent 100%);
transform: translateX(-100%);
animation: editorSkeletonShimmer 1.4s ease-in-out infinite;
}
.editorSkeleton__line {
border-radius: 999px;
background: color-mix(in srgb, var(--theme-surface-soft-2) 88%, transparent);
}
.editorSkeleton__line--eyebrow {
width: 92px;
height: 12px;
}
.editorSkeleton__line--title {
width: min(420px, 78%);
height: 34px;
}
.editorSkeleton__line--desc {
width: min(560px, 92%);
height: 14px;
}
.editorSkeleton__toolbar {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.editorSkeleton__chip {
width: 88px;
height: 40px;
border-radius: 12px;
background: color-mix(in srgb, var(--theme-surface-soft-2) 84%, transparent);
}
.editorSkeleton__row {
display: grid;
grid-template-columns: 132px 1fr;
gap: 10px;
}
.editorSkeleton__row + .editorSkeleton__row {
margin-top: 10px;
}
.editorSkeleton__label {
min-height: 110px;
border-radius: 14px;
background: color-mix(in srgb, var(--theme-surface-soft-2) 88%, transparent);
}
.editorSkeleton__cells {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.editorSkeleton__cell {
min-height: 110px;
border-radius: 14px;
background: color-mix(in srgb, var(--theme-surface-soft) 74%, transparent);
border: 1px solid color-mix(in srgb, var(--theme-border) 88%, transparent);
}
.editorSkeleton--preview .editorSkeleton__board {
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
}
@keyframes editorSkeletonShimmer {
100% {
transform: translateX(100%);
}
}
.head {
display: grid;
gap: 8px;
@@ -3309,6 +3480,21 @@ onUnmounted(() => {
border-radius: 14px;
}
@media (max-width: 980px) {
.editorSkeleton__body {
grid-template-columns: 1fr;
}
.editorSkeleton__side {
min-height: 320px;
}
.editorSkeleton__row {
grid-template-columns: 1fr;
}
.editorSkeleton__label {
min-height: 44px;
}
.editorSkeleton__cells {
grid-template-columns: 1fr;
}
.previewOnly__row,
.row {
grid-template-columns: 1fr;
@@ -3389,6 +3575,15 @@ onUnmounted(() => {
}
}
@media (max-width: 720px) {
.editorSkeleton__previewCard,
.editorSkeleton__board,
.editorSkeleton__side {
border-radius: 18px;
}
.editorSkeleton__previewCard,
.editorSkeleton__board {
padding: 16px;
}
.previewOnly {
padding: 14px;
}