Compare commits

...

3 Commits

12 changed files with 424 additions and 346 deletions

View File

@@ -1,5 +1,20 @@
# 의사결정 이력 # 의사결정 이력
## 2026-03-30 v1.2.3
- 로그인 유도는 좌측 하단의 단일 버튼이면 충분하므로, 비로그인 상태에서 사이드 상단에 별도 안내 카드를 또 보여주는 구조는 제거하는 편이 더 깔끔하다고 판단했다.
- 티어표 편집 화면은 공통 우측 패널의 generic 문맥 카드보다 실제 편집 필드가 우측에 있는 편이 훨씬 중요하므로, 이 화면은 전용 로컬 우측 패널을 두는 쪽으로 정리했다.
- 좌측 내비가 이미 라우팅 역할을 하므로, 에디터 우측 패널에서는 “게임 목록으로” 같은 중복 이동 CTA보다 저장과 편집 자체에 집중하는 것이 맞다고 판단했다.
## 2026-03-30 v1.2.2
- 우측 패널은 본문 내부 보조 박스가 아니라 별도 컬럼으로 보이는 것이 핵심이므로, 폭을 `320px`로 고정하고 접힘/펼침도 레이아웃 레벨에서 처리하는 편이 맞다고 판단했다.
- 좌측 패널도 시안 기준 인지 폭이 중요하므로 `248px`로 고정하고, 중앙 콘텐츠는 나머지 공간을 유동적으로 쓰게 하는 구조로 정리했다.
- 우측 패널 토글은 라우트별 개별 구현보다 공통 셸의 상단 컨트롤로 두는 편이 모든 화면에서 일관된 사용성을 제공한다고 판단했다.
## 2026-03-30 v1.2.1
- 공통 셸을 먼저 올린 직후에는 에디터와 관리자처럼 자체 패널이 많은 화면이 가장 크게 깨지므로, 이 화면들은 우선 공통 우측 패널을 숨기고 중앙 폭을 회복시키는 편이 안정적이라고 판단했다.
- 목록형 카드 화면은 셸 안쪽 폭이 줄어든 상태에서 이전보다 더 많은 컬럼을 유지하면 즉시 사용성이 무너지므로, 기본 컬럼 수를 줄여 먼저 읽히는 상태를 만드는 쪽을 우선하기로 했다.
- 리디자인 초기 단계에서는 “완벽한 시안 재현”보다 먼저 실제 조작 가능한 상태를 되찾는 것이 중요하므로, 이번 단계는 안정화 릴리스로 짧게 끊어 가기로 정리했다.
## 2026-03-30 v1.2.0 ## 2026-03-30 v1.2.0
- 피그마 시안은 단순 컴포넌트 교체보다 앱 전체의 정보 구조를 바꾸는 성격이 강하므로, 우선 공통 앱 셸부터 `좌측 내비 / 중앙 워크스페이스 / 우측 컨텍스트 패널`로 올리는 단계적 리디자인이 더 안전하다고 판단했다. - 피그마 시안은 단순 컴포넌트 교체보다 앱 전체의 정보 구조를 바꾸는 성격이 강하므로, 우선 공통 앱 셸부터 `좌측 내비 / 중앙 워크스페이스 / 우측 컨텍스트 패널`로 올리는 단계적 리디자인이 더 안전하다고 판단했다.
- 홈, 게임 허브, 내 티어표, 즐겨찾기처럼 카드 목록 중심 화면은 시안 톤을 먼저 맞추고, 에디터와 관리자처럼 상호작용이 무거운 화면은 같은 셸 안에서 후속 이관하는 방식이 리스크가 적다고 정리했다. - 홈, 게임 허브, 내 티어표, 즐겨찾기처럼 카드 목록 중심 화면은 시안 톤을 먼저 맞추고, 에디터와 관리자처럼 상호작용이 무거운 화면은 같은 셸 안에서 후속 이관하는 방식이 리스크가 적다고 정리했다.

View File

@@ -12,7 +12,7 @@
## `/editor/:gameId/new`, `/editor/:gameId/:tierListId` ## `/editor/:gameId/new`, `/editor/:gameId/:tierListId`
- 화면 파일: `frontend/src/views/TierEditorView.vue` - 화면 파일: `frontend/src/views/TierEditorView.vue`
- 역할: 티어 그룹 편집, 티어 행 추가/삭제, 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 티어표 썸네일 선택, 작성 권한 제어, 저장, 공개 여부 설정, 즐겨찾기 토글, PNG 다운로드 - 역할: 티어 그룹 편집, 티어 행 추가/삭제, 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 우측 전용 편집 패널에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어, 즐겨찾기 토글, PNG 다운로드
- 연동 API: `GET /api/games/:gameId`, `GET /api/tierlists/:id`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists` - 연동 API: `GET /api/games/:gameId`, `GET /api/tierlists/:id`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`
## `/login` ## `/login`
@@ -43,6 +43,7 @@
## 공통 레이아웃 ## 공통 레이아웃
- 앱 셸 파일: `frontend/src/App.vue` - 앱 셸 파일: `frontend/src/App.vue`
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, 로그인 상태 반영, 사용자 메뉴, 관리자 메뉴 노출 제어, 전역 우측 상단 토스트 렌더링 - 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, 로그인 상태 반영, 사용자 메뉴, 관리자 메뉴 노출 제어, 전역 우측 상단 토스트 렌더링
- 세부: 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 상단 토글 버튼으로 우측 패널을 접고 펼칠 수 있다.
## 백엔드 진입점 ## 백엔드 진입점
- 서버 엔트리: `backend/index.js` - 서버 엔트리: `backend/index.js`

