Compare commits

...

8 Commits

18 changed files with 1388 additions and 432 deletions

View File

@@ -649,6 +649,11 @@ async function listGameItems(gameId) {
return rows.map(mapGameItemRow)
}
async function findGameItemById(itemId) {
const rows = await query('SELECT id, game_id, src, label, created_at FROM game_items WHERE id = ? LIMIT 1', [itemId])
return mapGameItemRow(rows[0])
}
async function getGameDetail(gameId) {
const game = await findGameById(gameId)
if (!game) return null
@@ -1208,32 +1213,60 @@ async function getCustomItemUsageMeta() {
async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnly = false } = {}) {
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
const normalizedPage = Math.max(Number(page) || 1, 1)
const hasQuery = !!(queryText || '').trim()
const search = `%${(queryText || '').trim()}%`
const whereClause = hasQuery ? 'WHERE c.label LIKE ? OR c.src LIKE ? OR u.email LIKE ? OR u.nickname LIKE ?' : ''
const params = hasQuery ? [search, search, search, search] : []
const searchText = (queryText || '').trim()
const hasQuery = !!searchText
const search = `%${searchText}%`
const rows = await query(
`
SELECT
c.id,
c.owner_id,
c.src,
c.label,
c.created_at,
u.nickname,
u.email
FROM custom_items c
INNER JOIN users u ON u.id = c.owner_id
${whereClause}
ORDER BY c.created_at DESC
`,
params
)
const [customRows, gameItemRows, usageMeta] = await Promise.all([
query(
`
SELECT
c.id,
c.owner_id,
c.src,
c.label,
c.created_at,
u.nickname,
u.email
FROM custom_items c
INNER JOIN users u ON u.id = c.owner_id
${hasQuery ? 'WHERE c.label LIKE ? OR c.src LIKE ? OR u.email LIKE ? OR u.nickname LIKE ?' : ''}
ORDER BY c.created_at DESC
`,
hasQuery ? [search, search, search, search] : []
),
query(
`
SELECT
gi.id,
gi.game_id,
gi.src,
gi.label,
gi.created_at,
g.name AS game_name
FROM game_items gi
INNER JOIN games g ON g.id = gi.game_id
${hasQuery ? 'WHERE gi.label LIKE ? OR gi.src LIKE ? OR gi.game_id LIKE ? OR g.name LIKE ?' : ''}
ORDER BY gi.created_at DESC
`,
hasQuery ? [search, search, search, search] : []
),
getCustomItemUsageMeta(),
])
const { usageMap, linkedGamesMap } = await getCustomItemUsageMeta()
const allItems = rows
.map((row) => ({
const templateLinkedBySrc = new Map()
gameItemRows.forEach((row) => {
if (!row?.src) return
if (!templateLinkedBySrc.has(row.src)) templateLinkedBySrc.set(row.src, new Map())
templateLinkedBySrc.get(row.src).set(row.game_id, {
id: row.game_id,
name: row.game_name || row.game_id,
})
})
const customItems = customRows.map((row) => {
const linkedGames = Array.from((templateLinkedBySrc.get(row.src) || new Map()).values())
return {
id: row.id,
ownerId: row.owner_id,
src: row.src,
@@ -1241,10 +1274,37 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnl
createdAt: Number(row.created_at),
ownerName: row.nickname || row.email,
ownerEmail: row.email,
usageCount: usageMap.get(row.id) || 0,
linkedGames: linkedGamesMap.get(row.id) || [],
}))
.filter((item) => (orphanOnly ? item.usageCount === 0 : true))
usageCount: usageMeta.usageMap.get(row.id) || 0,
linkedGames,
sourceType: 'user',
sourceLabel: '사용자 업로드',
canDelete: true,
}
})
const templateItems = gameItemRows.map((row) => ({
id: row.id,
ownerId: '',
src: row.src,
label: row.label,
createdAt: Number(row.created_at),
ownerName: row.game_name || row.game_id,
ownerEmail: '',
usageCount: (templateLinkedBySrc.get(row.src) || new Map()).size,
linkedGames: Array.from((templateLinkedBySrc.get(row.src) || new Map()).values()),
sourceType: 'template',
sourceLabel: '관리자 템플릿',
canDelete: false,
sourceGameId: row.game_id,
sourceGameName: row.game_name || row.game_id,
}))
const allItems = [...customItems, ...templateItems]
.filter((item) => {
if (!orphanOnly) return true
return item.sourceType === 'user' && item.usageCount === 0 && item.linkedGames.length === 0
})
.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0))
const total = allItems.length
const offset = (normalizedPage - 1) * normalizedLimit
@@ -1935,6 +1995,7 @@ module.exports = {
listGames,
findGameById,
listGameItems,
findGameItemById,
getGameDetail,
createGame,
updateGameThumbnail,

View File

@@ -75,7 +75,7 @@ async function optimizeAndPersist({ file, width, height, fit, quality }) {
}
}
const filename = String(Date.now()) + '-' + nanoid() + '.webp'
const filename = nanoid() + '.webp'
const absoluteDir = path.join(UPLOAD_ROOT, OPTIMIZED_DIR)
const absolutePath = path.join(absoluteDir, filename)
const src = '/uploads/' + OPTIMIZED_DIR + '/' + filename

View File

@@ -8,6 +8,7 @@ const { nanoid } = require('nanoid')
const {
findUserById,
findGameById,
findGameItemById,
createGame,
listGames,
updateGameThumbnail,
@@ -322,12 +323,12 @@ async function removeCustomItemFiles(items) {
)
}
async function promoteCustomItemToGameItem({ customItem, gameId }) {
async function promoteLibraryItemToGameItem({ item, gameId }) {
return createGameItem({
id: nanoid(),
gameId,
src: customItem.src || '',
label: customItem.label,
src: item.src || '',
label: item.label,
})
}
@@ -428,6 +429,8 @@ router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
const result = await listCustomItems({ page: 1, limit: 200, orphanOnly: false })
const target = result.items.find((item) => item.id === req.params.itemId)
if (!target) return res.status(404).json({ error: 'not_found' })
if (!target.canDelete) return res.status(409).json({ error: 'item_locked' })
if (target.linkedGames.length > 0) return res.status(409).json({ error: 'item_linked' })
if (target.usageCount > 0) return res.status(409).json({ error: 'item_in_use' })
const items = await findCustomItemsByIds([target.id])
@@ -447,9 +450,11 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
if (!game) return res.status(404).json({ error: 'game_not_found' })
const customItem = await findCustomItemById(req.params.itemId)
if (!customItem) return res.status(404).json({ error: 'not_found' })
const gameItem = customItem ? null : await findGameItemById(req.params.itemId)
const sourceItem = customItem || gameItem
if (!sourceItem) return res.status(404).json({ error: 'not_found' })
const item = await promoteCustomItemToGameItem({ customItem, gameId: game.id })
const item = await promoteLibraryItemToGameItem({ item: sourceItem, gameId: game.id })
res.json({ item })
})

View File

@@ -1,6 +1,7 @@
# 할 일 및 이슈
## 중기 개선
- 라이트모드/다크모드 2차 보정까지 반영했으므로, 남은 작업은 전체 화면을 실제 사용 흐름으로 돌려 보며 대비·명도·아이콘 가독성을 미세하게 QA하는 최종 테마 점검 단계로 가져간다.
- 관리자용 티어표 승인/숨김 처리, 아이템 정렬 UI를 추가한다.
- 회원 일괄 작업(다중 선택, 일괄 비밀번호 초기화, 활동 저조 계정 정리) 같은 관리 보조 기능을 추가한다.
- 티어 행 프리셋 저장, 색상 관리, 행 복제 같은 고급 편집 기능을 추가한다.
@@ -8,3 +9,6 @@
- 업로드 파일은 MIME 타입뿐 아니라 파일 시그니처 기반 검증까지 확장한다.
- production에서 SESSION_SECRET 누락 시 서버가 부팅되지 않도록 강제한다.
- helmet 기반 보안 헤더와 업로드 정적 응답 헤더를 정리한다.
- 책 아이콘 기반 사용법 모달은 제작 흐름뿐 아니라 복사, 템플릿 업데이트 요청, 새 템플릿 요청까지 확장했으므로, 실제 16:9 스크린샷 자산과 단계별 문구를 운영 톤에 맞게 채운다.
- 관리자 아이템 라이브러리에서 동일 이미지(src)를 여러 템플릿이 공유하는 경우, 필요하면 묶어서 보거나 대표 카드로 합쳐 보는 후속 정리 옵션을 검토한다.

View File

