Compare commits

...

3 Commits

6 changed files with 185 additions and 162 deletions

View File

@@ -1,5 +1,16 @@
# 의사결정 이력
## 2026-04-02 v1.3.73
- 게임 선택이 여러 관리자 화면에 퍼지기 시작한 시점에서는 일부 화면만 셀렉트나 내부 리스트를 유지하기보다, 공용 검색 모달 하나로 통일하는 편이 장기적으로 더 일관되고 확장에 강하다고 정리했다.
- 검색 입력과 실행 버튼은 세로로 같은 문법으로 쌓기보다, 입력은 입력끼리 실행은 액션으로 읽히게 한 줄 배치로 적당히 구분해주는 편이 운영 화면에서 덜 답답하다고 판단했다.
## 2026-04-02 v1.3.72
- 라우트 복원용 watcher가 composable 반환값 초기화보다 먼저 돌 수 있는 구간에서는 직접 함수를 즉시 호출하기보다, 초기화 완료 뒤 실행되도록 한 템포 미루는 편이 안전하다고 정리했다.
## 2026-04-02 v1.3.71
- 관리자에서 게임 선택 지점이 늘어나는 구조라면 각 화면마다 셀렉트/긴 리스트를 따로 두기보다, 공용 검색 모달 하나로 통일하는 편이 이후 100개 이상 게임이 쌓여도 더 안정적이라고 정리했다.
- 아이템 모달은 참조 정보 정리 뒤에도 왼쪽 선택 요약 카드가 여전히 과하다고 판단해, 예전처럼 게임 선택 자체에 더 집중한 구조로 한 단계 더 되돌리는 편이 맞다고 판단했다.
## 2026-04-02 v1.3.70
- 관리자 티어표 목록은 페이지 단위 열람만으로는 운영 개입이 부족하므로, 게임별 필터와 카드 단위 관리 액션을 함께 붙여 실제 검수 도구로 쓰는 편이 맞다고 정리했다.
- 공개 여부는 문장 속 메타보다 배지로 따로 떼어 보여주는 편이 다른 관리자 카드들과 문법이 맞고, 공개/비공개 전환도 같은 관리 모달 안에서 바로 처리하는 쪽이 운영 흐름상 자연스럽다고 판단했다.

View File

@@ -1,6 +1,9 @@
# 할 일 및 이슈
## 단기 확인
- 공용 게임 선택 모달을 아이템 관리 모달에도 붙였으므로, 관리자 `게임 관리 / 전체 티어표 관리 / 아이템 관리` 세 흐름에서 같은 검색/선택 UX가 자연스럽게 느껴지는지 한 번 더 QA한다.
- 관리자 `/admin/games?gameId=...` 직접 진입과 새로고침에서 선택 게임이 정상 복원되고 콘솔 오류가 없는지 한 번 더 QA한다.
- 공용 `게임 선택` 검색 모달은 새로 붙였으므로, 게임 관리 선택/전체 티어표 관리 필터 양쪽에서 검색어 입력, 선택, 해제, 뒤로가기 흐름을 한 번 더 QA한다.
- 관리자 `전체 티어표 관리`의 게임 필터와 관리 모달은 새로 붙였으므로, 실제 운영 데이터에서 필터 전환 후 공개/비공개 집계, 제목 수정, 비공개 전환, 삭제 흐름이 자연스럽게 이어지는지 한 번 더 QA한다.
- 관리자 게임 관리 상단의 `전체 / 공개 / 비공개` 티어표 수치와 전체 티어표 관리 상단 집계가 실제 운영 데이터와 맞는지, 공개 전환 직후 숫자가 자연스럽게 갱신되는지 한 번 더 QA한다.
- 관리자 아이템 상세 모달은 왼쪽 선택 카드와 오른쪽 상세 본문으로 다시 정리했으므로, 데스크톱/모바일에서 긴 게임 목록과 긴 참조 목록이 각각 자연스럽게 스크롤되는지 한 번 더 QA한다.

View File