View File

@@ -10,6 +10,8 @@
- NAS HTTPS 리버스 프록시 운영 시 프런트 Nginx는 백엔드로 `X-Forwarded-Proto: https`를 전달하고, Express 세션은 프록시 환경에서 `secure` 쿠키를 허용하도록 설정한다. - NAS HTTPS 리버스 프록시 운영 시 프런트 Nginx는 백엔드로 `X-Forwarded-Proto: https`를 전달하고, Express 세션은 프록시 환경에서 `secure` 쿠키를 허용하도록 설정한다.
- 프런트 파비콘은 운영 정적 파일 차단 영향을 줄이기 위해 `index.html`의 인라인 데이터 URL로 제공한다. - 프런트 파비콘은 운영 정적 파일 차단 영향을 줄이기 위해 `index.html`의 인라인 데이터 URL로 제공한다.
- 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, preview 모드에서는 이 셸을 숨기고 콘텐츠만 렌더링한다. - 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, preview 모드에서는 이 셸을 숨기고 콘텐츠만 렌더링한다.
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
## 데이터 저장 구조 ## 데이터 저장 구조
- 메인 데이터베이스: MariaDB `tier_cursor` (기본값) - 메인 데이터베이스: MariaDB `tier_cursor` (기본값)
@@ -28,6 +30,10 @@
- 우측 패널 - 우측 패널
- 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다. - 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다.
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다. - 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
- 공통 토글 버튼으로 패널을 접으면 중앙 워크스페이스가 남는 공간을 확장 사용한다.
- 티어표 편집 화면
- 공통 우측 패널 대신 전용 로컬 편집 패널을 사용한다.
- 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다.
## DB 스키마 ## DB 스키마
- `users` - `users`

View File

@@ -1,7 +1,9 @@
# 할 일 및 이슈 # 할 일 및 이슈
## 즉시 확인 필요 ## 즉시 확인 필요
- 피그마 기반 리디자인은 현재 공통 셸과 카드 목록 화면까지만 1차 적용된 상태이므로, 에디터/관리자 우측 옵션 패널을 시안 구조에 맞게 실제 기능 패널로 이관하는 작업이 남아 있다. - 피그마 기반 리디자인은 현재 공통 셸과 카드 목록 화면, 포커스 화면 안정화까지만 반영된 상태이므로, 에디터/관리자 우측 옵션 패널을 시안 구조에 맞게 실제 기능 패널로 이관하는 작업이 남아 있다.
- 공통 우측 패널의 토글과 독립 컬럼 구조는 반영되었지만, 현재는 안내 카드 중심이므로 실제 화면별 기능 컨트롤을 이 패널로 옮기는 작업이 남아 있다.
- 티어표 편집 화면은 기본 편집 필드를 우측 로컬 패널로 옮겼지만, 관리자 화면도 같은 방식으로 실제 운영 패널 중심 레이아웃으로 다시 정리할 필요가 있다.
- 머티리얼 아이콘 SVG는 아직 임시 문자/배지 스타일로 대체된 부분이 있으므로, 최종 아이콘 에셋을 받아 반영하는 작업이 필요하다. - 머티리얼 아이콘 SVG는 아직 임시 문자/배지 스타일로 대체된 부분이 있으므로, 최종 아이콘 에셋을 받아 반영하는 작업이 필요하다.
- 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다. - 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다.
- 업로드 이미지는 현재 원본 파일을 그대로 저장하므로, 운영 부담이 커지면 서버 저장 전 리사이즈/압축(예: 긴 변 제한, WebP 변환) 도입이 필요하다. - 업로드 이미지는 현재 원본 파일을 그대로 저장하므로, 운영 부담이 커지면 서버 저장 전 리사이즈/압축(예: 긴 변 제한, WebP 변환) 도입이 필요하다.

View File

@@ -1,5 +1,20 @@
# 업데이트 로그 # 업데이트 로그
## 2026-03-30 v1.2.3
- **비로그인 중복 안내 제거**: 좌측 사이드 상단의 별도 로그인 안내 카드를 제거하고, 비로그인 상태에서는 좌측 하단 버튼만 `로그인` 진입점으로 사용하도록 단순화
- **에디터 우측 편집 패널 이관**: 티어표 편집 화면의 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 중앙 상단이 아니라 독립 우측 편집 패널로 이동
- **공통 우측 패널 예외 처리**: 티어표 편집 화면은 공통 우측 패널 대신 화면 내부 전용 편집 패널을 사용하도록 조정해, generic 안내 카드가 중복 표시되지 않게 정리
## 2026-03-30 v1.2.2
- **사이드 패널 폭 고정**: 공통 앱 셸의 좌측 패널 폭을 `248px`, 우측 패널 폭을 `320px` 기준으로 재정의해 피그마 시안과 더 가깝게 맞춤
- **우측 패널 토글 추가**: 상단 우측 토글 버튼으로 우측 패널을 접고 펼칠 수 있게 하고, 접힐 때는 중앙 작업 영역이 자연스럽게 확장되도록 전환 애니메이션을 추가
- **우측 패널 독립성 강화**: 우측 패널은 본문과 별도 컬럼으로 유지하고, 닫힐 때도 본문 레이아웃과 분리된 독립 패널처럼 동작하도록 셸 구조를 조정
## 2026-03-30 v1.2.1
- **포커스 화면 폭 복구**: 에디터·관리자·프로필·로그인 화면은 공통 우측 패널을 잠시 숨기고 중앙 작업 폭을 넓혀, 기존 기능 UI가 3단 셸과 충돌하며 깨지던 문제를 완화
- **목록 카드 밀도 재조정**: 홈, 게임 허브, 내 티어표, 즐겨찾기 화면의 기본 컬럼 수를 줄여 현재 셸 폭 안에서도 카드가 과도하게 눌리지 않도록 정리
- **에디터/관리자 패널 안정화**: 내부 작업 패널 색상과 폭을 새 셸 톤에 맞춰 다시 정리해, 중첩 패널 때문에 사용성이 무너지던 부분을 우선 복구
## 2026-03-30 v1.2.0 ## 2026-03-30 v1.2.0
- **피그마 기반 공통 앱 셸 1차 적용**: 상단 헤더 중심 구조를 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 앱 셸로 재구성하고, 데스크톱 기준의 어두운 대시보드형 톤으로 전환 - **피그마 기반 공통 앱 셸 1차 적용**: 상단 헤더 중심 구조를 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 앱 셸로 재구성하고, 데스크톱 기준의 어두운 대시보드형 톤으로 전환
- **홈/목록 화면 카드 UI 리디자인**: 홈, 게임 허브, 내 티어표, 즐겨찾기 화면의 카드 그리드와 툴바를 시안에 맞춰 더 조밀한 대시보드 형태로 재배치 - **홈/목록 화면 카드 UI 리디자인**: 홈, 게임 허브, 내 티어표, 즐겨찾기 화면의 카드 그리드와 툴바를 시안에 맞춰 더 조밀한 대시보드 형태로 재배치