@@ -1,5 +1,40 @@
# 업데이트 로그
## 2026-04-01 v1.3.34
- 관리자 아이템 관리 오른쪽 사이드에서는 `가져올 게임` 셀렉트를 제거하고, 사용자 업로드와 관리자 템플릿 이미지를 함께 검수하는 라이브러리 흐름으로 단순화함.
- 아이템 상세 모달은 좌측에 검색/정렬 가능한 게임 리스트를 두고 우측에 이미지·메타·액션을 배치하는 2단 레이아웃으로 재구성해, 많은 게임 속에서도 직접 검수 후 템플릿에 연결하기 쉽게 정리함.
- 아이템 라이브러리에는 이제 관리자 템플릿 이미지도 함께 표시하고, 배지로 `사용자 업로드 / 관리자 템플릿`을 구분하며 새 업로드 WebP 파일명에서는 시간 정보처럼 보이는 접두 숫자를 제거함.
- 템플릿 아이템까지 함께 보이는 구조에 맞춰 삭제 API도 사용자 업로드이면서 템플릿에 연결되지 않은 항목만 지울 수 있도록 안전 장치를 보강함.
## 2026-04-01 v1.3.33
- 라이트모드/다크모드 2차 보정으로 관리자 화면과 티어 에디터의 카드, 패널, 입력창, 모달, 썸네일 프레임을 전역 테마 변수 기준으로 다시 맞춰, 후속 화면에서도 명도 차가 더 자연스럽게 이어지도록 정리함.
- 공통 셸도 함께 손봐서 좌측 사이드 아이콘 필터와 텍스트 대비를 테마 변수 기반으로 전환하고, 가이드 모달·축소 검색 모달·내비 활성 상태까지 라이트모드에서 읽기 쉬운 톤으로 보정함.
- 전역 스타일 변수의 다크 기본값과 아이콘 필터 값을 바로잡아, 카드 배경과 텍스트 변수의 자기참조/오동작 가능성을 줄이고 이후 테마 QA 기준을 더 안정적으로 맞춤.
## 2026-04-01 v1.3.32
- 전역 테마 변수와 로컬 저장 기반 테마 토글을 추가해, Settings 화면 오른쪽 사이드에서 라이트모드/다크모드를 전환하고 재방문 시 같은 테마를 유지할 수 있게 함.
- 앱 셸, 홈, 게임 허브, 내 티어표, 즐겨찾기, 검색, 로그인, 설정 화면의 공통 카드·입력·텍스트 색을 테마 변수 기준으로 바꿔, 주요 사용자 화면은 라이트/다크 전환이 자연스럽게 이어지도록 1차 정리함.
- 관리자 화면과 티어 에디터처럼 스타일 밀도가 높은 화면은 후속 단계에서 세부 톤을 더 정교하게 맞추도록 todo 기준도 갱신함.
## 2026-04-01 v1.3.31
- 관리자 게임 관리의 오른쪽 사이드 게임 선택 리스트는 더 많은 항목을 한 번에 볼 수 있도록 최대 높이를 늘리고, 게임 카드 내부 간격도 사용자가 조정한 CSS 기준으로 반영해 목록 밀도를 다시 다듬음.
## 2026-04-01 v1.3.30
- 헤더의 `Tier Maker` 로고는 레인보우 그라데이션 텍스트로 바꿔 서비스 첫인상이 더 또렷하게 보이도록 정리하고, `by zenn`은 새 창으로 프로필 페이지를 여는 외부 링크로 연결함.
- 다음 단계 작업용으로 라이트모드/다크모드 전환 항목을 todo 문서에 추가해, 현재의 다크 톤 UI를 유지하면서도 이후 테마 확장 흐름을 공식 작업 목록에 올림.
## 2026-04-01 v1.3.29
- 책 아이콘 사용법 모달 진입점은 항상 보이는 오른쪽 사이드 하단 버튼 대신, Settings 화면에서만 왼쪽 사이드 하단의 보조 액션 버튼으로 옮겨 더 필요할 때만 찾게 되는 문맥형 진입 방식으로 정리함.
- 인증 스토어에 초기 세션 동기화 완료 상태를 추가하고, 앱 셸·로그인 화면·프로필 화면은 세션 확인 전까지 비로그인 UI를 먼저 그리지 않도록 보강해 첫 진입 시 화면이 갑자기 로그인 상태로 뒤집히는 플래시를 줄임.
## 2026-04-01 v1.3.28
- 책 아이콘 기반 사용법 모달은 기존의 단순 제작 흐름 안내를 넘어, 다른 사람 티어표 복사, 템플릿 업그레이드 요청, 새 템플릿 추가 요청, 즐겨찾기/내 티어표 관리까지 포함한 전체 기능 안내 허브로 확장함.
- 사용법 모달 제목과 단계 표기를 더 넓은 개념의 `기능 안내` 기준으로 정리하고, 실제 스크린샷이 없어도 설명만으로 핵심 기능을 순서대로 이해할 수 있게 단계 문구를 전면 보강함.
## 2026-04-01 v1.3.27
- 오른쪽 사이드 하단에 책 아이콘 진입점을 추가하고, 중앙 대형 사용법 모달을 열어 좌측 기능 리스트와 우측 16:9 설명 영역, 좌우 이동, 하단 페이지네이션까지 포함한 기본 가이드 흐름을 붙임.
- 사용법 모달의 스크린샷 영역은 우선 16:9 플레이스홀더와 설명 텍스트만 배치해, 실제 이미지 자산은 나중에 채워 넣을 수 있게 구조를 먼저 준비함.
## 2026-04-01 v1.3.26
- 오른쪽 사이드는 실제 광고 슬롯 기준을 300x600 세로 비율로 잡고, 데스크톱 우측 레일 폭도 325px로 조정해 300px 광고가 내부 패딩과 보더를 제외한 실폭 안에 자연스럽게 들어가도록 보정함.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M560-564v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-600q-38 0-73 9.5T560-564Zm0 220v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-380q-38 0-73 9t-67 27Zm0-110v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-490q-38 0-73 9.5T560-454ZM260-320q47 0 91.5 10.5T440-278v-394q-41-24-87-36t-93-12q-36 0-71.5 7T120-692v396q35-12 69.5-18t70.5-6Zm260 42q44-21 88.5-31.5T700-320q36 0 70.5 6t69.5 18v-396q-33-14-68.5-21t-71.5-7q-47 0-93 12t-87 36v394Zm-40 118q-48-38-104-59t-116-21q-42 0-82.5 11T100-198q-21 11-40.5-1T40-234v-482q0-11 5.5-21T62-752q46-24 96-36t102-12q58 0 113.5 15T480-740q51-30 106.5-45T700-800q52 0 102 12t96 36q11 5 16.5 15t5.5 21v482q0 23-19.5 35t-40.5 1q-37-20-77.5-31T700-240q-60 0-116 21t-104 59ZM280-494Z"/></svg>

After

Width:  |  Height:  |  Size: 895 B

View File

@@ -5,30 +5,40 @@ export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
status: 'idle',
hydrated: false,
}),
actions: {
async refresh() {
if (this.status === 'loading') return this.user
this.status = 'loading'
try {
const data = await api.me()
this.user = data.user
return this.user
} catch (error) {
this.user = null
return null
} finally {
this.status = 'idle'
this.hydrated = true
}
},
async signup(email, password) {
const user = await api.signup({ email, password })
this.user = user
this.hydrated = true
return user
},
async login(email, password) {
const user = await api.login({ email, password })
this.user = user
this.hydrated = true
return user
},
async logout() {
await api.logout()
this.user = null
this.hydrated = true
},
},
})

View File