@@ -1,5 +1,19 @@
# 업데이트 로그
## 2026-04-02 v1.3.73
- 전체 티어표 관리 카드 썸네일은 `draggable="false"`로 바꿔, 미리보기 진입 시 브라우저 기본 이미지 드래그가 클릭을 방해하지 않도록 정리함.
- 관리자 사이드바의 검색 입력과 검색 버튼은 한 줄로 묶어, 입력/선택/실행 버튼이 모두 같은 크기의 세로 스택처럼 보이던 답답함을 조금 줄이고 역할 구분을 더 분명하게 함.
- 아이템 관리 상세 모달의 템플릿 추가 대상 선택도 내부 전용 게임 리스트 대신 공용 `게임 선택` 검색 모달을 쓰도록 바꿔, 향후 게임 수가 많아져도 같은 선택 문법으로 이어지게 정리함.
## 2026-04-02 v1.3.72
- 관리자 화면 초기화 중 `/admin/games?gameId=...` 경로를 즉시 처리하는 watcher가 `loadGame` 초기화보다 먼저 실행되어 브라우저 콘솔에 `Cannot access 'loadGame' before initialization` 오류가 나던 문제를 수정함.
- 게임 라우트 진입 시 실제 게임 로딩 호출은 컴포넌트 초기화가 끝난 뒤 microtask로 미뤄 실행하도록 바꿔, 첫 진입/새로고침에서도 게임 선택 복원 흐름이 안전하게 이어지게 정리함.
## 2026-04-02 v1.3.71
- 관리자 아이템 모달은 최근 추가했던 선택 요약 카드를 다시 걷어내고, 더 단순한 `게임 선택 패널 + 상세 작업 영역` 구조로 되돌려 이전 흐름에 가깝게 정리함.
- 관리자 `게임 관리``전체 티어표 관리`의 게임 선택은 긴 셀렉트/목록 대신 공용 `게임 선택` 검색 모달로 바꿔, 게임 수가 많아져도 이름·ID 검색으로 바로 찾아 선택할 수 있게 함.
- 전체 티어표 관리의 게임 필터 해제도 같은 모달 흐름에 맞춰 `모든 게임 보기`로 처리하고, 사이드바에는 현재 선택된 게임만 요약 카드로 보여줘 긴 리스트가 계속 쌓이지 않게 정리함.
## 2026-04-02 v1.3.70
- 관리자 `전체 티어표 관리`는 이제 게임별 필터를 지원해, 특정 게임에서 만들어진 티어표만 따로 골라 보며 공개/비공개 분포를 확인할 수 있게 함.
- 전체 티어표 카드는 공개 여부를 텍스트 대신 다른 관리자 화면과 같은 배지 형식으로 표시하고, 카드의 `관리` 액션에서 제목·설명 수정, 공개/비공개 전환, 삭제를 바로 처리할 수 있게 보강함.

View File