View File

@@ -11,9 +11,11 @@ const auth = useAuthStore()
const { toasts, dismissToast } = useToast() const { toasts, dismissToast } = useToast()
const menuOpen = ref(false) const menuOpen = ref(false)
const rightRailOpen = ref(true)
const isAdmin = computed(() => !!auth.user?.isAdmin) const isAdmin = computed(() => !!auth.user?.isAdmin)
const isPreviewMode = computed(() => route.query.preview === '1') const isPreviewMode = computed(() => route.query.preview === '1')
const usesLocalRightRail = computed(() => ['editEditor', 'newEditor'].includes(String(route.name || '')))
const avatarUrl = computed(() => (auth.user?.avatarSrc ? toApiUrl(auth.user.avatarSrc) : '')) const avatarUrl = computed(() => (auth.user?.avatarSrc ? toApiUrl(auth.user.avatarSrc) : ''))
const accountName = computed(() => { const accountName = computed(() => {
const nickname = (auth.user?.nickname || '').trim() const nickname = (auth.user?.nickname || '').trim()
@@ -128,6 +130,10 @@ const favoriteLinks = computed(() => [
onMounted(async () => { onMounted(async () => {
await auth.refresh() await auth.refresh()
if (typeof window !== 'undefined') {
const saved = window.localStorage.getItem('tier-maker:right-rail-open')
if (saved === '0') rightRailOpen.value = false
}
document.addEventListener('click', onDocumentClick) document.addEventListener('click', onDocumentClick)
}) })
@@ -157,6 +163,13 @@ function toggleMenu() {
menuOpen.value = !menuOpen.value menuOpen.value = !menuOpen.value
} }
function toggleRightRail() {
rightRailOpen.value = !rightRailOpen.value
if (typeof window !== 'undefined') {
window.localStorage.setItem('tier-maker:right-rail-open', rightRailOpen.value ? '1' : '0')
}
}
function goProfile() { function goProfile() {
menuOpen.value = false menuOpen.value = false
router.push('/profile') router.push('/profile')
@@ -170,7 +183,7 @@ async function logout() {
</script> </script>
<template> <template>
<div class="appShell" :class="{ 'appShell--preview': isPreviewMode }"> <div class="appShell" :class="{ 'appShell--preview': isPreviewMode, 'appShell--rightClosed': !rightRailOpen }">
<template v-if="isPreviewMode"> <template v-if="isPreviewMode">
<main class="appMain appMain--preview"> <main class="appMain appMain--preview">
<RouterView /> <RouterView />
@@ -186,7 +199,7 @@ async function logout() {
</div> </div>
</div> </div>
<div class="appUserCard"> <div v-if="auth.user" class="appUserCard">
<button v-if="auth.user" class="appUserCard__button" type="button" @click.stop="toggleMenu"> <button v-if="auth.user" class="appUserCard__button" type="button" @click.stop="toggleMenu">
<img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" /> <img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" />
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div> <div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
@@ -195,13 +208,6 @@ async function logout() {
<div class="appUserCard__email">{{ accountEmail }}</div> <div class="appUserCard__email">{{ accountEmail }}</div>
</div> </div>
</button> </button>
<div v-else class="appUserCard__guest" @click="$router.push('/login')">
<div class="appUserCard__avatar appUserCard__avatar--fallback">G</div>
<div class="appUserCard__meta">
<div class="appUserCard__name">로그인 필요</div>
<div class="appUserCard__email">개인 메뉴를 사용하려면 로그인하세요.</div>
</div>
</div>
<div v-if="menuOpen" class="appUserMenu"> <div v-if="menuOpen" class="appUserMenu">
<button class="appUserMenu__item" type="button" @click="goProfile">프로필</button> <button class="appUserMenu__item" type="button" @click="goProfile">프로필</button>
<button class="appUserMenu__item" type="button" @click="logout">로그아웃</button> <button class="appUserMenu__item" type="button" @click="logout">로그아웃</button>
@@ -247,6 +253,11 @@ async function logout() {
<div class="workspaceHead__title">{{ routeMeta.title }}</div> <div class="workspaceHead__title">{{ routeMeta.title }}</div>
<div class="workspaceHead__subtitle">{{ routeMeta.subtitle }}</div> <div class="workspaceHead__subtitle">{{ routeMeta.subtitle }}</div>
</div> </div>
<div class="workspaceHead__actions">
<button class="ghostIcon ghostIcon--workspace" type="button" :aria-pressed="rightRailOpen" @click="toggleRightRail">
{{ rightRailOpen ? '우측 패널 숨기기' : '우측 패널 보기' }}
</button>
</div>
</header> </header>
<div class="workspaceBody"> <div class="workspaceBody">
<RouterView /> <RouterView />
@@ -254,7 +265,12 @@ async function logout() {
</section> </section>
</main> </main>
<aside class="rightRail"> <aside
v-if="!usesLocalRightRail"
class="rightRail"
:class="{ 'rightRail--closed': !rightRailOpen }"
:aria-hidden="!rightRailOpen"
>
<div class="rightRail__top"> <div class="rightRail__top">
<button class="ghostIcon" type="button" aria-label="상태"></button> <button class="ghostIcon" type="button" aria-label="상태"></button>
</div> </div>
@@ -296,11 +312,12 @@ async function logout() {
.appShell { .appShell {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
grid-template-columns: 260px minmax(0, 1fr) 280px; grid-template-columns: 248px minmax(0, 1fr) 320px;
background: background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 28%), radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 28%),
linear-gradient(180deg, #1a1a1a 0%, #121212 100%); linear-gradient(180deg, #1a1a1a 0%, #121212 100%);
color: rgba(255, 255, 255, 0.92); color: rgba(255, 255, 255, 0.92);
transition: grid-template-columns 220ms ease;
} }
.appShell--preview { .appShell--preview {
@@ -310,15 +327,35 @@ async function logout() {
.leftRail, .leftRail,
.rightRail { .rightRail {
min-height: 100vh; min-height: 100vh;
padding: 14px 10px; padding: 14px 12px;
border-right: 1px solid rgba(255, 255, 255, 0.08); border-right: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(14, 14, 14, 0.92); background: rgba(14, 14, 14, 0.92);
box-sizing: border-box; box-sizing: border-box;
min-width: 0;
} }
.rightRail { .rightRail {
border-right: 0; border-right: 0;
border-left: 1px solid rgba(255, 255, 255, 0.08); border-left: 1px solid rgba(255, 255, 255, 0.08);
transition:
opacity 220ms ease,
transform 220ms ease,
padding 220ms ease,
border-color 220ms ease;
}
.appShell--rightClosed {
grid-template-columns: 248px minmax(0, 1fr) 0px;
}
.appShell--rightClosed .rightRail {
opacity: 0;
transform: translateX(18px);
pointer-events: none;
overflow: hidden;
padding-left: 0;
padding-right: 0;
border-left-color: transparent;
} }
.leftRail__top, .leftRail__top,
@@ -330,8 +367,9 @@ async function logout() {
} }
.ghostIcon { .ghostIcon {
width: 28px; min-width: 28px;
height: 28px; height: 28px;
padding: 0 10px;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
@@ -339,6 +377,17 @@ async function logout() {
cursor: pointer; cursor: pointer;
} }
.ghostIcon--workspace {
min-width: 118px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.88);
font-size: 12px;
font-weight: 800;
}
.brandBlock { .brandBlock {
display: grid; display: grid;
gap: 2px; gap: 2px;
@@ -346,7 +395,7 @@ async function logout() {
} }
.brandBlock__title { .brandBlock__title {
font-size: 22px; font-size: 21px;
font-weight: 900; font-weight: 900;
letter-spacing: -0.04em; letter-spacing: -0.04em;
} }
@@ -359,6 +408,7 @@ async function logout() {
.appUserCard { .appUserCard {
position: relative; position: relative;
margin-bottom: 14px; margin-bottom: 14px;
min-height: 58px;
} }
.appUserCard__button, .appUserCard__button,
@@ -461,8 +511,8 @@ async function logout() {
.leftNav__item { .leftNav__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
padding: 10px 12px; padding: 10px;
border-radius: 12px; border-radius: 12px;
color: rgba(255, 255, 255, 0.76); color: rgba(255, 255, 255, 0.76);
text-decoration: none; text-decoration: none;
@@ -475,8 +525,8 @@ async function logout() {
} }
.leftNav__glyph { .leftNav__glyph {
width: 26px; width: 24px;
height: 26px; height: 24px;
border-radius: 8px; border-radius: 8px;
display: grid; display: grid;
place-items: center; place-items: center;
@@ -542,7 +592,7 @@ async function logout() {
.appMain { .appMain {
min-width: 0; min-width: 0;
padding: 18px 18px 28px; padding: 14px 18px 22px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -562,8 +612,15 @@ async function logout() {
gap: 16px; gap: 16px;
} }
.workspaceHead__actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.workspaceHead__title { .workspaceHead__title {
font-size: 34px; font-size: 28px;
font-weight: 900; font-weight: 900;
letter-spacing: -0.04em; letter-spacing: -0.04em;
} }
@@ -571,7 +628,7 @@ async function logout() {
.workspaceHead__subtitle { .workspaceHead__subtitle {
margin-top: 6px; margin-top: 6px;
color: rgba(255, 255, 255, 0.58); color: rgba(255, 255, 255, 0.58);
font-size: 14px; font-size: 13px;
} }
.workspaceBody { .workspaceBody {

View File

@@ -1354,18 +1354,14 @@ async function saveFeaturedOrder() {
<style scoped> <style scoped>
.wrap { .wrap {
padding: 10px 2px; display: grid;
} gap: 16px;
.title {
margin: 0 0 10px;
font-size: 26px;
letter-spacing: -0.02em;
} }
.card { .card {
border: 1px solid rgba(255, 255, 255, 0.12); border: 0;
background: rgba(255, 255, 255, 0.04); background: transparent;
border-radius: 16px; border-radius: 0;
padding: 14px; padding: 0;
} }
.desc { .desc {
opacity: 0.82; opacity: 0.82;
@@ -1392,7 +1388,7 @@ async function saveFeaturedOrder() {
} }
.tabs, .tabs,
.modeTabs { .modeTabs {
margin-top: 14px; margin-top: 0;
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1400,26 +1396,27 @@ async function saveFeaturedOrder() {
.tab, .tab,
.modeTab { .modeTab {
padding: 10px 14px; padding: 10px 14px;
border-radius: 12px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92); color: rgba(255, 255, 255, 0.92);
cursor: pointer; cursor: pointer;
font-weight: 800; font-weight: 800;
} }
.tab--active, .tab--active,
.modeTab--active { .modeTab--active {
background: rgba(96, 165, 250, 0.2); background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
} }
.panel { .panel {
margin-top: 14px; margin-top: 14px;
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.12); background: rgba(48, 48, 48, 0.78);
border-radius: 16px; border-radius: 18px;
padding: 14px; padding: 16px;
} }
.panel--compact { .panel--compact {
max-width: 480px; max-width: 520px;
} }
.featuredOrderPanel { .featuredOrderPanel {
margin-top: 14px; margin-top: 14px;

View File

@@ -154,7 +154,7 @@ onMounted(loadFavorites)
} }
.list { .list {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px; gap: 18px;
} }
.row { .row {
@@ -244,12 +244,12 @@ onMounted(loadFavorites)
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.list { .list {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 960px) { @media (max-width: 960px) {
.list { .list {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: 1fr;
} }
} }
@media (max-width: 720px) { @media (max-width: 720px) {

View File

@@ -214,7 +214,7 @@ function submitSearch() {
} }
.list { .list {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px; gap: 18px;
} }
.row { .row {
@@ -312,14 +312,14 @@ function submitSearch() {
padding: 7px 10px; padding: 7px 10px;
font-weight: 800; font-weight: 800;
} }
@media (max-width: 1100px) { @media (max-width: 1280px) {
.list { .list {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.list { .list {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: 1fr;
} }
} }
@media (max-width: 720px) { @media (max-width: 720px) {

View File

@@ -112,7 +112,7 @@ function thumbUrl(g) {
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px; gap: 18px;
} }
.error { .error {
@@ -165,12 +165,12 @@ function thumbUrl(g) {
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.grid { .grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.grid { .grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: 1fr;
} }
} }
@media (max-width: 720px) { @media (max-width: 720px) {

View File

@@ -129,7 +129,7 @@ async function removeList(t) {
} }
.list { .list {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px; gap: 18px;
} }
.row { .row {
@@ -215,12 +215,12 @@ async function removeList(t) {
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.list { .list {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 960px) { @media (max-width: 960px) {
.list { .list {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: 1fr;
} }
} }
@media (max-width: 720px) { @media (max-width: 720px) {

View File

@@ -629,77 +629,6 @@ onUnmounted(() => {
</section> </section>
<template v-else> <template v-else>
<section class="head">
<div class="heroCard">
<div class="heroCard__main">
<input v-model="title" class="titleInput" placeholder="티어표 이름을 입력하세요" :readonly="!canEdit" />
<div v-if="untitledWarning" class="titleNotice">{{ untitledWarning }}</div>
<input
v-model="description"
class="descInput"
placeholder="설명(선택): 이 티어표의 기준/룰"
:readonly="!canEdit"
/>
<div class="hint">
<template v-if="canEdit">
그룹 이름/순서 변경과 아이템 드래그&드롭이 가능합니다. 저장하려면 <b>저장</b> 누르세요.
</template>
<template v-else>
공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 있어요.
</template>
</div>
</div>
<div class="heroCard__side">
<div class="thumbComposer">
<input ref="thumbnailFileEl" type="file" accept="image/*" class="hidden" @change="onThumbnailChange" />
<div class="thumbComposer__header">
<div class="thumbComposer__eyebrow">대표 썸네일</div>
<div class="thumbComposer__caption">목록 카드 상단에 표시됩니다.</div>
</div>
<div class="thumbComposer__preview">
<img v-if="displayThumbnailUrl" class="thumbComposer__image" :src="displayThumbnailUrl" alt="썸네일 미리보기" />
<div v-else class="thumbComposer__empty">썸네일 없음</div>
</div>
<div v-if="canEdit" class="thumbComposer__actions">
<button class="btn btn--ghost thumbComposer__button" @click="openThumbnailFile">썸네일 선택</button>
<button class="btn btn--danger thumbComposer__button" :disabled="!pendingThumbnailFile && !thumbnailSrc" @click="clearThumbnail">제거</button>
</div>
</div>
</div>
</div>
<div class="actions">
<div class="actions__left">
<button class="btn btn--download" @click="downloadImage">이미지 다운로드</button>
<button v-if="canEdit && !isNewTierList" class="btn btn--danger" @click="removeTierList">삭제</button>
</div>
<div class="actions__right">
<button v-if="canFavorite" class="btn btn--ghost" :disabled="isFavoriteBusy" @click="toggleFavorite">
{{ isFavorited ? ' 즐겨찾기' : ' 즐겨찾기' }} {{ favoriteCount }}
</button>
<button
v-if="canRequestTemplateCreate"
class="btn btn--ghost"
:disabled="isRequestingTemplate"
@click="openTemplateRequestModal"
>
템플릿 등록 요청
</button>
<button
v-if="canRequestTemplateUpdate"
class="btn btn--ghost"
:disabled="isRequestingTemplate"
@click="requestTemplate('update')"
>
{{ isRequestingTemplate ? '요청중...' : '템플릿 업데이트 요청' }}
</button>
<label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
<input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
<span>{{ isPublic ? '공개 ON' : '공개 OFF' }}</span>
</label>
<button v-if="canEdit" class="btn btn--save" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
</div>
</div>
</section>
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal"> <div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle"> <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
@@ -741,7 +670,22 @@ onUnmounted(() => {
</div> </div>
<section class="layout" :style="{ '--thumb-size': `${iconSize}px` }"> <section class="layout" :style="{ '--thumb-size': `${iconSize}px` }">
<div ref="boardEl" class="board"> <div class="editorMain">
<section class="head">
<div class="editorMain__headCopy">
<div class="editorMain__title">{{ gameName || gameId }}</div>
<div class="editorMain__subtitle">
<template v-if="canEdit">
그룹 이름/순서 변경과 아이템 드래그&드롭이 가능합니다.
</template>
<template v-else>
공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 있어요.
</template>
</div>
</div>
</section>
<div ref="boardEl" class="board">
<div v-if="canEdit && !isExporting" class="boardTools"> <div v-if="canEdit && !isExporting" class="boardTools">
<div class="boardTools__left"> <div class="boardTools__left">
<button class="btn btn--ghost" @click="addGroup">티어 추가</button> <button class="btn btn--ghost" @click="addGroup">티어 추가</button>
@@ -765,100 +709,180 @@ onUnmounted(() => {
<div v-if="isExporting" class="exportBoard__title">{{ effectiveTitle }}</div> <div v-if="isExporting" class="exportBoard__title">{{ effectiveTitle }}</div>
<div v-if="isExporting && description" class="exportBoard__description">{{ description }}</div> <div v-if="isExporting && description" class="exportBoard__description">{{ description }}</div>
<div ref="groupListEl" class="rows"> <div ref="groupListEl" class="rows">
<div v-for="g in groups" :key="g.id" class="row"> <div v-for="g in groups" :key="g.id" class="row">
<div class="row__label"> <div class="row__label">
<template v-if="isExporting"> <template v-if="isExporting">
<div class="row__exportName">{{ g.name }}</div> <div class="row__exportName">{{ g.name }}</div>
</template> </template>
<template v-else> <template v-else>
<span class="grab" title="드래그로 순서 변경" data-group-handle></span> <span class="grab" title="드래그로 순서 변경" data-group-handle></span>
<input v-model="g.name" class="groupName" :readonly="!canEdit" /> <input v-model="g.name" class="groupName" :readonly="!canEdit" />
<button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button> <button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
</template> </template>
</div> </div>
<div <div
class="row__drop" class="row__drop"
:data-list-type="'group'" :data-list-type="'group'"
:data-group-id="g.id" :data-group-id="g.id"
:ref="(el) => setGroupDropEl(g.id, el)" :ref="(el) => setGroupDropEl(g.id, el)"
> >
<div v-if="!isExporting" class="row__empty" v-show="g.itemIds.length === 0">여기로 드래그해서 배치</div> <div v-if="!isExporting" class="row__empty" v-show="g.itemIds.length === 0">여기로 드래그해서 배치</div>
<div v-for="id in g.itemIds" :key="id" class="cell" :data-item-id="id"> <div v-for="id in g.itemIds" :key="id" class="cell" :data-item-id="id">
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" /> <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<button <button
v-if="canEdit && !isExporting" v-if="canEdit && !isExporting"
class="cellRemoveBtn" class="cellRemoveBtn"
type="button" type="button"
title="아이템 빼내기" title="아이템 빼내기"
@pointerdown.stop @pointerdown.stop
@click.stop="removeItemFromGroup(g.id, id)" @click.stop="removeItemFromGroup(g.id, id)"
> >
× ×
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div v-if="isExporting" class="exportBoard__footer"> <div v-if="isExporting" class="exportBoard__footer">
<span>{{ effectiveAuthorName }}</span> <span>{{ effectiveAuthorName }}</span>
<span>{{ formatExportDate(fallbackTimestamp) }}</span> <span>{{ formatExportDate(fallbackTimestamp) }}</span>
</div> </div>
</div> </div>
</div>
<div class="sidebar">
<div class="sidebar__title">아이템</div>
<div class="sidebar__hint">
{{ canEdit ? '게임별 기본 이미지와 커스텀 업로드를 여기에 모읍니다.' : '공개 티어표는 보기 전용입니다.' }}
</div>
<div ref="poolEl" class="pool" data-list-type="pool">
<div v-for="id in pool" :key="id" class="poolItem" :data-item-id="id">
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
</div>
</div>
<div v-if="canEdit && customItems.length" class="customItemEditor">
<div class="customItemEditor__title">추가한 커스텀 아이템 이름 정리</div>
<div class="customItemEditor__desc">
템플릿 요청 전에 이름을 정리해두면 관리자가 그대로 기본 템플릿으로 반영할 있어요.
</div>
<div class="customItemEditor__list">
<label v-for="item in customItems" :key="item.id" class="customItemEditor__row">
<img class="customItemEditor__thumb" :src="resolveItemSrc(item)" :alt="item.label" />
<input
class="customItemEditor__input"
:value="item.label"
maxlength="60"
placeholder="아이템 이름"
@input="updateCustomItemLabel(item.id, $event.target.value)"
/>
</label>
</div>
</div>
<div
v-if="canEdit"
class="dropzone"
:class="{ 'dropzone--active': isDragActive }"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragEnter"
@dragleave="onDragLeave"
@drop.prevent="onDropFiles"
>
<div class="dropzone__title">커스텀 이미지 추가</div>
<div class="dropzone__desc">여러 이미지를 번에 드래그하거나 파일 선택으로 추가할 있어요.</div>
</div>
<input ref="fileEl" type="file" accept="image/*" multiple class="hidden" @change="onFileChange" />
<button v-if="canEdit" class="btn btn--ghost" @click="openFile">파일 선택</button>
</div>
</div> </div>
<div class="sidebar"> <aside class="editorSidebar">
<div class="sidebar__title">아이템</div> <div class="editorSidebar__section">
<div class="sidebar__hint"> <div class="editorSidebar__label">Title</div>
{{ canEdit ? '게임별 기본 이미지와 커스텀 업로드를 여기에 모읍니다.' : '공개 티어표는 보기 전용입니다.' }} <input v-model="title" class="editorSidebar__input" placeholder="Title Text" :readonly="!canEdit" />
<div v-if="untitledWarning" class="editorSidebar__hint editorSidebar__hint--warn">{{ untitledWarning }}</div>
</div> </div>
<div ref="poolEl" class="pool" data-list-type="pool">
<div v-for="id in pool" :key="id" class="poolItem" :data-item-id="id"> <div class="editorSidebar__section">
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" /> <div class="editorSidebar__label">Desc</div>
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div> <textarea
v-model="description"
class="editorSidebar__textarea"
placeholder="Description Text"
:readonly="!canEdit"
></textarea>
</div>
<div class="editorSidebar__section">
<div class="editorSidebar__label">대표 썸네일</div>
<input ref="thumbnailFileEl" type="file" accept="image/*" class="hidden" @change="onThumbnailChange" />
<div class="editorSidebar__thumbFrame">
<img v-if="displayThumbnailUrl" class="editorSidebar__thumbImage" :src="displayThumbnailUrl" alt="썸네일 미리보기" />
<div v-else class="editorSidebar__thumbEmpty">대표 썸네일</div>
</div> </div>
<button v-if="canEdit" class="btn btn--ghost editorSidebar__button" @click="openThumbnailFile">파일 업로드</button>
<div v-if="pendingThumbnailFile" class="editorSidebar__fileName">{{ pendingThumbnailFile.name }}</div>
</div> </div>
<div v-if="canEdit && customItems.length" class="customItemEditor">
<div class="customItemEditor__title">추가한 커스텀 아이템 이름 정리</div> <div class="editorSidebar__section">
<div class="customItemEditor__desc"> <button v-if="canFavorite" class="editorSidebar__favorite" :disabled="isFavoriteBusy" @click="toggleFavorite">
템플릿 요청 전에 이름을 정리해두면 관리자가 그대로 기본 템플릿으로 반영할 있어요. <span> 즐겨찾기</span>
</div> <span>{{ favoriteCount }}</span>
<div class="customItemEditor__list"> </button>
<label v-for="item in customItems" :key="item.id" class="customItemEditor__row"> </div>
<img class="customItemEditor__thumb" :src="resolveItemSrc(item)" :alt="item.label" />
<input <div class="editorSidebar__section editorSidebar__section--footer">
class="customItemEditor__input" <label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
:value="item.label" <input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
maxlength="60" <span>공개</span>
placeholder="아이템 이름" </label>
@input="updateCustomItemLabel(item.id, $event.target.value)" <div class="editorSidebar__actionGrid">
/> <button class="btn btn--ghost editorSidebar__button" @click="downloadImage">이미지 다운로드</button>
</label> <button v-if="canEdit" class="btn btn--save editorSidebar__button" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
</div> </div>
<button v-if="canEdit && !isNewTierList" class="btn btn--danger editorSidebar__button" @click="removeTierList">삭제</button>
<button
v-if="canRequestTemplateCreate"
class="btn btn--ghost editorSidebar__button"
:disabled="isRequestingTemplate"
@click="openTemplateRequestModal"
>
템플릿 등록 요청
</button>
<button
v-if="canRequestTemplateUpdate"
class="btn btn--ghost editorSidebar__button"
:disabled="isRequestingTemplate"
@click="requestTemplate('update')"
>
{{ isRequestingTemplate ? '요청중...' : '템플릿 업데이트 요청' }}
</button>
</div> </div>
<div </aside>
v-if="canEdit"
class="dropzone"
:class="{ 'dropzone--active': isDragActive }"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragEnter"
@dragleave="onDragLeave"
@drop.prevent="onDropFiles"
>
<div class="dropzone__title">커스텀 이미지 추가</div>
<div class="dropzone__desc">여러 이미지를 번에 드래그하거나 파일 선택으로 추가할 있어요.</div>
</div>
<input ref="fileEl" type="file" accept="image/*" multiple class="hidden" @change="onFileChange" />
<button v-if="canEdit" class="btn btn--ghost" @click="openFile">파일 선택</button>
</div>
</section> </section>
</template> </template>
</template> </template>
<style scoped> <style scoped>
.head { .head {
display: grid;
gap: 8px;
padding: 2px 2px 8px;
}
.editorMain {
min-width: 0;
display: grid; display: grid;
gap: 14px; gap: 14px;
padding: 6px 2px 14px; }
.editorMain__title {
font-size: 28px;
font-weight: 900;
letter-spacing: -0.04em;
}
.editorMain__subtitle {
color: rgba(255, 255, 255, 0.58);
font-size: 13px;
line-height: 1.5;
} }
.previewOnly { .previewOnly {
min-height: 100vh; min-height: 100vh;
@@ -936,128 +960,6 @@ onUnmounted(() => {
.previewOnly__poolItem { .previewOnly__poolItem {
display: inline-flex; display: inline-flex;
} }
.heroCard {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 360px);
gap: 18px;
align-items: stretch;
}
.heroCard__main,
.heroCard__side {
min-width: 0;
}
.heroCard__main {
display: grid;
gap: 12px;
}
.heroCard__side {
display: flex;
}
.titleInput {
width: 100%;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.04));
color: rgba(255, 255, 255, 0.92);
outline: none;
box-sizing: border-box;
}
.descInput {
width: 100%;
min-height: 92px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
color: rgba(255, 255, 255, 0.92);
outline: none;
box-sizing: border-box;
}
.hint {
opacity: 0.78;
font-size: 13px;
line-height: 1.6;
padding: 0 2px;
}
.titleNotice {
font-size: 13px;
line-height: 1.5;
color: rgba(251, 191, 36, 0.94);
padding: 0 2px;
}
.thumbComposer {
display: grid;
gap: 10px;
width: 100%;
padding: 16px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.12);
background:
radial-gradient(circle at top right, rgba(96, 165, 250, 0.12), transparent 46%),
rgba(255, 255, 255, 0.04);
box-sizing: border-box;
}
.thumbComposer__header {
display: grid;
gap: 4px;
}
.thumbComposer__eyebrow {
font-size: 13px;
font-weight: 900;
letter-spacing: 0.02em;
}
.thumbComposer__caption {
font-size: 12px;
opacity: 0.68;
}
.thumbComposer__preview {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 18px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(11, 18, 32, 0.78);
}
.thumbComposer__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbComposer__empty {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.62);
font-size: 13px;
}
.thumbComposer__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.thumbComposer__button {
width: 100%;
margin-top: 0;
}
.actions {
display: flex;
gap: 14px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.actions__left,
.actions__right {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.toggle { .toggle {
display: inline-flex; display: inline-flex;
gap: 8px; gap: 8px;
@@ -1123,8 +1025,8 @@ onUnmounted(() => {
} }
.layout { .layout {
display: grid; display: grid;
grid-template-columns: 1fr 320px; grid-template-columns: minmax(0, 1fr) 320px;
gap: 14px; gap: 16px;
align-items: start; align-items: start;
} }
.error { .error {
@@ -1135,10 +1037,10 @@ onUnmounted(() => {
background: rgba(239, 68, 68, 0.12); background: rgba(239, 68, 68, 0.12);
} }
.board { .board {
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04); background: rgba(48, 48, 48, 0.78);
border-radius: 16px; border-radius: 18px;
padding: 20px; padding: 18px;
align-self: start; align-self: start;
} }
.modalOverlay { .modalOverlay {
@@ -1419,11 +1321,106 @@ onUnmounted(() => {
object-fit: cover; object-fit: cover;
} }
.sidebar { .sidebar {
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04); background: rgba(48, 48, 48, 0.78);
border-radius: 16px; border-radius: 18px;
padding: 12px; padding: 12px;
} }
.editorSidebar {
display: grid;
align-content: start;
gap: 14px;
padding: 14px 12px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(18, 18, 18, 0.96);
position: sticky;
top: 14px;
}
.editorSidebar__section {
display: grid;
gap: 10px;
}
.editorSidebar__label {
font-size: 13px;
font-weight: 800;
color: rgba(255, 255, 255, 0.82);
}
.editorSidebar__input,
.editorSidebar__textarea {
width: 100%;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.92);
padding: 11px 12px;
outline: none;
resize: vertical;
}
.editorSidebar__textarea {
min-height: 92px;
}
.editorSidebar__hint {
font-size: 12px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.56);
}
.editorSidebar__hint--warn {
color: rgba(251, 191, 36, 0.92);
}
.editorSidebar__thumbFrame {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #4c4c4c;
}
.editorSidebar__thumbImage {
width: 100%;
height: 100%;
object-fit: cover;
}
.editorSidebar__thumbEmpty {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.36);
font-size: 13px;
}
.editorSidebar__button {
width: 100%;
margin-top: 0;
}
.editorSidebar__fileName {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
word-break: break-word;
}
.editorSidebar__favorite {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 12px 0 0;
border: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
background: transparent;
color: rgba(255, 255, 255, 0.9);
font-weight: 800;
cursor: pointer;
}
.editorSidebar__section--footer {
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.editorSidebar__actionGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.sidebar__title { .sidebar__title {
font-weight: 900; font-weight: 900;
margin-bottom: 6px; margin-bottom: 6px;
@@ -1537,26 +1534,14 @@ onUnmounted(() => {
.layout { .layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.actions {
justify-content: stretch;
}
.actions__left,
.actions__right {
width: 100%;
}
.actions__right {
justify-content: flex-end;
}
.row { .row {
grid-template-columns: 150px 1fr; grid-template-columns: 150px 1fr;
} }
.thumbComposer { .editorSidebar {
padding: 14px; position: static;
border-radius: 18px;
} }
.titleInput, .editorSidebar__actionGrid {
.descInput { grid-template-columns: 1fr;
border-radius: 16px;
} }
.requestChecklist__item { .requestChecklist__item {
grid-template-columns: 1fr; grid-template-columns: 1fr;