@@ -2,12 +2,69 @@
font-family: 'Pretendard', 'Inter', 'Segoe UI', sans-serif;
line-height: 1.5;
font-weight: 400;
color: rgba(255, 255, 255, 0.92);
background: #121212;
color: var(--theme-text);
background: var(--theme-body-bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--theme-body-bg: #121212;
--theme-shell-bg: rgba(14, 14, 14, 0.96);
--theme-rail-bg: rgba(14, 14, 14, 0.92);
--theme-main-bg: rgba(18, 18, 18, 0.98);
--theme-workspace-bg: rgba(24, 24, 24, 0.92);
--theme-card-bg: rgba(62, 62, 62, 0.82);
--theme-card-bg-hover: rgba(70, 70, 70, 0.96);
--theme-card-border: rgba(255, 255, 255, 0.16);
--theme-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
--theme-surface-soft: rgba(255, 255, 255, 0.05);
--theme-surface-soft-2: rgba(255, 255, 255, 0.06);
--theme-surface-soft-3: rgba(255, 255, 255, 0.08);
--theme-pill-bg: rgba(255, 255, 255, 0.03);
--theme-border: rgba(255, 255, 255, 0.08);
--theme-border-strong: rgba(255, 255, 255, 0.12);
--theme-text: rgba(255, 255, 255, 0.92);
--theme-text-strong: rgba(255, 255, 255, 0.98);
--theme-text-muted: rgba(255, 255, 255, 0.74);
--theme-text-soft: rgba(255, 255, 255, 0.62);
--theme-text-faint: rgba(255, 255, 255, 0.4);
--theme-thumb-fallback-bg: #555;
--theme-select-arrow: rgba(255, 255, 255, 0.68);
--theme-danger-bg: rgba(239, 68, 68, 0.1);
--theme-danger-border: rgba(239, 68, 68, 0.18);
--theme-accent-bg: rgba(76, 133, 245, 0.92);
--theme-accent-text: #fff;
--theme-icon-filter: brightness(0) saturate(100%) invert(94%) sepia(6%) saturate(207%) hue-rotate(186deg) brightness(96%) contrast(92%);
}
:root[data-theme='light'] {
--theme-body-bg: #edf1f7;
--theme-shell-bg: rgba(244, 247, 252, 0.98);
--theme-rail-bg: rgba(248, 250, 253, 0.96);
--theme-main-bg: rgba(241, 244, 249, 0.98);
--theme-workspace-bg: rgba(250, 252, 255, 0.95);
--theme-card-bg: rgba(255, 255, 255, 0.98);
--theme-card-bg-hover: rgba(245, 248, 255, 0.98);
--theme-card-border: rgba(26, 32, 44, 0.1);
--theme-card-shadow: 0 18px 34px rgba(31, 41, 55, 0.08);
--theme-surface-soft: rgba(15, 23, 42, 0.05);
--theme-surface-soft-2: rgba(15, 23, 42, 0.07);
--theme-surface-soft-3: rgba(15, 23, 42, 0.1);
--theme-pill-bg: rgba(15, 23, 42, 0.04);
--theme-border: rgba(15, 23, 42, 0.1);
--theme-border-strong: rgba(15, 23, 42, 0.14);
--theme-text: rgba(20, 27, 40, 0.9);
--theme-text-strong: rgba(10, 15, 28, 0.98);
--theme-text-muted: rgba(55, 65, 81, 0.74);
--theme-text-soft: rgba(75, 85, 99, 0.64);
--theme-text-faint: rgba(100, 116, 139, 0.82);
--theme-thumb-fallback-bg: #d8dde8;
--theme-select-arrow: rgba(55, 65, 81, 0.72);
--theme-danger-bg: rgba(239, 68, 68, 0.1);
--theme-danger-border: rgba(239, 68, 68, 0.22);
--theme-accent-bg: rgba(64, 110, 226, 0.94);
--theme-accent-text: #fff;
--theme-icon-filter: brightness(0) saturate(100%) invert(14%) sepia(14%) saturate(652%) hue-rotate(182deg) brightness(95%) contrast(91%);
}
* {
@@ -22,7 +79,9 @@ body,
body {
margin: 0;
background: #121212;
background: var(--theme-body-bg);
color: var(--theme-text);
transition: background 220ms ease, color 220ms ease;
}
button,
@@ -43,7 +102,7 @@ a {
input,
select,
textarea {
color: rgba(255, 255, 255, 0.92);
color: var(--theme-text);
}
select {
@@ -51,8 +110,8 @@ select {
-webkit-appearance: none;
-moz-appearance: none;
background-image:
linear-gradient(45deg, transparent 50%, rgba(255, 255, 255, 0.72) 50%),
linear-gradient(135deg, rgba(255, 255, 255, 0.72) 50%, transparent 50%);
linear-gradient(45deg, transparent 50%, var(--theme-select-arrow) 50%),
linear-gradient(135deg, var(--theme-select-arrow) 50%, transparent 50%);
background-position:
calc(100% - 20px) calc(50% - 2px),
calc(100% - 14px) calc(50% - 2px);
@@ -99,19 +158,19 @@ p {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
}
.pageHead__title {
font-size: 32px;
line-height: 1.05;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
color: var(--theme-text-strong);
}
.pageHead__desc {
max-width: 720px;
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
}
.pageHead__aside {

View File

@@ -31,8 +31,9 @@ const customItemPage = ref(1)
const customItemLimit = ref(50)
const customItemTotal = ref(0)
const customItemOrphanOnly = ref(false)
const customItemTargetGameId = ref('')
const customItemModalTargetGameId = ref('')
const customItemModalGameQuery = ref('')
const customItemModalGameSort = ref('recent')
const adminTierLists = ref([])
const adminTierListQuery = ref('')
@@ -137,7 +138,7 @@ const activeTabDescription = computed(() => {
return '게임 생성, 선택, 썸네일, 기본 아이템 관리를 전용 작업 화면으로 분리했습니다.'
}
if (activeTab.value === 'items') {
return '사용자 커스텀 이미지를 검색하고, 미사용 이미지를 정리하거나 템플릿으로 승격할 수 있어요.'
return '사용자 업로드와 관리자 템플릿 이미지를 함께 검수하고, 필요한 게임에 직접 연결할 수 있어요.'
}
if (activeTab.value === 'tierlists') {
return tierlistsMode.value === 'requests'
@@ -170,7 +171,7 @@ const adminOverviewStats = computed(() => {
return [
{ label: '검색 결과', value: `${customItemTotal.value}` },
{ label: '미사용', value: `${orphanItems}` },
{ label: '대상 게임', value: customItemTargetGameId.value ? '선택됨' : '미선택' },
{ label: '템플릿 아이템', value: `${customItems.value.filter((item) => item.sourceType === 'template').length}` },
]
}
if (activeTab.value === 'tierlists') {
@@ -283,6 +284,21 @@ const imageDiagnosticsCards = computed(() => {
const visibleLinkedGames = computed(() =>
(modalTargetCustomItem.value?.linkedGames || []).filter((game) => game?.id && game.id !== 'freeform')
)
const filteredCustomItemModalGames = computed(() => {
const query = customItemModalGameQuery.value.trim().toLowerCase()
const linkedIds = new Set(visibleLinkedGames.value.map((game) => game.id))
const list = games.value.filter((game) => {
if (!query) return true
return `${game.name || ''} ${game.id || ''}`.toLowerCase().includes(query)
})
return list.slice().sort((a, b) => {
const linkedDelta = Number(linkedIds.has(a.id)) - Number(linkedIds.has(b.id))
if (linkedDelta !== 0) return linkedDelta
if (customItemModalGameSort.value === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
return Number(b.createdAt || 0) - Number(a.createdAt || 0)
})
})
const imageStatsPeriodLabel = computed(() => (imageStatsMonth.value ? `${imageStatsMonth.value} 기준` : '전체 기간'))
const imageStatsYearOptions = computed(() => {
@@ -532,7 +548,7 @@ async function refreshCustomItems() {
customItemPage.value = data.page || 1
customItemLimit.value = data.limit || customItemLimit.value
} catch (e) {
error.value = '사용자 커스텀 아이템을 불러오지 못했어요.'
error.value = '아이템 라이브러리를 불러오지 못했어요.'
}
}
@@ -1035,6 +1051,8 @@ function moveCustomItemPage(direction) {
function openCustomItemModal(item) {
modalTargetCustomItem.value = item || null
customItemModalTargetGameId.value = ''
customItemModalGameQuery.value = ''
customItemModalGameSort.value = 'recent'
customItemModalOpen.value = true
}
@@ -1042,12 +1060,14 @@ function closeCustomItemModal() {
customItemModalOpen.value = false
modalTargetCustomItem.value = null
customItemModalTargetGameId.value = ''
customItemModalGameQuery.value = ''
customItemModalGameSort.value = 'recent'
}
function openCustomItemDeleteModal(item) {
if (!item) return
if (item.usageCount > 0) {
error.value = '사용 중인 커스텀 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
error.value = '사용 중인 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
return
}
modalTargetCustomItem.value = item
@@ -1062,7 +1082,7 @@ async function removeCustomItem(item = modalTargetCustomItem.value) {
resetMessages()
if (!item) return
if (item.usageCount > 0) {
error.value = '사용 중인 커스텀 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
error.value = '사용 중인 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
return
}
@@ -1071,7 +1091,7 @@ async function removeCustomItem(item = modalTargetCustomItem.value) {
closeCustomItemDeleteModal()
closeCustomItemModal()
await refreshCustomItems()
success.value = '미사용 커스텀 이미지를 삭제했어요.'
success.value = '미사용 사용자 업로드 이미지를 삭제했어요.'
} catch (e) {
error.value = '커스텀 이미지 삭제에 실패했어요.'
}
@@ -1085,7 +1105,7 @@ async function removeUnusedCustomItems() {
try {
const data = await api.deleteAdminUnusedCustomItems({ q: customItemQuery.value })
await refreshCustomItems()
success.value = `${data.deletedCount || 0}개의 미사용 커스텀 이미지를 삭제했어요.`
success.value = `${data.deletedCount || 0}개의 미사용 사용자 업로드 이미지를 삭제했어요.`
} catch (e) {
error.value = '미사용 커스텀 이미지 일괄 삭제에 실패했어요.'
}
@@ -1094,7 +1114,7 @@ async function removeUnusedCustomItems() {
async function promoteCustomItem(item) {
resetMessages()
if (!customItemModalTargetGameId.value) {
error.value = '가져올 게임을 먼저 선택해주세요.'
error.value = '추가할 게임을 먼저 선택해주세요.'
return
}
@@ -1104,9 +1124,9 @@ async function promoteCustomItem(item) {
const targetGameName = games.value.find((game) => game.id === customItemModalTargetGameId.value)?.name || customItemModalTargetGameId.value
if (selectedGameId.value === customItemModalTargetGameId.value) await loadGame()
closeCustomItemModal()
success.value = `"${item.label}" 아이템을 ${targetGameName} 기본 템플릿으로 추가했어요.`
success.value = `"${item.label}" 이미지를 ${targetGameName} 템플릿으로 추가했어요.`
} catch (e) {
error.value = '커스텀 아이템을 기본 템플릿으로 가져오지 못했어요.'
error.value = '선택한 이미지를 템플릿으로 추가하지 못했어요.'
} finally {
item.isPromoting = false
}
@@ -1535,9 +1555,10 @@ async function saveFeaturedOrder() {
<template v-else-if="activeTab === 'items'">
<div class="panel">
<div v-if="!customItems.length" class="hint">조건에 맞는 커스텀 아이템이 없어요.</div>
<div v-if="!customItems.length" class="hint">조건에 맞는 관리 대상 아이템이 없어요.</div>
<div v-else class="customItemGrid">
<button v-for="item in customItems" :key="item.id" type="button" class="customItemCard" @click="openCustomItemModal(item)">
<span class="customItemCard__badge" :class="{ 'customItemCard__badge--template': item.sourceType === 'template' }">{{ item.sourceLabel }}</span>
<img class="customItemCard__image" :src="toApiUrl(item.src)" :alt="item.label" />
<div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
</button>
@@ -1912,19 +1933,54 @@ async function saveFeaturedOrder() {
<div v-if="customItemModalOpen" class="modalOverlay" @click.self="closeCustomItemModal">
<div class="modalCard modalCard--customItem" role="dialog" aria-modal="true">
<div class="modalCard__titleRow">
<div class="modalCard__title">커스텀 아이템 상세</div>
<div class="modalCard__title">아이템 상세</div>
<button class="btn btn--ghost btn--small" @click="closeCustomItemModal">닫기</button>
</div>
<div v-if="modalTargetCustomItem" class="customItemModal">
<div class="customItemModal__side">
<img class="customItemModal__image" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
<div class="customItemModal__selector">
<span class="customItemModal__label">기본 템플릿 추가</span>
<select v-model="customItemModalTargetGameId" class="select">
<option value="">게임 선택</option>
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
<aside class="customItemModal__pickerPanel">
<div class="customItemModal__pickerHead">
<div class="customItemModal__pickerEyebrow">GAME LIBRARY</div>
<div class="customItemModal__pickerTitle">템플릿으로 추가 게임</div>
</div>
<div class="customItemModal__pickerControls">
<input v-model="customItemModalGameQuery" class="input input--dense" placeholder="게임 이름, ID 검색" />
<select v-model="customItemModalGameSort" class="select">
<option value="recent">최신순</option>
<option value="oldest">오래된순</option>
</select>
</div>
<div class="customItemModal__gameList">
<button
v-for="game in filteredCustomItemModalGames"
:key="game.id"
type="button"
class="customItemModal__gameItem"
:class="{
'customItemModal__gameItem--active': customItemModalTargetGameId === game.id,
'customItemModal__gameItem--linked': visibleLinkedGames.some((entry) => entry.id === game.id),
}"
@click="customItemModalTargetGameId = game.id"
>
<span class="customItemModal__gameName">{{ game.name }}</span>
<span class="customItemModal__gameMeta">{{ game.id }}</span>
<span v-if="visibleLinkedGames.some((entry) => entry.id === game.id)" class="customItemModal__gameState">이미 포함됨</span>
</button>
</div>
</aside>
<div class="customItemModal__body">
<div class="customItemModal__titleRow">
<div>
<div class="customItemModal__title">{{ modalTargetCustomItem.label }}</div>
<div class="customItemModal__source">{{ modalTargetCustomItem.sourceLabel }}</div>
</div>
</div>
<img class="customItemModal__image" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
<div class="customItemModal__metaList">
<div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div>
<div class="customItemModal__metaRow"><span>업로더/출처</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div>
<div class="customItemModal__metaRow"><span>템플릿 연결</span><strong>{{ visibleLinkedGames.length }} 게임</strong></div>
<div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div>
</div>
<div class="customItemModal__linked">
<span class="customItemModal__label">템플릿에 사용 중인 게임</span>
<div v-if="visibleLinkedGames.length" class="customItemModal__chips">
@@ -1932,21 +1988,12 @@ async function saveFeaturedOrder() {
</div>
<div v-else class="hint hint--tight">아직 템플릿에 연결된 게임이 없어요.</div>
</div>
</div>
<div class="customItemModal__body">
<div class="customItemModal__title">{{ modalTargetCustomItem.label }}</div>
<div class="customItemModal__metaList">
<div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div>
<div class="customItemModal__metaRow"><span>업로더</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div>
<div class="customItemModal__metaRow"><span>사용 </span><strong>{{ modalTargetCustomItem.usageCount }} 티어표</strong></div>
<div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div>
</div>
<div class="customItemModal__actions">
<a class="btn btn--ghost" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
<button class="btn btn--ghost" :disabled="!customItemModalTargetGameId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
<a class="btn btn--ghost customItemModal__action" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
<button class="btn btn--ghost customItemModal__action" :disabled="!customItemModalTargetGameId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
{{ modalTargetCustomItem.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
</button>
<button class="btn btn--danger" :disabled="modalTargetCustomItem.usageCount > 0" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
<button v-if="modalTargetCustomItem.canDelete" class="btn btn--danger customItemModal__action" :disabled="modalTargetCustomItem.usageCount > 0 || visibleLinkedGames.length > 0" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
</div>
</div>
</div>
@@ -1956,7 +2003,7 @@ async function saveFeaturedOrder() {
<div v-if="customItemDeleteModalOpen" class="modalOverlay" @click.self="closeCustomItemDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">커스텀 아이템 삭제</div>
<div class="modalCard__desc">{{ modalTargetCustomItem ? '"' + modalTargetCustomItem.label + '" 이미지를 삭제할까요? 사용 상태의 이미지에만 삭제를 허용합니다.' : '' }}</div>
<div class="modalCard__desc">{{ modalTargetCustomItem ? '"' + modalTargetCustomItem.label + '" 이미지를 삭제할까요? 사용자 업로드이면서 어디에도 연결되지 않은 이미지에만 삭제를 허용합니다.' : '' }}</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeCustomItemDeleteModal">취소</button>
<button class="btn btn--danger" @click="removeCustomItem()">삭제</button>
@@ -2110,13 +2157,9 @@ async function saveFeaturedOrder() {
<option :value="50">50개씩 보기</option>
<option :value="200">200개씩 보기</option>
</select>
<select v-model="customItemTargetGameId" class="select">
<option value="">가져올 게임 선택</option>
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
</select>
<label class="checkRow checkRow--compact">
<input v-model="customItemOrphanOnly" type="checkbox" @change="toggleCustomItemOrphanOnly" />
<span>미사용 커스텀 이미지 보기</span>
<span>미사용 사용자 업로드 보기</span>
</label>
</div>
<div class="adminSidebar__actions">
@@ -2267,17 +2310,17 @@ async function saveFeaturedOrder() {
gap: 10px;
padding: 22px 24px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--theme-border);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)),
rgba(255, 255, 255, 0.02);
linear-gradient(180deg, var(--theme-surface-soft-2), var(--theme-pill-bg)),
var(--theme-pill-bg);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.22);
}
.adminHero__eyebrow {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-faint);
}
.adminHero__title {
margin: 0;
@@ -2288,7 +2331,7 @@ async function saveFeaturedOrder() {
}
.adminHero__desc {
margin: 0;
color: rgba(255, 255, 255, 0.66);
color: var(--theme-text-muted);
line-height: 1.6;
}
.adminHero__stats {
@@ -2302,14 +2345,14 @@ async function saveFeaturedOrder() {
gap: 6px;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(7, 7, 7, 0.18);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.adminHeroStat__label {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.46);
color: var(--theme-text-faint);
}
.adminHeroStat__value {
font-size: 22px;
@@ -2326,10 +2369,10 @@ async function saveFeaturedOrder() {
gap: 12px;
padding: 16px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--theme-border);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.015)),
rgba(13, 13, 13, 0.94);
linear-gradient(180deg, var(--theme-surface-soft), var(--theme-pill-bg)),
color-mix(in srgb, var(--theme-rail-bg) 98%, transparent);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
}
@@ -2344,10 +2387,10 @@ async function saveFeaturedOrder() {
}
.imageJobRow {
border: 1px solid var(--line);
border: 1px solid var(--theme-border);
border-radius: 14px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.02);
background: var(--theme-pill-bg);
display: grid;
gap: 4px;
}
@@ -2361,14 +2404,14 @@ async function saveFeaturedOrder() {
}
.imageJobRow__status {
color: var(--text-muted);
color: var(--theme-text-soft);
text-transform: capitalize;
}
.adminSidebar__label {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-faint);
}
.adminSidebar__tabs,
.adminSidebar__group,
@@ -2404,24 +2447,24 @@ async function saveFeaturedOrder() {
.adminSidebar__groupTitle {
font-size: 13px;
font-weight: 800;
color: rgba(255, 255, 255, 0.84);
color: var(--theme-text);
}
.adminGamePicker {
display: grid;
gap: 8px;
max-height: 320px;
max-height: 640px;
overflow: auto;
padding-right: 4px;
}
.adminGamePicker__item {
display: grid;
gap: 2px;
/* gap: 2px; */
padding: 11px 12px;
text-align: left;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.9);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
color: var(--theme-text);
cursor: pointer;
}
.adminGamePicker__item--active {
@@ -2434,7 +2477,7 @@ async function saveFeaturedOrder() {
}
.adminGamePicker__meta {
font-size: 11px;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -2444,12 +2487,12 @@ async function saveFeaturedOrder() {
gap: 4px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.sidebarStat__label {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
}
.sidebarStat__value {
font-size: 14px;
@@ -2500,8 +2543,8 @@ async function saveFeaturedOrder() {
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.92);
background: var(--theme-surface-soft);
color: var(--theme-text);
cursor: pointer;
font-weight: 800;
transition:
@@ -2512,7 +2555,7 @@ async function saveFeaturedOrder() {
.tab:hover,
.modeTab:hover {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.07);
background: var(--theme-surface-soft-2);
transform: translateY(-1px);
}
.tab--active,
@@ -2529,8 +2572,8 @@ async function saveFeaturedOrder() {
.panel {
border: 1px solid rgba(255, 255, 255, 0.1);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.018)),
rgba(34, 34, 34, 0.84);
linear-gradient(180deg, var(--theme-surface-soft), var(--theme-pill-bg)),
color-mix(in srgb, var(--theme-card-bg) 96%, transparent);
border-radius: 24px;
padding: 18px;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18);
@@ -2554,7 +2597,7 @@ async function saveFeaturedOrder() {
font-weight: 900;
}
.emptyState__desc {
color: rgba(255, 255, 255, 0.66);
color: var(--theme-text-muted);
line-height: 1.6;
}
.featuredOrderPanel {
@@ -2566,7 +2609,7 @@ async function saveFeaturedOrder() {
.featuredOrderPanel__list,
.featuredOrderPanel__picker {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.025);
background: color-mix(in srgb, var(--theme-pill-bg) 85%, transparent);
border-radius: 18px;
padding: 16px;
}
@@ -2585,8 +2628,8 @@ async function saveFeaturedOrder() {
align-items: center;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.22);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.featuredCard__meta {
display: flex;
@@ -2630,9 +2673,9 @@ async function saveFeaturedOrder() {
align-items: center;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.16);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
color: var(--theme-text);
cursor: pointer;
text-align: left;
}
@@ -2651,7 +2694,7 @@ async function saveFeaturedOrder() {
.section {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid var(--theme-border);
}
.section--topGrid {
display: grid;
@@ -2674,13 +2717,13 @@ async function saveFeaturedOrder() {
}
.adminCard {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.025);
background: color-mix(in srgb, var(--theme-pill-bg) 85%, transparent);
border-radius: 18px;
padding: 16px;
min-width: 0;
}
.adminCard--muted {
background: rgba(255, 255, 255, 0.02);
background: var(--theme-pill-bg);
}
.sectionHeader {
display: flex;
@@ -2725,8 +2768,8 @@ async function saveFeaturedOrder() {
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
color: rgba(255, 255, 255, 0.92);
background: var(--theme-pill-bg);
color: var(--theme-text);
outline: none;
/* margin-top: 10px; */
}
@@ -2777,7 +2820,7 @@ async function saveFeaturedOrder() {
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
color: var(--theme-text);
cursor: pointer;
font-weight: 800;
text-align: center;
@@ -2816,7 +2859,7 @@ async function saveFeaturedOrder() {
border-color: rgba(239, 68, 68, 0.28);
}
.btn--ghost {
background: rgba(255, 255, 255, 0.03);
background: var(--theme-pill-bg);
}
.detailHead {
display: flex;
@@ -2845,12 +2888,12 @@ async function saveFeaturedOrder() {
object-fit: cover;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
}
.selectedThumb--empty {
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-soft);
}
.selectedThumb--sidebar {
width: 100%;
@@ -2872,7 +2915,7 @@ async function saveFeaturedOrder() {
overflow: hidden;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
background: var(--theme-pill-bg);
text-align: left;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
@@ -2888,7 +2931,7 @@ async function saveFeaturedOrder() {
place-items: center;
min-height: 52px;
padding: 12px 16px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(6, 9, 16, 0.86) 46%, rgba(6, 9, 16, 0.94) 100%);
background: linear-gradient(180deg, transparent 0%, color-mix(in srgb, var(--theme-main-bg) 82%, transparent) 46%, color-mix(in srgb, var(--theme-main-bg) 94%, transparent) 100%);
}
.thumbDropZone__title {
font-weight: 900;
@@ -2910,8 +2953,8 @@ async function saveFeaturedOrder() {
.dropZone {
padding: 18px;
border-radius: 16px;
border: 1px dashed rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
border: 1px dashed var(--theme-border-strong);
background: var(--theme-pill-bg);
transition:
border-color 0.16s ease,
background 0.16s ease,
@@ -2941,7 +2984,7 @@ async function saveFeaturedOrder() {
padding: 12px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
}
.itemPreviewGrid {
display: grid;
@@ -2964,7 +3007,7 @@ async function saveFeaturedOrder() {
overflow: hidden;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
background: var(--theme-pill-bg);
}
.itemDraftRow__body {
min-width: 0;
@@ -2976,7 +3019,7 @@ async function saveFeaturedOrder() {
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
background: var(--theme-pill-bg);
}
.itemPreviewImage {
width: 100%;
@@ -2987,7 +3030,7 @@ async function saveFeaturedOrder() {
min-height: 192px;
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-soft);
font-size: 13px;
text-align: center;
line-height: 1.5;
@@ -3001,7 +3044,7 @@ async function saveFeaturedOrder() {
.thumbCard {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
padding: 12px;
min-width: 0;
}
@@ -3039,9 +3082,10 @@ async function saveFeaturedOrder() {
}
.customItemCard {
appearance: none;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
overflow: hidden;
display: grid;
gap: 10px;
@@ -3051,6 +3095,24 @@ async function saveFeaturedOrder() {
cursor: pointer;
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.customItemCard__badge {
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
display: inline-flex;
align-items: center;
padding: 5px 8px;
border-radius: 999px;
background: color-mix(in srgb, var(--theme-main-bg) 82%, transparent);
color: var(--theme-text);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.02em;
}
.customItemCard__badge--template {
background: rgba(96, 165, 250, 0.18);
}
.customItemCard:hover {
border-color: rgba(126, 162, 255, 0.42);
background: rgba(255, 255, 255, 0.06);
@@ -3062,7 +3124,7 @@ async function saveFeaturedOrder() {
object-fit: cover;
display: block;
border-radius: 14px;
background: rgba(0, 0, 0, 0.18);
background: var(--theme-pill-bg);
}
.customItemCard__title {
min-width: 0;
@@ -3076,36 +3138,91 @@ async function saveFeaturedOrder() {
}
.customItemModal {
display: grid;
grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.customItemModal__side {
.customItemModal__pickerPanel {
display: grid;
gap: 12px;
min-width: 0;
padding: 16px;
border-radius: 20px;
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.customItemModal__image {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
.customItemModal__pickerHead {
display: grid;
gap: 4px;
}
.customItemModal__pickerEyebrow {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--theme-text-faint);
}
.customItemModal__pickerTitle {
font-size: 18px;
font-weight: 900;
}
.customItemModal__pickerControls {
display: grid;
gap: 10px;
}
.customItemModal__gameList {
display: grid;
gap: 8px;
max-height: 360px;
overflow: auto;
}
.customItemModal__gameItem {
display: grid;
gap: 4px;
padding: 12px 13px;
border-radius: 16px;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
color: var(--theme-text);
text-align: left;
cursor: pointer;
}
.customItemModal__gameItem--active {
border-color: rgba(96, 165, 250, 0.42);
background: rgba(96, 165, 250, 0.12);
}
.customItemModal__gameItem--linked {
border-style: dashed;
}
.customItemModal__gameName {
font-size: 13px;
font-weight: 800;
}
.customItemModal__gameMeta,
.customItemModal__gameState {
font-size: 11px;
color: var(--theme-text-soft);
}
.customItemModal__body {
display: grid;
gap: 14px;
min-width: 0;
}
.customItemModal__selector,
.customItemModal__titleRow,
.customItemModal__linked {
display: grid;
gap: 8px;
}
.customItemModal__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 20px;
background: var(--theme-surface-soft);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.customItemModal__label {
font-size: 11px;
color: rgba(255, 255, 255, 0.52);
color: var(--theme-text-faint);
}
.customItemModal__chips {
display: flex;
@@ -3118,6 +3235,11 @@ async function saveFeaturedOrder() {
line-height: 1.35;
word-break: break-word;
}
.customItemModal__source {
margin-top: 4px;
font-size: 12px;
color: var(--theme-text-soft);
}
.customItemModal__metaList {
display: grid;
gap: 10px;
@@ -3129,7 +3251,7 @@ async function saveFeaturedOrder() {
}
.customItemModal__metaRow span {
font-size: 11px;
color: rgba(255, 255, 255, 0.46);
color: var(--theme-text-faint);
}
.customItemModal__metaRow strong {
min-width: 0;
@@ -3137,15 +3259,23 @@ async function saveFeaturedOrder() {
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: rgba(255, 255, 255, 0.84);
color: var(--theme-text);
}
.customItemModal__actions {
display: flex;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
flex-wrap: wrap;
}
.customItemModal__action {
width: 100%;
min-width: 0;
padding-inline: 12px;
white-space: normal;
line-height: 1.35;
text-align: center;
}
.modalCard--customItem {
width: min(760px, 100%);
width: min(980px, 100%);
}
.pager {
margin-top: 16px;
@@ -3169,7 +3299,7 @@ async function saveFeaturedOrder() {
position: relative;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
padding: 24px 16px 16px;
overflow: visible;
}
@@ -3253,7 +3383,7 @@ async function saveFeaturedOrder() {
inset: auto 0 0 0;
padding: 10px 0 6px;
background: linear-gradient(180deg, rgba(7, 10, 18, 0), rgba(7, 10, 18, 0.88));
color: rgba(255, 255, 255, 0.9);
color: var(--theme-text);
font-size: 10px;
font-weight: 800;
opacity: 0;
@@ -3296,7 +3426,7 @@ async function saveFeaturedOrder() {
}
.userInfoLine span {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
}
.userInfoLine strong {
min-width: 0;
@@ -3310,7 +3440,7 @@ async function saveFeaturedOrder() {
}
.field__label {
font-size: 13px;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-soft);
}
.field__input {
width: 100%;
@@ -3318,7 +3448,7 @@ async function saveFeaturedOrder() {
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
background: transparent;
color: rgba(255, 255, 255, 0.94);
color: var(--theme-text-strong);
outline: none;
font-size: 18px;
letter-spacing: -0.02em;
@@ -3328,7 +3458,7 @@ async function saveFeaturedOrder() {
}
.field__hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-faint);
}
.userEditForm {
@@ -3358,7 +3488,7 @@ async function saveFeaturedOrder() {
border-radius: 999px;
border: 1px solid rgba(96, 165, 250, 0.28);
background: rgba(96, 165, 250, 0.1);
color: rgba(191, 219, 254, 0.92);
color: color-mix(in srgb, var(--theme-accent-bg) 80%, white);
font-size: 12px;
font-weight: 700;
}
@@ -3377,11 +3507,11 @@ async function saveFeaturedOrder() {
padding: 0;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
cursor: pointer;
}
.iconActionButton__icon {
color: rgba(255, 255, 255, 0.92);
color: var(--theme-text);
}
.iconActionButton:disabled {
cursor: not-allowed;
@@ -3400,7 +3530,7 @@ async function saveFeaturedOrder() {
padding: 0;
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.46);
color: var(--theme-text-faint);
font-size: 9px;
line-height: 1.4;
letter-spacing: 0.01em;
@@ -3421,7 +3551,7 @@ async function saveFeaturedOrder() {
padding: 18px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
background: var(--theme-pill-bg);
}
.templateRequestCard__head {
display: flex;
@@ -3436,7 +3566,7 @@ async function saveFeaturedOrder() {
}
.templateRequestCard__desc {
margin-top: 6px;
color: rgba(255, 255, 255, 0.74);
color: var(--theme-text-muted);
line-height: 1.55;
white-space: pre-line;
}
@@ -3460,7 +3590,7 @@ async function saveFeaturedOrder() {
object-fit: cover;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
}
.templateRequestItem__label {
font-size: 12px;
@@ -3480,7 +3610,7 @@ async function saveFeaturedOrder() {
}
.templateRequestField__label {
font-size: 11px;
color: rgba(255, 255, 255, 0.52);
color: var(--theme-text-faint);
}
.templateRequestCard__actions {
display: flex;
@@ -3497,11 +3627,11 @@ async function saveFeaturedOrder() {
max-height: 240px;
object-fit: cover;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.requestPreview__desc {
color: rgba(255, 255, 255, 0.74);
color: var(--theme-text-muted);
line-height: 1.6;
white-space: pre-line;
}
@@ -3532,7 +3662,7 @@ async function saveFeaturedOrder() {
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
min-height: 72px;
}
.requestPreview__item--muted {
@@ -3569,7 +3699,7 @@ async function saveFeaturedOrder() {
gap: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px;
background: rgba(255, 255, 255, 0.03);
background: var(--theme-pill-bg);
padding: 16px;
}
.tierAdminCard__preview {
@@ -3581,7 +3711,7 @@ async function saveFeaturedOrder() {
object-fit: cover;
display: block;
border-radius: 14px;
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
}
.tierAdminCard__thumb--empty {
background: linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
@@ -3603,7 +3733,7 @@ async function saveFeaturedOrder() {
}
.tierAdminCard__desc {
margin-top: 6px;
color: rgba(255, 255, 255, 0.74);
color: var(--theme-text-muted);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -3627,7 +3757,7 @@ async function saveFeaturedOrder() {
padding: 7px 10px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
background: var(--theme-surface-soft);
font-size: 12px;
font-weight: 800;
}
@@ -3641,8 +3771,8 @@ async function saveFeaturedOrder() {
gap: 10px;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.14);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.tierAdminSection__title {
font-weight: 800;
@@ -3663,9 +3793,9 @@ async function saveFeaturedOrder() {
justify-items: center;
padding: 12px 10px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
color: var(--theme-text);
cursor: pointer;
text-align: center;
min-width: 0;
@@ -3691,7 +3821,7 @@ async function saveFeaturedOrder() {
display: grid;
place-items: center;
padding: 20px;
background: rgba(3, 7, 18, 0.66);
background: color-mix(in srgb, var(--theme-body-bg) 76%, transparent);
backdrop-filter: blur(6px);
}
.modalCard {
@@ -3701,7 +3831,7 @@ async function saveFeaturedOrder() {
padding: 20px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(11, 18, 32, 0.96);
background: color-mix(in srgb, var(--theme-main-bg) 96%, transparent);
}
.modalCard--preview {
width: min(1200px, 100%);
@@ -3737,7 +3867,7 @@ async function saveFeaturedOrder() {
min-height: min(80vh, 820px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
background: rgba(255, 255, 255, 0.02);
background: var(--theme-pill-bg);
}
.importModeTabs {
display: flex;
@@ -3764,6 +3894,9 @@ async function saveFeaturedOrder() {
.customItemModal {
grid-template-columns: 1fr;
}
.customItemModal__actions {
grid-template-columns: 1fr;
}
.adminSidebar {
display: none;
}

View File

@@ -110,16 +110,16 @@ onMounted(loadFavorites)
.select {
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
color: var(--theme-text);
}
.btn {
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft-2);
color: var(--theme-text);
font-weight: 800;
cursor: pointer;
}
@@ -133,18 +133,18 @@ onMounted(loadFavorites)
}
.boardCard {
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
border: 1px solid var(--theme-card-border);
background: var(--theme-card-bg);
overflow: hidden;
display: grid;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
transition:
transform 0.16s ease,
background 0.16s ease;
}
.boardCard:hover {
transform: translateY(-2px);
background: rgba(70, 70, 70, 0.96);
background: var(--theme-card-bg-hover);
}
.boardCard__body {
border: 0;
@@ -172,10 +172,10 @@ onMounted(loadFavorites)
object-fit: cover;
}
.boardCard__thumbPlaceholder {
background: #555;
background: var(--theme-thumb-fallback-bg);
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.4);
color: var(--theme-text-faint);
font-size: 13px;
font-weight: 700;
}
@@ -222,7 +222,7 @@ onMounted(loadFavorites)
height: 22px;
border-radius: 9999px;
object-fit: cover;
background: rgba(255, 255, 255, 0.08);
background: var(--theme-border);
flex: 0 0 auto;
}
.boardCard__avatar--fallback {
@@ -235,7 +235,7 @@ onMounted(loadFavorites)
.favoriteStat {
flex: 0 0 auto;
font-size: 13px;
color: rgba(255, 255, 255, 0.64);
color: var(--theme-text-faint);
white-space: nowrap;
}

View File

@@ -149,7 +149,7 @@ function submitSearch() {
}
.dashboardHero__eyebrow {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
text-transform: uppercase;
letter-spacing: 0.08em;
}
@@ -157,15 +157,15 @@ function submitSearch() {
margin: 4px 0 6px;
font-size: 32px;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
color: var(--theme-text-strong);
}
.dashboardHero__desc {
margin: 0;
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
max-width: 720px;
}
.panel {
/* border: 1px solid rgba(255, 255, 255, 0.08); */
/* border: 1px solid var(--theme-border); */
background: transparent;
border-radius: 0;
padding: 0;
@@ -174,8 +174,8 @@ function submitSearch() {
margin: 10px 0 14px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.12);
border: 1px solid var(--theme-danger-border);
background: var(--theme-danger-bg);
}
.panel__title {
font-weight: 800;
@@ -183,7 +183,7 @@ function submitSearch() {
}
.panel__sub {
margin-top: 6px;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-muted);
font-size: 13px;
}
.panel__head {
@@ -204,16 +204,16 @@ function submitSearch() {
min-width: 240px;
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
color: var(--theme-text);
}
.searchBar__button {
padding: 11px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft-2);
color: var(--theme-text);
font-weight: 800;
cursor: pointer;
}
@@ -232,18 +232,18 @@ function submitSearch() {
.boardCard {
min-width: 0;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-card-border);
background: var(--theme-card-bg);
color: var(--theme-text);
display: grid;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
transition:
transform 0.16s ease,
background 0.16s ease;
}
.boardCard:hover {
background: rgba(70, 70, 70, 0.96);
background: var(--theme-card-bg-hover);
transform: translateY(-2px);
}
.boardCard__body {
@@ -293,10 +293,10 @@ function submitSearch() {
.boardCard__thumbPlaceholder {
width: 100%;
height: 100%;
background: #555;
background: var(--theme-thumb-fallback-bg);
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.4);
color: var(--theme-text-faint);
font-size: 13px;
font-weight: 700;
border-radius: 18px;
@@ -362,7 +362,7 @@ function submitSearch() {
border-radius: 9999px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.08);
background: var(--theme-border);
flex: 0 0 auto;
}
.boardCard__avatar--fallback {
@@ -377,7 +377,7 @@ function submitSearch() {
min-width: 0;
max-width: 100%;
font-size: 13px;
color: rgba(255, 255, 255, 0.64);
color: var(--theme-text-faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -120,31 +120,31 @@ function thumbUrl(g) {
margin: 0 0 16px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(239, 68, 68, 0.18);
background: rgba(239, 68, 68, 0.1);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-danger-border);
background: var(--theme-danger-bg);
color: var(--theme-text);
}
.pageHead__searchState {
margin-top: 8px;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-muted);
}
.libraryCard {
position: relative;
text-align: left;
padding: 14px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-card-border);
background: var(--theme-card-bg);
color: var(--theme-text);
cursor: pointer;
display: grid;
gap: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
transition: transform 0.16s ease, background 0.16s ease;
will-change: transform, opacity;
}
.libraryCard:hover {
background: rgba(70, 70, 70, 0.96);
background: var(--theme-card-bg-hover);
transform: translateY(-2px);
}
.libraryCard__main {
@@ -191,8 +191,8 @@ function thumbUrl(g) {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #555;
border: 1px solid var(--theme-surface-soft-2);
background: var(--theme-thumb-fallback-bg);
overflow: hidden;
display: grid;
place-items: center;
@@ -204,7 +204,7 @@ function thumbUrl(g) {
}
.libraryCard__thumbFallback {
font-size: 14px;
color: rgba(255, 255, 255, 0.4);
color: var(--theme-text-faint);
}
.libraryCard__body {
display: grid;
@@ -241,7 +241,7 @@ function thumbUrl(g) {
.libraryEmpty {
padding: 20px 0;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-muted);
}
@media (max-width: 1400px) {
.libraryGrid {

View File

@@ -30,8 +30,15 @@ const description = computed(() =>
: '저장한 티어표와 즐겨찾기, 프로필 설정을 이어서 관리할 수 있어요.'
)
const submitLabel = computed(() => (mode.value === 'signup' ? '가입하기' : '로그인'))
const authReady = computed(() => auth.hydrated)
const checkingSession = computed(() => !authReady.value || auth.status === 'loading')
onMounted(async () => {
if (!auth.hydrated) await auth.refresh()
if (auth.user) {
router.replace(typeof route.query.redirect === 'string' ? route.query.redirect : '/me')
return
}
try {
const meta = await api.authMeta()
hasUsers.value = !!meta.hasUsers
@@ -40,6 +47,15 @@ onMounted(async () => {
}
})
watch(
() => [auth.hydrated, auth.user],
([hydrated, user]) => {
if (!hydrated || !user) return
router.replace(typeof route.query.redirect === 'string' ? route.query.redirect : '/me')
},
{ immediate: true }
)
async function submit() {
error.value = ''
if (mode.value === 'signup' && password.value !== passwordConfirm.value) {
@@ -66,7 +82,11 @@ async function submit() {
</div>
</header>
<section class="authScreen">
<section v-if="checkingSession" class="authScreen authScreen--loading">
<div class="authLoading">로그인 상태를 확인하고 있어요.</div>
</section>
<section v-else class="authScreen">
<div class="authTabs" role="tablist" aria-label="로그인 또는 회원가입">
<button type="button" class="authTabs__button" :class="{ 'authTabs__button--active': mode === 'login' }" @click="mode = 'login'">
로그인
@@ -128,14 +148,24 @@ async function submit() {
padding-top: 4px;
}
.authScreen--loading {
min-height: 220px;
align-items: center;
}
.authLoading {
color: var(--theme-text-muted);
font-size: 15px;
}
.authTabs {
display: inline-flex;
gap: 8px;
width: fit-content;
padding: 6px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.authTabs__button {
@@ -144,14 +174,14 @@ async function submit() {
border: 0;
border-radius: 999px;
background: transparent;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-muted);
font-weight: 700;
cursor: pointer;
}
.authTabs__button--active {
background: rgba(76, 133, 245, 0.22);
color: rgba(255, 255, 255, 0.96);
color: var(--theme-text-strong);
}
.authFields {
@@ -166,16 +196,16 @@ async function submit() {
.field__label {
font-size: 13px;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-muted);
}
.field__input {
width: 100%;
padding: 14px 0;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid var(--theme-border-strong);
background: transparent;
color: rgba(255, 255, 255, 0.94);
color: var(--theme-text);
outline: none;
font-size: 18px;
letter-spacing: -0.02em;
@@ -187,7 +217,7 @@ async function submit() {
.field__hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
}
.roleBadge {
@@ -196,7 +226,7 @@ async function submit() {
border-radius: 999px;
border: 1px solid rgba(96, 165, 250, 0.28);
background: rgba(96, 165, 250, 0.1);
color: rgba(191, 219, 254, 0.92);
color: var(--theme-text);
font-size: 12px;
font-weight: 700;
}
@@ -218,14 +248,14 @@ async function submit() {
.primaryAction {
border: 1px solid rgba(76, 133, 245, 0.96);
background: rgba(76, 133, 245, 0.92);
color: #fff;
background: var(--theme-accent-bg);
color: var(--theme-accent-text);
}
.secondaryAction {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.86);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft);
color: var(--theme-text);
}
@media (max-width: 720px) {

View File

@@ -124,7 +124,7 @@ function openList(t) {
}
.dashboardHero__eyebrow {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
text-transform: uppercase;
letter-spacing: 0.08em;
}
@@ -132,11 +132,11 @@ function openList(t) {
margin: 4px 0 6px;
font-size: 32px;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
color: var(--theme-text-strong);
}
.dashboardHero__desc {
margin: 0;
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
max-width: 720px;
}
.panel {
@@ -155,18 +155,18 @@ function openList(t) {
.boardCard {
min-width: 0;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-card-border);
background: var(--theme-card-bg);
color: var(--theme-text);
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
transition:
transform 0.16s ease,
background 0.16s ease;
}
.boardCard:hover {
transform: translateY(-2px);
background: rgba(70, 70, 70, 0.96);
background: var(--theme-card-bg-hover);
}
.boardCard__body {
min-width: 0;
@@ -197,10 +197,10 @@ function openList(t) {
.boardCard__thumbPlaceholder {
width: 100%;
height: 100%;
background: #555;
background: var(--theme-thumb-fallback-bg);
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.4);
color: var(--theme-text-faint);
font-size: 13px;
font-weight: 700;
border-radius: 18px;
@@ -259,7 +259,7 @@ function openList(t) {
border-radius: 9999px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.08);
background: var(--theme-border);
flex: 0 0 auto;
}
.boardCard__avatar--fallback {
@@ -274,7 +274,7 @@ function openList(t) {
min-width: 0;
max-width: 100%;
font-size: 13px;
color: rgba(255, 255, 255, 0.64);
color: var(--theme-text-faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -30,14 +30,19 @@ const avatarUrl = computed(() => {
return toApiUrl(auth.user.avatarSrc)
})
const authReady = computed(() => auth.hydrated)
const displayInitial = computed(() => {
const email = auth.user?.email || 'U'
return email[0].toUpperCase()
})
onMounted(async () => {
await auth.refresh()
if (!auth.user) router.push('/login')
if (!auth.hydrated) await auth.refresh()
if (!auth.user) {
router.replace('/login')
return
}
nickname.value = auth.user?.nickname || ''
removeAvatar.value = false
})
@@ -121,7 +126,11 @@ async function logout() {
</div>
</header>
<section v-if="auth.user" class="settingsScreen">
<section v-if="!authReady" class="settingsScreen settingsScreen--loading">
<div class="settingsLoading">계정 정보를 불러오고 있어요.</div>
</section>
<section v-else-if="auth.user" class="settingsScreen">
<div class="settingsIdentity">
<div class="avatarButtonWrap">
<button class="avatarButton" type="button" @click="openAvatarPicker">
@@ -185,6 +194,16 @@ async function logout() {
padding-top: 4px;
}
.settingsScreen--loading {
min-height: 240px;
align-items: center;
}
.settingsLoading {
color: var(--theme-text-muted);
font-size: 15px;
}
.settingsIdentity {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
@@ -202,15 +221,15 @@ async function logout() {
position: relative;
width: 120px;
height: 120px;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--theme-border-strong);
border-radius: 9999px;
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.92);
background: var(--theme-pill-bg);
color: var(--theme-text);
overflow: hidden;
cursor: pointer;
display: grid;
place-items: center;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: var(--theme-card-shadow);
}
.avatarButton__image {
@@ -222,7 +241,7 @@ async function logout() {
.avatarButton__fallback {
font-size: 34px;
font-weight: 900;
color: rgba(255, 255, 255, 0.86);
color: var(--theme-text);
}
.avatarButton__overlay {
@@ -232,7 +251,7 @@ async function logout() {
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.72));
font-size: 12px;
font-weight: 700;
color: rgba(255, 255, 255, 0.82);
color: var(--theme-text);
}
.avatarButton__remove {
@@ -243,8 +262,8 @@ async function logout() {
height: 30px;
border: 0;
border-radius: 999px;
background: rgba(10, 10, 10, 0.72);
color: rgba(255, 255, 255, 0.88);
background: var(--theme-shell-bg);
color: var(--theme-text);
display: grid;
place-items: center;
cursor: pointer;
@@ -264,7 +283,7 @@ async function logout() {
.avatarButton__remove:hover {
background: rgba(190, 24, 24, 0.88);
color: #fff;
color: var(--theme-accent-text);
}
.identityMeta {
@@ -276,7 +295,7 @@ async function logout() {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.36);
color: var(--theme-text-soft);
}
.identityMeta__title {
@@ -286,7 +305,7 @@ async function logout() {
}
.identityMeta__desc {
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
line-height: 1.6;
}
@@ -307,16 +326,16 @@ async function logout() {
.field__label {
font-size: 13px;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-muted);
}
.field__input {
width: 100%;
padding: 14px 0;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid var(--theme-border-strong);
background: transparent;
color: rgba(255, 255, 255, 0.94);
color: var(--theme-text);
outline: none;
font-size: 18px;
letter-spacing: -0.02em;
@@ -327,12 +346,12 @@ async function logout() {
}
.field__input--readonly {
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
}
.field__hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
}
.roleBadge {
@@ -341,7 +360,7 @@ async function logout() {
border-radius: 999px;
border: 1px solid rgba(96, 165, 250, 0.28);
background: rgba(96, 165, 250, 0.1);
color: rgba(191, 219, 254, 0.92);
color: var(--theme-text);
font-size: 12px;
font-weight: 700;
}
@@ -363,14 +382,14 @@ async function logout() {
.primaryAction {
border: 1px solid rgba(76, 133, 245, 0.96);
background: rgba(76, 133, 245, 0.92);
color: #fff;
background: var(--theme-accent-bg);
color: var(--theme-accent-text);
}
.secondaryAction {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.86);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft);
color: var(--theme-text);
}
@media (max-width: 720px) {

View File

@@ -122,24 +122,24 @@ watch(
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.42);
color: var(--theme-text-soft);
}
.title {
margin: 4px 0 0;
font-size: 32px;
color: rgba(255, 255, 255, 0.96);
color: var(--theme-text-strong);
letter-spacing: -0.04em;
}
.desc {
margin-top: 6px;
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-muted);
}
.error {
margin: 0 0 8px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(239, 68, 68, 0.18);
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--theme-danger-border);
background: var(--theme-danger-bg);
}
.empty {
opacity: 0.76;
@@ -151,16 +151,16 @@ watch(
}
.boardCard {
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
border: 1px solid var(--theme-card-border);
background: var(--theme-card-bg);
overflow: hidden;
display: grid;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
transition: transform 0.16s ease, background 0.16s ease;
}
.boardCard:hover {
transform: translateY(-2px);
background: rgba(70, 70, 70, 0.96);
background: var(--theme-card-bg-hover);
}
.boardCard__body {
border: 0;
@@ -188,10 +188,10 @@ watch(
object-fit: cover;
}
.boardCard__thumbPlaceholder {
background: #555;
background: var(--theme-thumb-fallback-bg);
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.4);
color: var(--theme-text-faint);
font-size: 13px;
font-weight: 700;
}
@@ -238,7 +238,7 @@ watch(
height: 22px;
border-radius: 9999px;
object-fit: cover;
background: rgba(255, 255, 255, 0.08);
background: var(--theme-border);
flex: 0 0 auto;
}
.boardCard__avatar--fallback {
@@ -251,7 +251,7 @@ watch(
.favoriteStat {
flex: 0 0 auto;
font-size: 13px;
color: rgba(255, 255, 255, 0.64);
color: var(--theme-text-faint);
white-space: nowrap;
}

View File

@@ -1361,7 +1361,7 @@ onUnmounted(() => {
letter-spacing: -0.04em;
}
.editorMain__subtitle {
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-soft);
font-size: 13px;
line-height: 1.5;
}
@@ -1372,13 +1372,13 @@ onUnmounted(() => {
gap: 8px;
flex-wrap: wrap;
font-size: 12px;
color: rgba(255, 255, 255, 0.62);
color: var(--theme-text-soft);
}
.editorMain__sourceLink {
border: 0;
padding: 0;
background: transparent;
color: rgba(191, 219, 254, 0.94);
color: color-mix(in srgb, var(--theme-accent-bg) 78%, white);
font: inherit;
cursor: pointer;
}
@@ -1388,7 +1388,7 @@ onUnmounted(() => {
box-sizing: border-box;
background:
radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 38%),
rgba(11, 18, 32, 0.98);
var(--theme-shell-bg);
}
.previewOnly__sheet {
display: grid;
@@ -1451,13 +1451,13 @@ onUnmounted(() => {
text-align: center;
font-weight: 900;
border-radius: 14px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
background: var(--theme-surface-soft-2);
border: 1px solid var(--theme-border-strong);
}
.previewOnly__drop {
border-radius: 14px;
background: rgba(0, 0, 0, 0.18);
border: 1px solid rgba(255, 255, 255, 0.1);
background: var(--theme-pill-bg);
border: 1px solid var(--theme-border);
min-height: calc(var(--thumb-size, 80px) + 24px);
padding: 10px;
display: flex;
@@ -1498,8 +1498,8 @@ onUnmounted(() => {
gap: 12px;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
cursor: pointer;
user-select: none;
}
@@ -1513,8 +1513,8 @@ onUnmounted(() => {
width: 42px;
height: 24px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.12);
background: var(--theme-surface-soft-3);
border: 1px solid var(--theme-border-strong);
transition: background 180ms ease, border-color 180ms ease;
flex: 0 0 auto;
}
@@ -1525,13 +1525,13 @@ onUnmounted(() => {
width: 18px;
height: 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.94);
background: var(--theme-text-strong);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
transition: transform 180ms ease;
}
.toggleSwitch__label {
font-weight: 800;
color: rgba(255, 255, 255, 0.9);
color: var(--theme-text);
}
.toggleSwitch input:checked ~ .toggleSwitch__track {
background: rgba(96, 165, 250, 0.34);
@@ -1547,14 +1547,14 @@ onUnmounted(() => {
.btn {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft-2);
color: var(--theme-text);
cursor: pointer;
font-weight: 700;
}
.btn:hover {
background: rgba(255, 255, 255, 0.08);
background: var(--theme-surface-soft-3);
}
.btn--primary {
background: rgba(110, 231, 183, 0.18);
@@ -1600,8 +1600,8 @@ onUnmounted(() => {
}
.board {
width: min(100%, 960px);
border: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(180deg, rgba(55, 55, 55, 0.86), rgba(42, 42, 42, 0.82));
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;
padding: 20px;
align-self: start;
@@ -1614,15 +1614,15 @@ onUnmounted(() => {
display: grid;
place-items: center;
padding: 20px;
background: rgba(4, 8, 16, 0.68);
background: color-mix(in srgb, var(--theme-body-bg) 76%, transparent);
backdrop-filter: blur(4px);
}
.modalCard {
width: min(100%, 420px);
border-radius: 20px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: linear-gradient(180deg, rgba(17, 24, 39, 0.96), rgba(11, 18, 32, 0.96));
border: 1px solid var(--theme-border-strong);
background: linear-gradient(180deg, color-mix(in srgb, var(--theme-main-bg) 98%, transparent), color-mix(in srgb, var(--theme-shell-bg) 98%, transparent));
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.38);
display: grid;
gap: 10px;
@@ -1695,24 +1695,24 @@ onUnmounted(() => {
}
.templateRequestDraft__label {
font-size: 12px;
color: rgba(255, 255, 255, 0.64);
color: var(--theme-text-soft);
}
.templateRequestDraft__hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.46);
color: var(--theme-text-faint);
}
.templateRequestDraft__note {
font-size: 12px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
color: var(--theme-text-soft);
}
.templateRequestDraft__input {
width: 100%;
padding: 14px 0;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid var(--theme-border-strong);
background: transparent;
color: rgba(255, 255, 255, 0.94);
color: var(--theme-text-strong);
outline: none;
font-size: 18px;
line-height: 1.5;
@@ -1723,7 +1723,7 @@ onUnmounted(() => {
border-bottom-color: rgba(96, 165, 250, 0.9);
}
.templateRequestDraft__input::placeholder {
color: rgba(255, 255, 255, 0.34);
color: var(--theme-text-faint);
}
.templateRequestDraft__textarea {
min-height: 92px;
@@ -1738,8 +1738,8 @@ onUnmounted(() => {
flex-wrap: wrap;
padding: 10px 12px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.boardTools__left,
.boardTools__right {
@@ -1758,9 +1758,9 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.9);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft);
color: var(--theme-text);
cursor: pointer;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}
@@ -1811,9 +1811,9 @@ onUnmounted(() => {
min-width: 48px;
padding: 9px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft);
color: var(--theme-text);
cursor: pointer;
font-weight: 800;
}
@@ -1831,7 +1831,7 @@ onUnmounted(() => {
border-radius: 28px;
background:
radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 38%),
rgba(11, 18, 32, 0.98);
var(--theme-shell-bg);
}
.exportBoard__title {
font-size: 28px;
@@ -1869,8 +1869,8 @@ onUnmounted(() => {
.row__label {
position: relative;
border-radius: 16px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
background: var(--theme-surface-soft-3);
border: 1px solid var(--theme-border-strong);
display: flex;
align-items: center;
justify-content: center;
@@ -1898,9 +1898,9 @@ onUnmounted(() => {
.columnName {
width: 100%;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid var(--theme-border-strong);
background: transparent;
color: rgba(255, 255, 255, 0.88);
color: var(--theme-text);
padding: 4px 0;
text-align: center;
font-size: 12px;
@@ -1909,7 +1909,7 @@ onUnmounted(() => {
outline: none;
}
.columnName::placeholder {
color: rgba(255, 255, 255, 0.34);
color: var(--theme-text-faint);
}
.columnRemoveText {
position: absolute;
@@ -1924,15 +1924,15 @@ onUnmounted(() => {
border: 0;
border-radius: 999px;
background: transparent;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
font-size: 16px;
line-height: 1;
font-weight: 800;
cursor: pointer;
}
.columnRemoveText:hover {
color: rgba(255, 255, 255, 0.92);
background: rgba(255, 255, 255, 0.06);
color: var(--theme-text);
background: var(--theme-surface-soft-2);
}
.columnRemoveText:disabled {
opacity: 0.32;
@@ -1949,15 +1949,15 @@ onUnmounted(() => {
display: grid;
place-items: center;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--theme-border);
background: rgba(0, 0, 0, 0.16);
font-size: 12px;
}
.groupName {
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(0, 0, 0, 0.18);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border-strong);
background: var(--theme-pill-bg);
color: var(--theme-text);
border-radius: 10px;
padding: 8px 10px;
font-weight: 900;
@@ -1977,15 +1977,15 @@ onUnmounted(() => {
border: 0;
border-radius: 999px;
background: transparent;
color: rgba(255, 255, 255, 0.6);
color: var(--theme-text-soft);
cursor: pointer;
font-size: 16px;
line-height: 1;
font-weight: 800;
}
.rowRemoveText:hover {
color: rgba(255, 255, 255, 0.92);
background: rgba(255, 255, 255, 0.06);
color: var(--theme-text);
background: var(--theme-surface-soft-2);
}
.rowRemoveText:disabled {
opacity: 0.32;
@@ -1999,7 +1999,7 @@ onUnmounted(() => {
}
.row__drop {
border-radius: 16px;
background: rgba(0, 0, 0, 0.18);
background: var(--theme-pill-bg);
border: 1px solid rgba(255, 255, 255, 0.10);
min-height: calc(var(--thumb-size, 80px) + 24px);
padding: 10px;
@@ -2055,7 +2055,7 @@ onUnmounted(() => {
border-radius: 999px;
border: 1px solid rgba(239, 68, 68, 0.32);
background: rgba(11, 18, 32, 0.92);
color: rgba(255, 255, 255, 0.92);
color: var(--theme-text);
font-size: 16px;
line-height: 1;
font-weight: 900;
@@ -2070,14 +2070,14 @@ onUnmounted(() => {
width: var(--thumb-size, 80px);
height: var(--thumb-size, 80px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--theme-border-strong);
background: var(--theme-surface-soft-2);
object-fit: cover;
}
.sidebar {
min-width: 0;
border: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(180deg, rgba(52, 52, 52, 0.84), rgba(36, 36, 36, 0.8));
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;
padding: 14px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
@@ -2114,7 +2114,7 @@ onUnmounted(() => {
.editorSidebar__label {
font-size: 11px;
font-weight: 800;
color: rgba(255, 255, 255, 0.52);
color: var(--theme-text-faint);
text-transform: uppercase;
letter-spacing: 0.12em;
}
@@ -2122,9 +2122,9 @@ onUnmounted(() => {
.editorSidebar__textarea {
width: 100%;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
color: var(--theme-text);
padding: 11px 12px;
outline: none;
resize: vertical;
@@ -2135,7 +2135,7 @@ onUnmounted(() => {
.editorSidebar__hint {
font-size: 12px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
word-break: keep-all;
}
.editorSidebar__hint--warn {
@@ -2147,8 +2147,8 @@ onUnmounted(() => {
aspect-ratio: 16 / 9;
border-radius: 16px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #4c4c4c;
border: 1px solid var(--theme-border);
background: var(--theme-thumb-fallback-bg);
}
.editorSidebar__thumbFrame--active {
@@ -2165,7 +2165,7 @@ onUnmounted(() => {
height: 100%;
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.36);
color: var(--theme-text-faint);
font-size: 13px;
}
@@ -2184,7 +2184,7 @@ onUnmounted(() => {
}
.editorSidebar__fileName {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
color: var(--theme-text-soft);
word-break: break-word;
}
.editorSidebar__favorite {
@@ -2195,9 +2195,9 @@ onUnmounted(() => {
width: 100%;
padding: 11px 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.9);
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
color: var(--theme-text);
font-weight: 800;
cursor: pointer;
}
@@ -2223,7 +2223,7 @@ onUnmounted(() => {
border: 0;
padding: 0;
background: transparent;
color: rgba(255, 255, 255, 0.74);
color: var(--theme-text-muted);
font-size: 14px;
cursor: pointer;
}
@@ -2285,16 +2285,16 @@ onUnmounted(() => {
height: 44px;
border-radius: 10px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.14);
border: 1px solid var(--theme-border-strong);
}
.customItemEditor__input {
width: 100%;
min-width: 0;
padding: 9px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--theme-border-strong);
background: var(--theme-pill-bg);
color: var(--theme-text);
outline: none;
box-sizing: border-box;
}
@@ -2303,7 +2303,7 @@ onUnmounted(() => {
padding: 14px;
border-radius: 16px;
border: 1px dashed rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.04);
background: var(--theme-surface-soft);
}
.dropzone--active {
border-color: rgba(110, 231, 183, 0.6);
@@ -2334,7 +2334,7 @@ onUnmounted(() => {
padding: 10px 8px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.10);
background: rgba(0, 0, 0, 0.18);
background: var(--theme-pill-bg);
}
.poolItem--readonly {
opacity: 0.58;
@@ -2363,7 +2363,7 @@ onUnmounted(() => {
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.58);
color: var(--theme-text-soft);
}
.hidden {
display: none;