@@ -40,7 +40,7 @@ const props = defineProps({
<article v-for="request in props.templateRequests" :key="request.id" class="tierAdminCard templateRequestCard templateRequestCard--aligned">
<div class="templateRequestCard__side">
<button class="tierAdminCard__preview templateRequestCard__preview" type="button" @click="props.openTemplateRequestPreview(request)">
<img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" />
<img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" draggable="false" />
<div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
</button>
<div class="templateRequestCard__thumbMeta">
@@ -142,7 +142,7 @@ const props = defineProps({
<div v-else class="tierAdminList">
<article v-for="tierList in props.adminTierLists" :key="tierList.id" class="tierAdminCard">
<button class="tierAdminCard__preview" type="button" @click="props.openAdminTierList(tierList)">
<img v-if="props.tierListThumbUrl(tierList)" class="tierAdminCard__thumb" :src="props.tierListThumbUrl(tierList)" :alt="tierList.title" />
<img v-if="props.tierListThumbUrl(tierList)" class="tierAdminCard__thumb" :src="props.tierListThumbUrl(tierList)" :alt="tierList.title" draggable="false" />
<div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
</button>

View File

@@ -16,8 +16,6 @@ export function useAdminCustomItems({
customItemModalDraftLabel,
customItemModalLabelSaving,
customItemModalTargetGameId,
customItemModalGameQuery,
customItemModalGameSort,
games,
selectedGameId,
refreshCustomItems,
@@ -62,8 +60,6 @@ export function useAdminCustomItems({
modalTargetCustomItem.value = item || null
customItemModalDraftLabel.value = item?.label || ''
customItemModalTargetGameId.value = ''
customItemModalGameQuery.value = ''
customItemModalGameSort.value = 'recent'
customItemModalOpen.value = true
pushCustomItemModalHistoryState()
}
@@ -75,8 +71,6 @@ export function useAdminCustomItems({
customItemModalDraftLabel.value = ''
customItemModalLabelSaving.value = false
customItemModalTargetGameId.value = ''
customItemModalGameQuery.value = ''
customItemModalGameSort.value = 'recent'
if (fromPopState) {
customItemModalHistoryActive.value = false

View File

@@ -34,8 +34,10 @@ const games = ref([])
const selectedGameId = ref('')
const selectedGame = ref(null)
const featuredGameIds = ref([])
const gameAdminQuery = ref('')
const gameAdminSort = ref('recent')
const gamePickerModalOpen = ref(false)
const gamePickerMode = ref('game-admin')
const gamePickerQuery = ref('')
const gamePickerSort = ref('recent')
const customItems = ref([])
const customItemQuery = ref('')
@@ -44,8 +46,6 @@ const customItemLimit = ref(50)
const customItemTotal = ref(0)
const customItemFilter = ref('all')
const customItemModalTargetGameId = ref('')
const customItemModalGameQuery = ref('')
const customItemModalGameSort = ref('recent')
const adminTierLists = ref([])
const adminTierListQuery = ref('')
@@ -197,8 +197,8 @@ const featuredGames = computed(() =>
.filter(Boolean)
)
const availableGamesForFeatured = computed(() => games.value.filter((game) => !featuredGameIds.value.includes(game.id)))
const filteredAdminGames = computed(() => {
const query = gameAdminQuery.value.trim().toLowerCase()
const filteredGamePickerGames = computed(() => {
const query = gamePickerQuery.value.trim().toLowerCase()
const list = games.value.filter((game) => {
if (!query) return true
const haystack = `${game.name || ''} ${game.id || ''}`.toLowerCase()
@@ -206,10 +206,11 @@ const filteredAdminGames = computed(() => {
})
return list.slice().sort((a, b) => {
if (gameAdminSort.value === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
if (gamePickerSort.value === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
return Number(b.createdAt || 0) - Number(a.createdAt || 0)
})
})
const customItemTargetGame = computed(() => games.value.find((game) => game.id === customItemModalTargetGameId.value) || null)
const importModalItemCount = computed(() => importModalItems.value.length)
const activeTabTitle = computed(() => {
if (activeTab.value === 'featured') return '목록 관리'
@@ -289,6 +290,7 @@ const adminOverviewStats = computed(() => {
const isAnyModalOpen = computed(
() =>
gameCreateModalOpen.value ||
gamePickerModalOpen.value ||
userEditModalOpen.value ||
userPasswordModalOpen.value ||
userDeleteModalOpen.value ||
@@ -424,7 +426,9 @@ watch(
const nextGameId = typeof route.query.gameId === 'string' ? route.query.gameId : ''
if (nextGameId && nextGameId !== selectedGameId.value) {
selectedGameId.value = nextGameId
loadGame()
queueMicrotask(() => {
if (selectedGameId.value === nextGameId) void loadGame()
})
}
return
}
@@ -485,7 +489,6 @@ watch(
customItemQuery.value = ''
customItemFilter.value = 'all'
customItemPage.value = 1
customItemModalGameQuery.value = ''
await refreshCustomItems()
return
}
@@ -617,21 +620,6 @@ 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(() => {
@@ -1035,8 +1023,6 @@ const {
customItemModalDraftLabel,
customItemModalLabelSaving,
customItemModalTargetGameId,
customItemModalGameQuery,
customItemModalGameSort,
games,
selectedGameId,
refreshCustomItems,
@@ -1303,6 +1289,35 @@ function setAdminTierListGameId(gameId) {
refreshAdminTierLists()
}
function openGamePickerModal(mode = 'game-admin') {
gamePickerMode.value = mode
gamePickerQuery.value = ''
gamePickerSort.value = 'recent'
gamePickerModalOpen.value = true
}
function closeGamePickerModal() {
gamePickerModalOpen.value = false
gamePickerQuery.value = ''
}
async function chooseGameFromPicker(gameId) {
if (!gameId) return
if (gamePickerMode.value === 'tierlists-filter') {
setAdminTierListGameId(gameId)
closeGamePickerModal()
return
}
if (gamePickerMode.value === 'custom-item-target') {
customItemModalTargetGameId.value = gameId
closeGamePickerModal()
return
}
await selectAdminGame(gameId)
closeGamePickerModal()
}
function changeAdminTierListLimit(limit) {
adminTierListLimit.value = limit
adminTierListPage.value = 1
@@ -1931,45 +1946,19 @@ function userAvatarFallback(user) {
<div class="modalCard modalCard--customItem" role="dialog" aria-modal="true">
<div v-if="modalTargetCustomItem" class="customItemModal">
<aside class="customItemModal__pickerPanel">
<div class="customItemModal__selected">
<img class="customItemModal__selectedImage" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
<div class="customItemModal__selectedMeta">
<div class="customItemModal__selectedTitle">{{ modalTargetCustomItem.label }}</div>
<div class="customItemModal__selectedChips">
<span class="pill">{{ modalTargetCustomItem.sourceLabel }}</span>
<span class="pill" v-if="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</span>
</div>
</div>
</div>
<div class="customItemModal__pickerHead">
<div class="customItemModal__pickerEyebrow">GAME PICKER</div>
<div class="customItemModal__pickerTitle">템플릿으로 추가할 게임</div>
</div>
<div class="adminSelectionCard">
<div class="adminSelectionCard__label">선택한 게임</div>
<div class="adminSelectionCard__title">{{ customItemTargetGame?.name || '아직 선택하지 않음' }}</div>
<div class="adminSelectionCard__meta">{{ customItemTargetGame?.id || '게임을 골라 주세요.' }}</div>
</div>
<div class="customItemModal__pickerActions">
<button class="btn btn--ghost" type="button" @click="openGamePickerModal('custom-item-target')">게임 선택</button>
<button class="btn btn--ghost btn--small customItemModal__createGameButton" type="button" @click="openGameCreateModal"> 템플릿 만들기</button>
</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">
<button class="customItemModal__close" type="button" @click="closeCustomItemModal">닫기</button>
@@ -2015,6 +2004,49 @@ function userAvatarFallback(user) {
</div>
</div>
<div v-if="gamePickerModalOpen" class="modalOverlay" @click.self="closeGamePickerModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__titleRow">
<div>
<div class="modalCard__title">게임 선택</div>
<div class="modalCard__desc">
{{ gamePickerMode === 'tierlists-filter' ? '특정 게임의 티어표만 보려면 게임을 선택하세요.' : '관리할 게임을 검색해서 바로 열 수 있어요.' }}
</div>
</div>
<button class="btn btn--ghost btn--small" @click="closeGamePickerModal">닫기</button>
</div>
<div class="modalCard__form">
<input v-model="gamePickerQuery" class="input" placeholder="게임 이름 또는 ID 검색" />
<select v-model="gamePickerSort" class="select">
<option value="recent">최신순</option>
<option value="oldest">오래된순</option>
</select>
<button
v-if="gamePickerMode === 'tierlists-filter' && adminTierListGameId"
class="btn btn--ghost"
type="button"
@click="setAdminTierListGameId(''); closeGamePickerModal()"
>
모든 게임 보기
</button>
</div>
<div class="gamePickerModalList">
<button
v-for="game in filteredGamePickerGames"
:key="game.id"
class="adminGamePicker__item"
:class="{ 'adminGamePicker__item--active': gamePickerMode === 'tierlists-filter' ? adminTierListGameId === game.id : selectedGameId === game.id }"
type="button"
@click="chooseGameFromPicker(game.id)"
>
<span class="adminGamePicker__name">{{ game.name }}</span>
<span class="adminGamePicker__meta">{{ game.id }}</span>
</button>
<div v-if="!filteredGamePickerGames.length" class="hint hint--tight">검색 결과가 없어요.</div>
</div>
</div>
</div>
<div v-if="customItemDeleteModalOpen" class="modalOverlay" @click.self="closeCustomItemDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">아이템 삭제</div>
@@ -2169,24 +2201,11 @@ function userAvatarFallback(user) {
<div class="adminSidebar__label">Game</div>
<div class="adminSidebar__group">
<button class="btn btn--primary" @click="openGameCreateModal"> 게임 생성</button>
<input v-model="gameAdminQuery" class="input" placeholder="게임 이름 또는 ID 검색" />
<select v-model="gameAdminSort" class="select">
<option value="recent">최신순</option>
<option value="oldest">오래된순</option>
</select>
<div class="adminGamePicker">
<button
v-for="game in filteredAdminGames"
:key="game.id"
class="adminGamePicker__item"
:class="{ 'adminGamePicker__item--active': selectedGameId === game.id }"
type="button"
@click="selectAdminGame(game.id)"
>
<span class="adminGamePicker__name">{{ game.name }}</span>
<span class="adminGamePicker__meta">{{ game.id }}</span>
</button>
<div v-if="!filteredAdminGames.length" class="hint hint--tight">검색 결과가 없어요.</div>
<button class="btn btn--ghost" @click="openGamePickerModal('game-admin')">게임 선택</button>
<div v-if="selectedGame?.game" class="adminSelectionCard">
<div class="adminSelectionCard__label">선택한 게임</div>
<div class="adminSelectionCard__title">{{ selectedGame.game.name }}</div>
<div class="adminSelectionCard__meta">{{ selectedGame.game.id }}</div>
</div>
<div v-if="selectedGameId && !hasSelectedGame && !isGameLoading" class="hint hint--tight">선택된 게임 ID: {{ selectedGameId }}</div>
</div>
@@ -2195,8 +2214,10 @@ function userAvatarFallback(user) {
<section v-else-if="activeTab === 'items'" class="adminSidebar__panel">
<div class="adminSidebar__label">Filters</div>
<div class="adminSidebar__group">
<input v-model="customItemQuery" class="input" placeholder="파일명, 라벨, 업로더 검색" @keydown.enter.prevent="submitCustomItemSearch" />
<button class="btn btn--ghost" @click="submitCustomItemSearch">검색</button>
<div class="adminSidebar__inlineRow">
<input v-model="customItemQuery" class="input" placeholder="파일명, 라벨, 업로더 검색" @keydown.enter.prevent="submitCustomItemSearch" />
<button class="btn btn--ghost" @click="submitCustomItemSearch">검색</button>
</div>
</div>
<div class="adminSidebar__group">
<select :value="customItemLimit" class="select" @change="changeCustomItemLimit(Number($event.target.value))">
@@ -2241,17 +2262,22 @@ function userAvatarFallback(user) {
<template v-if="tierlistsMode === 'requests'"></template>
<template v-else>
<div class="adminSidebar__group">
<input
v-model="adminTierListQuery"
class="input"
placeholder="제목, 작성자, 게임 이름 검색"
@keydown.enter.prevent="submitAdminTierListSearch"
/>
<button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
<select :value="adminTierListGameId" class="select" @change="setAdminTierListGameId($event.target.value)">
<option value="">모든 게임</option>
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
</select>
<div class="adminSidebar__inlineRow">
<input
v-model="adminTierListQuery"
class="input"
placeholder="제목, 작성자, 게임 이름 검색"
@keydown.enter.prevent="submitAdminTierListSearch"
/>
<button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
</div>
<button class="btn btn--ghost" @click="openGamePickerModal('tierlists-filter')">게임 선택</button>
<div v-if="adminTierListGameId" class="adminSelectionCard">
<div class="adminSelectionCard__label">필터된 게임</div>
<div class="adminSelectionCard__title">{{ games.find((game) => game.id === adminTierListGameId)?.name || adminTierListGameId }}</div>
<div class="adminSelectionCard__meta">{{ adminTierListGameId }}</div>
<button class="btn btn--ghost btn--small" @click="setAdminTierListGameId('')">필터 해제</button>
</div>
<select :value="adminTierListLimit" class="select" @change="changeAdminTierListLimit(Number($event.target.value))">
<option :value="50">50개씩 보기</option>
<option :value="200">200개씩 보기</option>
@@ -2486,6 +2512,15 @@ function userAvatarFallback(user) {
display: grid;
gap: 10px;
}
.adminUiScope .adminSidebar__inlineRow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
}
.adminUiScope .adminSidebar__inlineRow .btn {
white-space: nowrap;
}
.adminUiScope .adminSidebar__group--monthPicker {
align-items: start;
}
@@ -2548,6 +2583,34 @@ function userAvatarFallback(user) {
overflow: hidden;
text-overflow: ellipsis;
}
.adminUiScope .gamePickerModalList {
margin-top: 14px;
display: grid;
gap: 8px;
max-height: min(56dvh, 520px);
overflow: auto;
}
.adminUiScope .adminSelectionCard {
display: grid;
gap: 6px;
padding: 12px 13px;
border-radius: 14px;
border: 1px solid var(--theme-border);
background: var(--theme-pill-bg);
}
.adminUiScope .adminSelectionCard__label {
font-size: 11px;
color: var(--theme-text-faint);
}
.adminUiScope .adminSelectionCard__title {
font-size: 14px;
font-weight: 800;
}
.adminUiScope .adminSelectionCard__meta {
font-size: 11px;
color: var(--theme-text-soft);
word-break: break-word;
}
.adminUiScope .sidebarStat {
display: grid;
gap: 4px;
@@ -3373,35 +3436,6 @@ function userAvatarFallback(user) {
display: grid;
gap: 10px;
}
.adminUiScope .customItemModal__selected {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 20px;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.adminUiScope .customItemModal__selectedImage {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 18px;
object-fit: cover;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.adminUiScope .customItemModal__selectedMeta {
display: grid;
gap: 10px;
}
.adminUiScope .customItemModal__selectedTitle {
font-size: 18px;
font-weight: 900;
}
.adminUiScope .customItemModal__selectedChips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.adminUiScope .customItemModal__pickerEyebrow {
font-size: 11px;
letter-spacing: 0.12em;
@@ -3412,46 +3446,13 @@ function userAvatarFallback(user) {
font-size: 18px;
font-weight: 900;
}
.adminUiScope .customItemModal__pickerControls {
.adminUiScope .customItemModal__pickerActions {
display: grid;
gap: 10px;
}
.adminUiScope .customItemModal__gameList {
display: grid;
gap: 8px;
max-height: 440px;
overflow: auto;
}
.adminUiScope .customItemModal__createGameButton {
justify-self: start;
}
.adminUiScope .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;
}
.adminUiScope .customItemModal__gameItem--active {
border-color: rgba(96, 165, 250, 0.42);
background: rgba(96, 165, 250, 0.12);
}
.adminUiScope .customItemModal__gameItem--linked {
border-style: dashed;
}
.adminUiScope .customItemModal__gameName {
font-size: 13px;
font-weight: 800;
}
.adminUiScope .customItemModal__gameMeta,
.adminUiScope .customItemModal__gameState {
font-size: 11px;
color: var(--theme-text-soft);
}
.adminUiScope .customItemModal__body {
min-width: 0;
min-height: 0;