2204 lines
60 KiB
Vue
2204 lines
60 KiB
Vue
<script setup>
|
||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useAuthStore } from './stores/auth'
|
||
import { editorNewPath, favoritesPath, followingFeedPath, homePath, loginPath, mePath } from './lib/paths'
|
||
import { toApiUrl } from './lib/runtime'
|
||
import { useToast } from './composables/useToast'
|
||
import iconDockToLeft from './assets/icons/dock_to_left.svg'
|
||
import iconDockToRight from './assets/icons/dock_to_right.svg'
|
||
import iconGridView from './assets/icons/grid_view.svg'
|
||
import iconFavorite from './assets/icons/favorite.svg'
|
||
import iconLists from './assets/icons/lists.svg'
|
||
import iconAddNotes from './assets/icons/add_notes.svg'
|
||
import iconDashboardCustomize from './assets/icons/dashboard_customize.svg'
|
||
import iconSearch from './assets/icons/search.svg'
|
||
import iconSettings from './assets/icons/settings.svg'
|
||
import iconKidStar from './assets/icons/kid_star.svg'
|
||
import iconMenuBook from './assets/icons/menu_book.svg'
|
||
import RightRailAd from './components/RightRailAd.vue'
|
||
import SvgIcon from './components/SvgIcon.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const { toasts, dismissToast } = useToast()
|
||
const RIGHT_RAIL_COPYRIGHT_URL = 'https://x.com/zennbox'
|
||
const currentTopicId = computed(() => route.params.topicId || '')
|
||
|
||
const leftRailCollapsed = ref(false)
|
||
const mobileLeftNavOpen = ref(false)
|
||
const rightRailOpen = ref(true)
|
||
const searchQuery = ref('')
|
||
const leftRailSearchPlaceholder = '주제 템플릿 검색'
|
||
const isCollapsedSearchOpen = ref(false)
|
||
const isGuideModalOpen = ref(false)
|
||
const themeMode = ref('dark')
|
||
const guideStepIndex = ref(0)
|
||
const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
|
||
const backendState = ref('online')
|
||
const backendMessage = ref('')
|
||
provide('rightRailOpen', rightRailOpen)
|
||
provide('localRightRailTarget', '#local-right-rail-root')
|
||
|
||
const authReady = computed(() => auth.hydrated)
|
||
const isAdmin = computed(() => authReady.value && !!auth.user?.isAdmin)
|
||
const isPreviewMode = computed(() => route.query.preview === '1')
|
||
const isAdminRoute = computed(() => String(route.name || '').startsWith('admin'))
|
||
const usesLocalRightRail = computed(
|
||
() => ['editEditor', 'newEditor'].includes(String(route.name || '')) || isAdminRoute.value
|
||
)
|
||
const isRightRailOverlay = computed(() => viewportWidth.value <= 1200)
|
||
const isMobileLayout = computed(() => viewportWidth.value <= 860)
|
||
const avatarUrl = computed(() => (auth.user?.avatarSrc ? toApiUrl(auth.user.avatarSrc) : ''))
|
||
const accountName = computed(() => {
|
||
const nickname = (auth.user?.nickname || '').trim()
|
||
if (nickname) return nickname
|
||
const email = (auth.user?.email || '').trim()
|
||
if (email) return email.split('@')[0] || email
|
||
return 'Guest'
|
||
})
|
||
const accountEmail = computed(() => {
|
||
if (!authReady.value) return '계정 상태를 확인하고 있어요.'
|
||
return (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.'
|
||
})
|
||
const shellStyle = computed(() => ({
|
||
'--left-rail-width': leftRailCollapsed.value ? '76px' : '248px',
|
||
'--right-rail-width': !isRightRailOverlay.value && rightRailOpen.value ? '325px' : '0px',
|
||
}))
|
||
const leftNavItems = computed(() => {
|
||
const items = [
|
||
{ key: 'home', label: '주제 선택', path: '/', iconSrc: iconGridView },
|
||
{ key: 'me', label: '나의 티어표', path: '/me', iconSrc: iconLists, requiresAuth: true },
|
||
{ key: 'favorites', label: '즐겨찾기', path: '/favorites', iconSrc: iconFavorite, requiresAuth: true },
|
||
{ key: 'followingFeed', label: '팔로우 피드', path: '/following', iconSrc: iconKidStar, requiresAuth: true },
|
||
{ key: 'profile', label: '설정', path: '/profile', iconSrc: iconSettings, requiresAuth: true },
|
||
]
|
||
return items.filter((item) => !item.requiresAuth || (authReady.value && auth.user))
|
||
})
|
||
const activeLeftNavIndex = computed(() => leftNavItems.value.findIndex((item) => isRouteActive(item.path)))
|
||
const showRightRailAction = computed(() => false)
|
||
const showSettingsGuideButton = computed(() => route.name === 'profile')
|
||
const guideSteps = [
|
||
{
|
||
id: 'select-topic',
|
||
title: '주제 또는 양식 선택',
|
||
summary: '주제 템플릿을 고르거나 커스텀 티어표 만들기로 바로 시작합니다.',
|
||
description:
|
||
'홈 화면에서는 주제 템플릿을 선택하거나 커스텀 티어표 만들기로 바로 새 보드를 열 수 있어요. 원하는 주제를 먼저 고르면 해당 주제의 공개 티어표도 같이 살펴볼 수 있어서, 완전히 처음 만드는지 기존 흐름을 참고할지 결정하기 쉽습니다.',
|
||
},
|
||
{
|
||
id: 'arrange-board',
|
||
title: '행과 열 구성',
|
||
summary: '랭크 행과 가로 열을 정리해 보드 구조를 먼저 잡습니다.',
|
||
description:
|
||
'기본 랭크를 그대로 써도 되고, 행 이름을 바꾸거나 행과 열을 추가해 공격·방어·지원처럼 더 세밀한 구조로 나눌 수도 있어요. 먼저 판을 정리한 뒤 배치를 시작하면 뒤에서 크게 손댈 일이 줄어듭니다.',
|
||
},
|
||
{
|
||
id: 'drop-items',
|
||
title: '아이템 배치와 커스텀 추가',
|
||
summary: '프리셋 아이템과 직접 올린 이미지를 드래그로 배치합니다.',
|
||
description:
|
||
'오른쪽 아이템 영역의 이미지를 원하는 칸으로 끌어다 놓으면 바로 배치됩니다. 주제 템플릿에 없는 이미지는 커스텀 이미지로 추가해 같이 쓸 수 있고, 이름 표시 옵션을 켜면 결과 이미지를 더 설명적으로 정리할 수 있어요.',
|
||
},
|
||
{
|
||
id: 'save-share',
|
||
title: '저장과 이미지 다운로드',
|
||
summary: '완성한 티어표를 내 목록에 저장하거나 PNG 이미지로 내려받습니다.',
|
||
description:
|
||
'보드 작업이 끝나면 저장해서 내 티어표 목록에 남길 수 있고, 이미지 다운로드로 한 장의 결과물로 바로 공유할 수도 있어요. 공개 여부도 함께 정할 수 있어서 개인 메모용과 공유용 흐름을 나눠 쓰기 좋습니다.',
|
||
},
|
||
{
|
||
id: 'copy-existing',
|
||
title: '다른 사람 티어표 복사',
|
||
summary: '공개된 티어표를 그대로 가져와 내 이름의 새 작업본으로 이어서 수정합니다.',
|
||
description:
|
||
'누군가 만든 티어표가 거의 마음에 드는데 일부만 바꾸고 싶다면, 복사 기능으로 현재 배치 상태를 그대로 가져와 새 티어표로 시작할 수 있어요. 복사본에는 원본을 참고했다는 정보가 함께 남아서 출처도 자연스럽게 구분됩니다.',
|
||
},
|
||
{
|
||
id: 'request-template-update',
|
||
title: '템플릿 업그레이드 요청',
|
||
summary: '현재 템플릿에 공통 아이템을 추가해 달라고 관리자에게 요청합니다.',
|
||
description:
|
||
'직접 추가한 아이템 중 여러 사람이 함께 써도 좋을 것 같은 항목이 있다면 템플릿 업데이트 요청을 보낼 수 있어요. 요청 모달에서는 현재 티어표 제목과 설명을 기본값으로 가져오고, 필요하면 요청 제목과 설명을 더 다듬어 공통 템플릿에 왜 필요한지 설명할 수 있습니다.',
|
||
},
|
||
{
|
||
id: 'request-new-template',
|
||
title: '새 템플릿 추가 요청',
|
||
summary: '아직 없는 주제나 새로운 양식을 관리자에게 제안합니다.',
|
||
description:
|
||
'원하는 주제 템플릿이 아직 없다면 새 템플릿 추가 요청으로 관리자에게 직접 제안할 수 있어요. 이때는 제목과 설명에 어떤 주제인지, 어떤 캐릭터나 항목이 기본으로 필요할지 적어두면 검토 속도가 훨씬 빨라집니다.',
|
||
},
|
||
{
|
||
id: 'manage-library',
|
||
title: '즐겨찾기와 내 티어표 관리',
|
||
summary: '마음에 드는 템플릿과 저장한 결과물을 나중에 다시 쉽게 찾습니다.',
|
||
description:
|
||
'주제 템플릿은 즐겨찾기로 상단에 고정해둘 수 있고, 저장한 보드는 내 티어표에서 다시 열어 이어서 수정할 수 있어요. 자주 보는 템플릿, 공개 티어표, 내가 만든 결과물을 각각 다른 화면에서 정리해두면 이후 작업이 훨씬 빨라집니다.',
|
||
},
|
||
]
|
||
const currentGuideStep = computed(() => guideSteps[guideStepIndex.value] || guideSteps[0])
|
||
const isGuidePrevDisabled = computed(() => guideStepIndex.value <= 0)
|
||
const isGuideNextDisabled = computed(() => guideStepIndex.value >= guideSteps.length - 1)
|
||
const isLightTheme = computed(() => themeMode.value === 'light')
|
||
const themeToggleLabel = computed(() => (isLightTheme.value ? '다크 모드' : '라이트 모드'))
|
||
const showSettingsThemePanel = computed(() => route.name === 'profile')
|
||
const showTopicViewToggle = computed(() => route.name === 'topicHub')
|
||
const topicViewMode = computed(() => (route.query.view === 'list' ? 'list' : 'grid'))
|
||
const showBackendFallback = computed(() => !isPreviewMode.value && ['maintenance', 'offline'].includes(backendState.value))
|
||
const leftBottomPrimaryAction = computed(() => {
|
||
if (!authReady.value) return null
|
||
if (route.name === 'home' && auth.user) {
|
||
return { label: '커스텀 티어표 만들기', to: editorNewPath('freeform'), iconSrc: iconDashboardCustomize }
|
||
}
|
||
if (route.name === 'topicHub') {
|
||
const target = editorNewPath(currentTopicId.value)
|
||
return { label: '새 티어표 만들기', to: auth.user ? target : loginPath(target), iconSrc: iconAddNotes }
|
||
}
|
||
return null
|
||
})
|
||
|
||
const routeMeta = computed(() => {
|
||
if (route.name === 'home') {
|
||
return {
|
||
title: '주제 선택',
|
||
subtitle: '주제 템플릿 선택과 커스텀 보드 시작',
|
||
contextTitle: '빠른 시작',
|
||
contextText: auth.user ? '커스텀 티어표를 만들거나 원하는 주제를 바로 선택할 수 있어요.' : '로그인하면 커스텀 티어표 생성과 개인 목록 관리가 열립니다.',
|
||
actionLabel: auth.user ? '커스텀 티어표 만들기' : '로그인하러 가기',
|
||
action: () => {
|
||
router.push(auth.user ? editorNewPath('freeform') : loginPath())
|
||
},
|
||
}
|
||
}
|
||
if (route.name === 'topicHub') {
|
||
return {
|
||
title: '주제 티어표',
|
||
subtitle: '주제별 공개 티어표 탐색',
|
||
contextTitle: '작성 작업',
|
||
contextText: auth.user ? '이 주제의 새 티어표를 만들거나 기존 공개 티어표를 확인할 수 있어요.' : '로그인 후 새 티어표를 만들 수 있어요.',
|
||
actionLabel: auth.user ? '새 티어표 만들기' : '로그인하러 가기',
|
||
action: () => {
|
||
const target = editorNewPath(currentTopicId.value)
|
||
router.push(auth.user ? target : loginPath(target))
|
||
},
|
||
}
|
||
}
|
||
if (route.name === 'editEditor' || route.name === 'newEditor') {
|
||
return {
|
||
title: '티어표 만들기',
|
||
subtitle: '티어표 편집 및 공유',
|
||
contextTitle: '편집 패널',
|
||
contextText: '현재 편집 옵션은 중앙 화면 안에 유지되어 있습니다. 다음 단계에서 우측 패널로 정리해갈게요.',
|
||
actionLabel: '주제 목록으로',
|
||
action: () => router.push(homePath()),
|
||
}
|
||
}
|
||
if (isAdminRoute.value) {
|
||
return {
|
||
title: '관리자 작업실',
|
||
subtitle: '템플릿·아이템·회원 관리',
|
||
contextTitle: '운영 노트',
|
||
contextText: '관리자 화면은 기능이 많아 우선 공통 셸 톤을 맞췄고, 세부 패널은 다음 단계에서 시안 방식으로 더 세밀하게 나눌 예정입니다.',
|
||
actionLabel: '주제 목록으로',
|
||
action: () => router.push(homePath()),
|
||
}
|
||
}
|
||
if (route.name === 'me') {
|
||
return {
|
||
title: '나의 티어표',
|
||
subtitle: '저장한 티어표 모아보기',
|
||
contextTitle: '작성 이력',
|
||
contextText: '최근 저장한 티어표를 열람하고 정리할 수 있어요.',
|
||
actionLabel: '즐겨찾기 보기',
|
||
action: () => router.push(favoritesPath()),
|
||
}
|
||
}
|
||
if (route.name === 'favorites') {
|
||
return {
|
||
title: '즐겨찾기',
|
||
subtitle: '마음에 드는 티어표 모음',
|
||
contextTitle: '정리 도구',
|
||
contextText: '정렬과 검색으로 원하는 티어표를 빠르게 다시 찾을 수 있어요.',
|
||
actionLabel: '나의 티어표 보기',
|
||
action: () => router.push(mePath()),
|
||
}
|
||
}
|
||
if (route.name === 'followingFeed') {
|
||
return {
|
||
title: '팔로우 피드',
|
||
subtitle: '팔로우한 작성자의 새 티어표',
|
||
contextTitle: '구독 목록',
|
||
contextText: '작성자 프로필에서 팔로우한 사람의 공개 티어표를 한곳에서 볼 수 있어요.',
|
||
actionLabel: '즐겨찾기 보기',
|
||
action: () => router.push(favoritesPath()),
|
||
}
|
||
}
|
||
if (route.name === 'userProfile') {
|
||
return {
|
||
title: '작성자 프로필',
|
||
subtitle: '공개 티어표와 팔로우',
|
||
contextTitle: '작성자 탐색',
|
||
contextText: auth.user ? '마음에 드는 작성자를 팔로우하고 새 공개 티어표를 피드에서 이어서 볼 수 있어요.' : '로그인하면 작성자를 팔로우할 수 있어요.',
|
||
actionLabel: auth.user ? '팔로우 피드 보기' : '로그인하러 가기',
|
||
action: () => router.push(auth.user ? followingFeedPath() : loginPath(route.fullPath)),
|
||
}
|
||
}
|
||
if (route.name === 'profile') {
|
||
return {
|
||
title: '설정',
|
||
subtitle: '프로필 및 계정 설정',
|
||
contextTitle: '계정 관리',
|
||
contextText: '아바타와 닉네임을 관리하고 표시 정보를 확인할 수 있어요.',
|
||
actionLabel: '나의 티어표 보기',
|
||
action: () => router.push(mePath()),
|
||
}
|
||
}
|
||
if (route.name === 'search') {
|
||
return {
|
||
title: '검색',
|
||
subtitle: '전체 공개 티어표 검색 결과',
|
||
contextTitle: '검색',
|
||
contextText: '제목, 작성자 기준으로 공개 티어표를 전체 검색할 수 있어요.',
|
||
actionLabel: '홈으로',
|
||
action: () => router.push(homePath()),
|
||
}
|
||
}
|
||
return {
|
||
title: 'Tier Maker',
|
||
subtitle: '주제 템플릿으로 만드는 티어표',
|
||
contextTitle: '작업 공간',
|
||
contextText: '현재 화면에 맞는 도구와 안내를 여기에 배치할 수 있습니다.',
|
||
actionLabel: '홈으로',
|
||
action: () => router.push(homePath()),
|
||
}
|
||
})
|
||
|
||
function syncViewportWidth() {
|
||
if (typeof window === 'undefined') return
|
||
viewportWidth.value = window.innerWidth
|
||
}
|
||
|
||
function handleBackendStatus(event) {
|
||
const state = event?.detail?.state
|
||
if (!state) return
|
||
backendState.value = state
|
||
backendMessage.value = typeof event?.detail?.message === 'string' ? event.detail.message : ''
|
||
}
|
||
|
||
function applyTheme(mode) {
|
||
themeMode.value = mode === 'light' ? 'light' : 'dark'
|
||
if (typeof document !== 'undefined') document.documentElement.dataset.theme = themeMode.value
|
||
if (typeof window !== 'undefined') window.localStorage.setItem('tier-maker:theme', themeMode.value)
|
||
}
|
||
|
||
function toggleTheme() {
|
||
applyTheme(isLightTheme.value ? 'dark' : 'light')
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if (typeof window !== 'undefined') {
|
||
const savedTheme = window.localStorage.getItem('tier-maker:theme')
|
||
if (savedTheme === 'light' || savedTheme === 'dark') applyTheme(savedTheme)
|
||
else applyTheme('dark')
|
||
}
|
||
await auth.refresh()
|
||
if (typeof window !== 'undefined') {
|
||
syncViewportWidth()
|
||
window.addEventListener('tier-maker:backend-status', handleBackendStatus)
|
||
window.addEventListener('resize', syncViewportWidth)
|
||
window.addEventListener('keydown', handleGlobalKeydown)
|
||
const leftSaved = window.localStorage.getItem('tier-maker:left-rail-collapsed')
|
||
if (leftSaved === '1') leftRailCollapsed.value = true
|
||
const saved = window.localStorage.getItem('tier-maker:right-rail-open')
|
||
if (saved === '0') rightRailOpen.value = false
|
||
}
|
||
if (isMobileLayout.value) {
|
||
mobileLeftNavOpen.value = false
|
||
rightRailOpen.value = false
|
||
} else {
|
||
rightRailOpen.value = true
|
||
}
|
||
searchQuery.value = typeof route.query.q === 'string' ? route.query.q : ''
|
||
})
|
||
|
||
function handleGlobalKeydown(event) {
|
||
if (event.key === 'Escape' && isGuideModalOpen.value) {
|
||
closeGuideModal()
|
||
return
|
||
}
|
||
if (event.key === 'Escape' && isCollapsedSearchOpen.value) {
|
||
closeCollapsedSearch()
|
||
}
|
||
}
|
||
|
||
onBeforeUnmount(() => {
|
||
if (typeof window !== 'undefined') {
|
||
window.removeEventListener('tier-maker:backend-status', handleBackendStatus)
|
||
window.removeEventListener('resize', syncViewportWidth)
|
||
window.removeEventListener('keydown', handleGlobalKeydown)
|
||
}
|
||
})
|
||
|
||
watch(
|
||
() => route.fullPath,
|
||
() => {
|
||
searchQuery.value = typeof route.query.q === 'string' ? route.query.q : ''
|
||
isCollapsedSearchOpen.value = false
|
||
isGuideModalOpen.value = false
|
||
if (isMobileLayout.value) {
|
||
mobileLeftNavOpen.value = false
|
||
rightRailOpen.value = false
|
||
}
|
||
}
|
||
)
|
||
|
||
watch(
|
||
isMobileLayout,
|
||
(mobile) => {
|
||
if (mobile) {
|
||
leftRailCollapsed.value = false
|
||
mobileLeftNavOpen.value = false
|
||
rightRailOpen.value = false
|
||
return
|
||
}
|
||
mobileLeftNavOpen.value = false
|
||
rightRailOpen.value = true
|
||
if (typeof window !== 'undefined') {
|
||
window.localStorage.setItem('tier-maker:right-rail-open', '1')
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
watch(
|
||
usesLocalRightRail,
|
||
(needed) => {
|
||
if (!needed || rightRailOpen.value || isMobileLayout.value) return
|
||
rightRailOpen.value = true
|
||
if (typeof window !== 'undefined') {
|
||
window.localStorage.setItem('tier-maker:right-rail-open', '1')
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
function isRouteActive(path) {
|
||
if (path === '/') return route.path === '/'
|
||
return route.path.startsWith(path)
|
||
}
|
||
|
||
function toggleLeftRail() {
|
||
if (isMobileLayout.value) {
|
||
mobileLeftNavOpen.value = !mobileLeftNavOpen.value
|
||
return
|
||
}
|
||
leftRailCollapsed.value = !leftRailCollapsed.value
|
||
if (typeof window !== 'undefined') {
|
||
window.localStorage.setItem('tier-maker:left-rail-collapsed', leftRailCollapsed.value ? '1' : '0')
|
||
}
|
||
}
|
||
|
||
function toggleRightRail() {
|
||
rightRailOpen.value = !rightRailOpen.value
|
||
if (typeof window !== 'undefined') {
|
||
window.localStorage.setItem('tier-maker:right-rail-open', rightRailOpen.value ? '1' : '0')
|
||
}
|
||
}
|
||
|
||
function setTopicViewMode(mode) {
|
||
if (route.name !== 'topicHub') return
|
||
const nextQuery = { ...route.query }
|
||
if (mode === 'list') nextQuery.view = 'list'
|
||
else delete nextQuery.view
|
||
router.replace({ path: route.path, query: nextQuery })
|
||
}
|
||
|
||
function openCollapsedSearch() {
|
||
if (!leftRailCollapsed.value || isMobileLayout.value) return
|
||
isCollapsedSearchOpen.value = true
|
||
}
|
||
|
||
function closeCollapsedSearch() {
|
||
isCollapsedSearchOpen.value = false
|
||
}
|
||
|
||
function openGuideModal(stepIndex = 0) {
|
||
guideStepIndex.value = Math.min(Math.max(Number(stepIndex) || 0, 0), guideSteps.length - 1)
|
||
isGuideModalOpen.value = true
|
||
}
|
||
|
||
function closeGuideModal() {
|
||
isGuideModalOpen.value = false
|
||
}
|
||
|
||
function selectGuideStep(index) {
|
||
guideStepIndex.value = Math.min(Math.max(index, 0), guideSteps.length - 1)
|
||
}
|
||
|
||
function showPrevGuideStep() {
|
||
if (isGuidePrevDisabled.value) return
|
||
guideStepIndex.value -= 1
|
||
}
|
||
|
||
function showNextGuideStep() {
|
||
if (isGuideNextDisabled.value) return
|
||
guideStepIndex.value += 1
|
||
}
|
||
|
||
function handleLeftRailSearch() {
|
||
if (leftRailCollapsed.value && !isMobileLayout.value) {
|
||
openCollapsedSearch()
|
||
return
|
||
}
|
||
submitGlobalSearch()
|
||
}
|
||
|
||
function submitGlobalSearch() {
|
||
const query = (searchQuery.value || '').trim()
|
||
isCollapsedSearchOpen.value = false
|
||
router.push(homePath(query))
|
||
}
|
||
|
||
function reloadApp() {
|
||
if (typeof window === 'undefined') return
|
||
window.location.reload()
|
||
}
|
||
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
class="appShell"
|
||
:class="{
|
||
'appShell--leftCollapsed': leftRailCollapsed,
|
||
'appShell--mobileNavClosed': isMobileLayout && !mobileLeftNavOpen,
|
||
'appShell--rightClosed': !rightRailOpen,
|
||
'appShell--rightOverlay': isRightRailOverlay,
|
||
}"
|
||
:style="shellStyle"
|
||
>
|
||
<template v-if="showBackendFallback">
|
||
<main class="backendFallback">
|
||
<section class="backendFallback__card">
|
||
<div class="backendFallback__eyebrow">{{ backendState === 'maintenance' ? 'Maintenance' : 'Connection' }}</div>
|
||
<h1 class="backendFallback__title">{{ backendState === 'maintenance' ? '서비스 점검 중' : '서버 연결 확인 중' }}</h1>
|
||
<p class="backendFallback__desc">
|
||
{{
|
||
backendMessage ||
|
||
(backendState === 'maintenance'
|
||
? '백엔드 또는 데이터베이스 작업으로 인해 잠시 이용이 어렵습니다. 잠시 후 다시 시도해주세요.'
|
||
: '네트워크 또는 서버 연결 상태를 확인한 뒤 다시 시도해주세요.')
|
||
}}
|
||
</p>
|
||
<div class="backendFallback__actions">
|
||
<button class="backendFallback__button" type="button" @click="reloadApp">다시 시도</button>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</template>
|
||
<template v-else>
|
||
<aside class="leftRail">
|
||
<div class="leftRail__top railHeader">
|
||
<button v-if="!isMobileLayout" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="왼쪽 패널 토글" @click="toggleLeftRail">
|
||
<SvgIcon :src="iconDockToRight" :size="24" />
|
||
</button>
|
||
</div>
|
||
|
||
<div class="leftRail__body">
|
||
<div class="leftRail__content">
|
||
<div v-if="authReady" class="appUserCard">
|
||
<div class="appUserCard__button">
|
||
<img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" draggable="false" />
|
||
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
|
||
<div class="appUserCard__meta">
|
||
<div class="appUserCard__name">{{ accountName }}</div>
|
||
<div class="appUserCard__email">{{ accountEmail }}</div>
|
||
</div>
|
||
<button
|
||
v-if="isMobileLayout"
|
||
class="appUserCard__navToggle"
|
||
type="button"
|
||
:aria-label="mobileLeftNavOpen ? '네비게이션 메뉴 닫기' : '네비게이션 메뉴 열기'"
|
||
:aria-expanded="mobileLeftNavOpen"
|
||
@click="toggleLeftRail"
|
||
>
|
||
<SvgIcon :src="mobileLeftNavOpen ? iconDockToLeft : iconDockToRight" :size="24" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="leftRail__mobileMenu">
|
||
<form class="searchStub" @submit.prevent="submitGlobalSearch">
|
||
<button class="searchStub__iconButton" type="button" :aria-label="leftRailCollapsed ? '검색 열기' : '검색'" @click="handleLeftRailSearch">
|
||
<span class="searchStub__icon">
|
||
<SvgIcon :src="iconSearch" :size="24" />
|
||
</span>
|
||
</button>
|
||
<input v-model="searchQuery" class="searchStub__input" type="search" :placeholder="leftRailCollapsed ? '' : leftRailSearchPlaceholder" />
|
||
</form>
|
||
|
||
<nav
|
||
class="leftNav"
|
||
:class="{ 'leftNav--hasActive': activeLeftNavIndex >= 0 }"
|
||
:style="{ '--left-nav-active-index': String(Math.max(activeLeftNavIndex, 0)) }"
|
||
>
|
||
<span class="leftNav__indicator" aria-hidden="true"></span>
|
||
<RouterLink
|
||
v-for="item in leftNavItems"
|
||
:key="item.key"
|
||
:to="item.path"
|
||
class="leftNav__item"
|
||
:class="{ 'leftNav__item--active': isRouteActive(item.path) }"
|
||
:title="leftRailCollapsed ? item.label : ''"
|
||
:aria-label="leftRailCollapsed ? item.label : undefined"
|
||
>
|
||
<span class="leftNav__glyph">
|
||
<SvgIcon v-if="item.iconSrc" :src="item.iconSrc" :size="24" />
|
||
<svg v-else viewBox="0 0 24 24" aria-hidden="true"><path :d="item.icon" /></svg>
|
||
</span>
|
||
<span class="leftNav__label">{{ item.label }}</span>
|
||
</RouterLink>
|
||
</nav>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="leftRail__bottom">
|
||
<RouterLink v-if="leftBottomPrimaryAction" :to="leftBottomPrimaryAction.to" class="adminButton">{{ leftBottomPrimaryAction.label }}</RouterLink>
|
||
<RouterLink
|
||
v-if="leftBottomPrimaryAction"
|
||
:to="leftBottomPrimaryAction.to"
|
||
class="leftRail__collapsedAction"
|
||
:title="leftBottomPrimaryAction.label"
|
||
:aria-label="leftBottomPrimaryAction.label"
|
||
>
|
||
<SvgIcon :src="leftBottomPrimaryAction.iconSrc || iconAddNotes" :size="24" />
|
||
</RouterLink>
|
||
<button v-if="showSettingsGuideButton" class="adminButton adminButton--icon" type="button" @click="openGuideModal()">
|
||
<SvgIcon :src="iconMenuBook" :size="18" class="adminButton__icon" />
|
||
<span>가이드 보기</span>
|
||
</button>
|
||
<RouterLink v-if="authReady && isAdmin" to="/admin/featured" class="adminButton">관리자 메뉴</RouterLink>
|
||
<RouterLink v-else-if="authReady && !auth.user" to="/login" class="adminButton">로그인</RouterLink>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="appMain">
|
||
<section class="workspace" :class="{ 'workspace--localRail': usesLocalRightRail }">
|
||
<header class="workspaceHead railHeader">
|
||
<div class="workspaceHead__brand" @click="$router.push('/')">
|
||
<span class="workspaceHead__brandTitle">Tier Maker</span>
|
||
</div>
|
||
<div class="workspaceHead__actions">
|
||
<div v-if="showTopicViewToggle" class="viewToggle" role="group" aria-label="티어표 보기 방식">
|
||
<button class="ghostIcon ghostIcon--iconOnly" :class="{ 'ghostIcon--active': topicViewMode === 'grid' }" type="button" aria-label="그리드 보기" @click="setTopicViewMode('grid')">
|
||
<SvgIcon :src="iconGridView" :size="24" />
|
||
</button>
|
||
<button class="ghostIcon ghostIcon--iconOnly" :class="{ 'ghostIcon--active': topicViewMode === 'list' }" type="button" aria-label="리스트 보기" @click="setTopicViewMode('list')">
|
||
<SvgIcon :src="iconLists" :size="24" />
|
||
</button>
|
||
</div>
|
||
<button v-if="!rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 열기" @click="toggleRightRail">
|
||
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
<div class="workspaceBody" :class="{ 'workspaceBody--localRail': usesLocalRightRail }">
|
||
<RouterView />
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<div v-if="isCollapsedSearchOpen" class="collapsedSearchModal" role="dialog" aria-modal="true" :aria-label="leftRailSearchPlaceholder" @click.self="closeCollapsedSearch">
|
||
<form class="collapsedSearchBar" @submit.prevent="submitGlobalSearch">
|
||
<span class="collapsedSearchBar__icon">
|
||
<SvgIcon :src="iconSearch" :size="24" />
|
||
</span>
|
||
<input v-model="searchQuery" class="collapsedSearchBar__input" type="search" :placeholder="leftRailSearchPlaceholder" autofocus />
|
||
</form>
|
||
</div>
|
||
|
||
<div v-if="isGuideModalOpen" class="guideModal" role="dialog" aria-modal="true" aria-label="티어 메이커 기능 안내" @click.self="closeGuideModal">
|
||
<div class="guideModal__dialog">
|
||
<div class="guideModal__sidebar">
|
||
<div class="guideModal__eyebrow">Guide</div>
|
||
<div class="guideModal__title">티어 메이커 기능 안내</div>
|
||
<div class="guideModal__mobilePicker">
|
||
<label class="guideModal__mobileLabel" for="guide-step-select">단계 선택</label>
|
||
<select id="guide-step-select" class="guideModal__mobileSelect" :value="guideStepIndex" @change="selectGuideStep(Number($event.target.value))">
|
||
<option v-for="(step, index) in guideSteps" :key="step.id + '-select'" :value="index">{{ index + 1 }}. {{ step.title }}</option>
|
||
</select>
|
||
</div>
|
||
<div class="guideModal__list">
|
||
<button
|
||
v-for="(step, index) in guideSteps"
|
||
:key="step.id"
|
||
class="guideModal__listItem"
|
||
:class="{ 'guideModal__listItem--active': index === guideStepIndex }"
|
||
type="button"
|
||
@click="selectGuideStep(index)"
|
||
>
|
||
<span class="guideModal__listIndex">{{ index + 1 }}</span>
|
||
<span class="guideModal__listLabel">{{ step.title }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="guideModal__main">
|
||
<button class="guideModal__close" type="button" aria-label="사용법 닫기" @click="closeGuideModal">닫기</button>
|
||
<div class="guideModal__content">
|
||
<button class="guideModal__arrow" type="button" aria-label="이전 단계" :disabled="isGuidePrevDisabled" @click="showPrevGuideStep">‹</button>
|
||
<div class="guideModal__body">
|
||
<div class="guideModal__media">
|
||
<div class="guideModal__mediaPlaceholder">
|
||
<div class="guideModal__mediaBadge">16:9</div>
|
||
<div class="guideModal__mediaTitle">{{ currentGuideStep.title }}</div>
|
||
<div class="guideModal__mediaHint">스크린샷 준비 중</div>
|
||
</div>
|
||
</div>
|
||
<div class="guideModal__text">
|
||
<div class="guideModal__stepLabel">GUIDE {{ guideStepIndex + 1 }}</div>
|
||
<div class="guideModal__stepTitle">{{ currentGuideStep.title }}</div>
|
||
<div class="guideModal__stepSummary">{{ currentGuideStep.summary }}</div>
|
||
<p class="guideModal__stepDescription">{{ currentGuideStep.description }}</p>
|
||
</div>
|
||
<div class="guideModal__footer">
|
||
<div class="guideModal__pagination">
|
||
<button
|
||
v-for="(step, index) in guideSteps"
|
||
:key="step.id + '-dot'"
|
||
class="guideModal__dot"
|
||
:class="{ 'guideModal__dot--active': index === guideStepIndex }"
|
||
type="button"
|
||
:aria-label="step.title"
|
||
@click="selectGuideStep(index)"
|
||
></button>
|
||
</div>
|
||
<button class="guideModal__next" type="button" @click="isGuideNextDisabled ? closeGuideModal() : showNextGuideStep()">
|
||
{{ isGuideNextDisabled ? '닫기' : '다음' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button class="guideModal__arrow" type="button" aria-label="다음 단계" :disabled="isGuideNextDisabled" @click="showNextGuideStep">›</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button v-if="rightRailOpen && isRightRailOverlay" class="rightRailBackdrop" type="button" aria-label="오른쪽 패널 닫기" @click="toggleRightRail"></button>
|
||
|
||
<aside class="rightRail" :class="{ 'rightRail--closed': !rightRailOpen, 'rightRail--overlay': isRightRailOverlay }" :aria-hidden="!rightRailOpen">
|
||
<div class="rightRail__top railHeader">
|
||
<button v-if="rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 닫기" @click="toggleRightRail">
|
||
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||
</button>
|
||
</div>
|
||
<div class="rightRail__body">
|
||
<div class="rightRail__content">
|
||
<div
|
||
id="local-right-rail-root"
|
||
class="localRightRailRoot"
|
||
:class="{ 'localRightRailRoot--hidden': !usesLocalRightRail }"
|
||
></div>
|
||
<template v-if="!usesLocalRightRail">
|
||
<section v-if="showSettingsThemePanel" class="settingsThemePanel">
|
||
<div class="settingsThemePanel__eyebrow">Appearance</div>
|
||
<div class="settingsThemePanel__title">테마 설정</div>
|
||
<div class="settingsThemePanel__desc">밝은 톤과 어두운 톤 중 원하는 작업 환경으로 전환할 수 있어요.</div>
|
||
<label class="toggleSwitch settingsThemePanel__toggle">
|
||
<input :checked="isLightTheme" type="checkbox" @change="toggleTheme" />
|
||
<span class="toggleSwitch__label">{{ isLightTheme ? '라이트 모드' : '다크 모드' }}</span>
|
||
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||
</label>
|
||
</section>
|
||
<RightRailAd v-else />
|
||
</template>
|
||
</div>
|
||
<div class="rightRail__bottom">
|
||
<template v-if="showRightRailAction">
|
||
<section class="rightRailAction">
|
||
<button class="rightRailAction__button" type="button" @click="routeMeta.action">
|
||
{{ routeMeta.actionLabel }}
|
||
</button>
|
||
</section>
|
||
</template>
|
||
<div class="rightRail__footer">
|
||
<span>Copyright © 2026 </span>
|
||
<a :href="RIGHT_RAIL_COPYRIGHT_URL" target="_blank" rel="noreferrer">zenn</a>
|
||
<span>. All rights reserved.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<div class="toastStack" :class="{ 'toastStack--preview': isPreviewMode }" aria-live="polite" aria-atomic="true">
|
||
<div v-for="item in toasts" :key="item.id" class="toast" :class="[`toast--${item.type}`, { 'toast--closing': item.isClosing }]">
|
||
<div class="toast__body">
|
||
<div class="toast__message">{{ item.message }}</div>
|
||
<div v-if="item.count > 1" class="toast__count">x{{ item.count }}</div>
|
||
</div>
|
||
<button class="toast__close" type="button" @click="dismissToast(item.id)">닫기</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.appShell {
|
||
min-height: 100dvh;
|
||
display: grid;
|
||
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr) var(--right-rail-width, 325px);
|
||
background: var(--theme-shell-bg);
|
||
color: var(--theme-text);
|
||
transition: grid-template-columns 220ms ease;
|
||
}
|
||
|
||
.backendFallback {
|
||
min-width: 100dvw;
|
||
min-height: 100dvh;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 32px;
|
||
background:
|
||
radial-gradient(circle at top, rgba(86, 153, 255, 0.14), transparent 38%),
|
||
var(--theme-shell-bg);
|
||
}
|
||
|
||
.backendFallback__card {
|
||
width: min(100%, 560px);
|
||
display: grid;
|
||
gap: 18px;
|
||
padding: 28px;
|
||
border-radius: 28px;
|
||
border: 1px solid var(--theme-card-border);
|
||
background: var(--theme-card-bg);
|
||
box-shadow: inset 0 1px 0 var(--theme-card-shadow);
|
||
}
|
||
|
||
.backendFallback__eyebrow {
|
||
color: var(--theme-accent-strong);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.backendFallback__title {
|
||
margin: 0;
|
||
font-size: clamp(28px, 4vw, 42px);
|
||
line-height: 1.05;
|
||
letter-spacing: -0.04em;
|
||
}
|
||
|
||
.backendFallback__desc {
|
||
margin: 0;
|
||
color: var(--theme-text-muted);
|
||
font-size: 15px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.backendFallback__actions {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.backendFallback__button {
|
||
min-width: 128px;
|
||
padding: 12px 18px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--theme-accent-soft-strong);
|
||
background: var(--theme-accent-soft);
|
||
color: var(--theme-text-strong);
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.leftRail,
|
||
.rightRail {
|
||
min-height: 100dvh;
|
||
border-right: 1px solid var(--theme-border);
|
||
background: var(--theme-rail-bg);
|
||
box-sizing: border-box;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.rightRail {
|
||
border-right: 0;
|
||
border-left: 1px solid var(--theme-border);
|
||
transition:
|
||
opacity 220ms ease,
|
||
transform 220ms ease,
|
||
padding 220ms ease,
|
||
border-color 220ms ease;
|
||
}
|
||
|
||
.appShell--rightClosed .rightRail {
|
||
opacity: 0;
|
||
transform: translateX(18px);
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
border-left-color: transparent;
|
||
}
|
||
|
||
.railHeader {
|
||
height: 56px;
|
||
min-height: 56px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 12px;
|
||
border-bottom: 1px solid var(--theme-border);
|
||
background: var(--theme-rail-bg);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
|
||
.leftRail__top,
|
||
.rightRail__top {
|
||
gap: 12px;
|
||
}
|
||
|
||
.leftRail__top {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.rightRail__top {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.leftRail__body,
|
||
.rightRail__body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 14px 12px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.leftRail__body {
|
||
max-height: calc(100dvh - 56px);
|
||
}
|
||
|
||
.rightRail__body {
|
||
max-height: none;
|
||
}
|
||
|
||
.leftRail__content,
|
||
.rightRail__content {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.rightRail__body {
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.rightRail__content {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
overflow: visible;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ghostIcon {
|
||
min-width: 28px;
|
||
height: 28px;
|
||
padding: 0 10px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--theme-border);
|
||
background: var(--theme-pill-bg);
|
||
color: var(--theme-text-soft);
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ghostIcon svg,
|
||
.searchStub__icon svg,
|
||
.leftNav__glyph svg,
|
||
.contextLink svg {
|
||
width: 28px;
|
||
height: 28px;
|
||
stroke: currentColor;
|
||
stroke-width: 1.8;
|
||
fill: none;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
|
||
.ghostIcon img,
|
||
.leftNav__glyph img,
|
||
.searchStub__icon img {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: block;
|
||
filter: var(--theme-icon-filter);
|
||
}
|
||
|
||
.ghostIcon--iconOnly {
|
||
min-width: 32px;
|
||
width: 32px;
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.appUserCard {
|
||
position: relative;
|
||
margin-bottom: 14px;
|
||
min-height: 58px;
|
||
transition: margin 220ms ease;
|
||
}
|
||
|
||
.appUserCard__button,
|
||
.appUserCard__guest {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
padding: 10px;
|
||
border-radius: 16px;
|
||
color: inherit;
|
||
text-align: left;
|
||
cursor: default;
|
||
box-sizing: border-box;
|
||
transition: padding 220ms ease, justify-content 220ms ease;
|
||
}
|
||
|
||
.appUserCard__avatar {
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 999px;
|
||
object-fit: cover;
|
||
flex: 0 0 auto;
|
||
border: 1px solid var(--theme-avatar-border);
|
||
background: var(--theme-surface-soft-3);
|
||
}
|
||
|
||
.appUserCard__avatar--fallback {
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--theme-surface-soft-3);
|
||
font-weight: 900;
|
||
}
|
||
|
||
.appUserCard__meta {
|
||
min-width: 0;
|
||
display: grid;
|
||
gap: 4px;
|
||
max-width: 180px;
|
||
overflow: hidden;
|
||
transition: opacity 180ms ease, max-width 220ms ease, transform 220ms ease;
|
||
}
|
||
|
||
.leftRail__mobileMenu {
|
||
display: grid;
|
||
}
|
||
|
||
.appUserCard__navToggle {
|
||
display: none;
|
||
width: 42px;
|
||
height: 42px;
|
||
margin-left: auto;
|
||
border: 0;
|
||
border-radius: 14px;
|
||
background: var(--theme-surface-soft);
|
||
color: var(--theme-text-soft);
|
||
cursor: pointer;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.appUserCard__name {
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.appUserCard__email {
|
||
font-size: 12px;
|
||
color: var(--theme-text-muted);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.searchStub {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 11px 12px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--theme-border);
|
||
background: var(--theme-pill-bg);
|
||
color: var(--theme-text-soft);
|
||
margin-bottom: 14px;
|
||
box-sizing: border-box;
|
||
transition: padding 220ms ease, justify-content 220ms ease;
|
||
}
|
||
|
||
.searchStub__input {
|
||
min-width: 0;
|
||
flex: 1;
|
||
max-width: 100%;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--theme-text);
|
||
outline: none;
|
||
font: inherit;
|
||
overflow: hidden;
|
||
transition: opacity 180ms ease, max-width 220ms ease, transform 220ms ease;
|
||
}
|
||
|
||
.searchStub__input::placeholder {
|
||
color: var(--theme-text-soft);
|
||
}
|
||
|
||
.searchStub__iconButton {
|
||
border: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.searchStub__icon {
|
||
width: 18px;
|
||
height: 18px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.leftNav {
|
||
--left-nav-gap: 8px;
|
||
--left-nav-item-height: 50px;
|
||
position: relative;
|
||
display: grid;
|
||
gap: var(--left-nav-gap);
|
||
isolation: isolate;
|
||
}
|
||
|
||
.leftNav__indicator {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: var(--left-nav-item-height);
|
||
border-radius: 14px;
|
||
background: var(--theme-surface-soft-3);
|
||
transform: translateY(calc(var(--left-nav-active-index, 0) * (var(--left-nav-item-height) + var(--left-nav-gap))));
|
||
transition: transform 240ms ease, opacity 200ms ease;
|
||
opacity: 0;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.leftNav--hasActive .leftNav__indicator {
|
||
opacity: 1;
|
||
}
|
||
|
||
.leftNav__item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: var(--left-nav-item-height);
|
||
gap: 12px;
|
||
padding: 11px 12px;
|
||
border-radius: 14px;
|
||
color: var(--theme-text-muted);
|
||
text-decoration: none;
|
||
transition: background 180ms ease, color 180ms ease, transform 180ms ease;
|
||
z-index: 1;
|
||
}
|
||
|
||
.leftNav__label {
|
||
min-width: 0;
|
||
max-width: 140px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
transition: opacity 180ms ease, max-width 220ms ease, transform 220ms ease;
|
||
}
|
||
|
||
.leftNav__item--active,
|
||
.leftNav__item.router-link-active {
|
||
color: var(--theme-text-strong);
|
||
}
|
||
|
||
.leftNav__glyph {
|
||
/* width: 28px; */
|
||
/* height: 28px; */
|
||
/* border-radius: 10px; */
|
||
display: grid;
|
||
place-items: center;
|
||
/* background: rgba(255, 255, 255, 0.06); */
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__top {
|
||
justify-content: center;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__body {
|
||
padding-left: 10px;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard {
|
||
min-height: 50px;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__button,
|
||
.appShell--leftCollapsed .appUserCard__guest {
|
||
width: 100%;
|
||
height: 50px;
|
||
min-height: 44px;
|
||
padding: 0;
|
||
gap: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__meta,
|
||
.appShell--leftCollapsed .leftNav__label,
|
||
.appShell--leftCollapsed .searchStub__input {
|
||
display: none;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__avatar {
|
||
width: 44px;
|
||
height: 44px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .searchStub {
|
||
height: 50px;
|
||
margin-bottom: 0;
|
||
padding: 11px 0;
|
||
gap: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.appShell--leftCollapsed .searchStub__iconButton {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftNav {
|
||
--left-nav-gap: 10px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftNav__item {
|
||
width: 100%;
|
||
min-height: 50px;
|
||
height: 50px;
|
||
padding: 11px 0;
|
||
gap: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftNav__glyph {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__content {
|
||
display: grid;
|
||
align-content: start;
|
||
justify-items: stretch;
|
||
gap: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.leftRail__bottom {
|
||
display: grid;
|
||
gap: 10px;
|
||
justify-content: stretch;
|
||
align-items: flex-end;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.leftRail__collapsedAction {
|
||
display: none;
|
||
}
|
||
|
||
.adminButton {
|
||
width: 100%;
|
||
display: inline-flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: var(--theme-surface-soft);
|
||
color: var(--theme-text);
|
||
text-decoration: none;
|
||
box-sizing: border-box;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.adminButton--icon {
|
||
text-align: center;
|
||
}
|
||
|
||
.adminButton__icon {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__bottom {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__bottom .adminButton {
|
||
display: none;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__bottom .leftRail__collapsedAction {
|
||
width: 100%;
|
||
min-height: 50px;
|
||
height: 50px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: var(--theme-surface-soft);
|
||
color: var(--theme-text);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.appMain {
|
||
min-width: 0;
|
||
min-height: 0;
|
||
box-sizing: border-box;
|
||
background: var(--theme-main-bg);
|
||
border-left: 1px solid var(--theme-border);
|
||
border-right: 1px solid var(--theme-border);
|
||
}
|
||
|
||
.workspace {
|
||
display: grid;
|
||
grid-template-rows: 56px minmax(0, 1fr);
|
||
gap: 0;
|
||
min-height: 100dvh;
|
||
}
|
||
|
||
.workspace--localRail {
|
||
gap: 0;
|
||
}
|
||
|
||
.workspaceHead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.workspaceHead__brand {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.workspaceHead__brandTitle {
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
letter-spacing: -0.05em;
|
||
color: var(--theme-text-strong);
|
||
}
|
||
|
||
.workspaceHead__actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.viewToggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px;
|
||
border-radius: 14px;
|
||
background: var(--theme-surface-soft);
|
||
}
|
||
|
||
.viewToggle .ghostIcon--iconOnly {
|
||
width: 36px;
|
||
height: 36px;
|
||
min-width: 36px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.ghostIcon--active {
|
||
background: var(--theme-surface-soft-3);
|
||
}
|
||
|
||
.workspaceBody {
|
||
min-height: 0;
|
||
padding: 18px 18px 32px;
|
||
border: 0;
|
||
border-radius: 0;
|
||
background: var(--theme-workspace-bg);
|
||
box-shadow: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.workspaceBody--localRail {
|
||
min-height: 0;
|
||
padding: 18px 18px 32px;
|
||
border: 0;
|
||
border-radius: 0;
|
||
background: var(--theme-workspace-bg);
|
||
box-shadow: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.rightRail {
|
||
gap: 0;
|
||
}
|
||
|
||
.rightRailAction {
|
||
display: grid;
|
||
width: 100%;
|
||
}
|
||
|
||
.rightRail__bottom {
|
||
margin-top: auto;
|
||
display: grid;
|
||
gap: 10px;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.rightRail__footer {
|
||
padding: 0 4px 2px;
|
||
font-size: 9px;
|
||
line-height: 1.4;
|
||
text-align: center;
|
||
color: var(--theme-text-faint);
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.rightRail__footer a {
|
||
color: var(--theme-text-strong);
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.rightRail__footer a:hover {
|
||
color: var(--theme-text);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.settingsThemePanel {
|
||
display: grid;
|
||
gap: 10px;
|
||
padding: 18px;
|
||
border-radius: 22px;
|
||
border: 1px solid var(--theme-border);
|
||
background: var(--theme-pill-bg);
|
||
}
|
||
|
||
.settingsThemePanel__eyebrow {
|
||
font-size: 11px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--theme-text-soft);
|
||
}
|
||
|
||
.settingsThemePanel__title {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: var(--theme-text-strong);
|
||
}
|
||
|
||
.settingsThemePanel__desc {
|
||
color: var(--theme-text-muted);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.settingsThemePanel__toggle {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.toggleSwitch {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: var(--theme-surface-soft);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.toggleSwitch input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.toggleSwitch__track {
|
||
position: relative;
|
||
width: 42px;
|
||
height: 24px;
|
||
border-radius: 999px;
|
||
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;
|
||
}
|
||
|
||
.toggleSwitch__thumb {
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 2px;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
|
||
transition: transform 180ms ease;
|
||
}
|
||
|
||
:root[data-theme='light'] .toggleSwitch__thumb {
|
||
background: #fff;
|
||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.16);
|
||
}
|
||
|
||
.toggleSwitch__label {
|
||
font-weight: 800;
|
||
color: var(--theme-text);
|
||
}
|
||
|
||
.toggleSwitch input:checked ~ .toggleSwitch__track {
|
||
background: rgba(96, 165, 250, 0.34);
|
||
border-color: rgba(96, 165, 250, 0.42);
|
||
}
|
||
|
||
.toggleSwitch input:checked ~ .toggleSwitch__track .toggleSwitch__thumb {
|
||
transform: translateX(18px);
|
||
}
|
||
|
||
.rightRailAction__button {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(77, 127, 233, 0.96);
|
||
background: var(--theme-accent-bg);
|
||
color: var(--theme-accent-text);
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.rightRailBackdrop {
|
||
display: none;
|
||
}
|
||
|
||
.guideModal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 36;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 32px 20px;
|
||
background: var(--theme-overlay-scrim);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.guideModal__dialog {
|
||
width: min(1180px, calc(100vw - 40px));
|
||
height: min(760px, calc(100dvh - 64px));
|
||
display: grid;
|
||
grid-template-columns: 260px minmax(0, 1fr);
|
||
border-radius: 28px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: linear-gradient(180deg, rgba(34, 34, 34, 0.98), rgba(18, 18, 18, 0.98));
|
||
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
|
||
}
|
||
|
||
.guideModal__sidebar {
|
||
display: grid;
|
||
align-content: start;
|
||
gap: 18px;
|
||
padding: 28px 22px;
|
||
background: var(--theme-pill-bg);
|
||
border-right: 1px solid var(--theme-border);
|
||
}
|
||
|
||
.guideModal__eyebrow {
|
||
font-size: 11px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--theme-text-faint);
|
||
}
|
||
|
||
.guideModal__title {
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
line-height: 1.1;
|
||
letter-spacing: -0.04em;
|
||
}
|
||
|
||
.guideModal__mobilePicker {
|
||
display: none;
|
||
}
|
||
|
||
.guideModal__mobileLabel {
|
||
font-size: 11px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--theme-text-faint);
|
||
}
|
||
|
||
.guideModal__mobileSelect {
|
||
width: 100%;
|
||
min-height: 56px;
|
||
padding: 0 18px;
|
||
border-radius: 18px;
|
||
border: 1px solid rgba(77, 127, 233, 0.46);
|
||
background: rgba(77, 127, 233, 0.14);
|
||
color: var(--theme-text-strong);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.guideModal__list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.guideModal__listItem {
|
||
display: grid;
|
||
grid-template-columns: 26px minmax(0, 1fr);
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 12px 14px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--theme-border);
|
||
background: var(--theme-pill-bg);
|
||
color: var(--theme-text-muted);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
}
|
||
|
||
.guideModal__listItem--active {
|
||
border-color: rgba(77, 127, 233, 0.5);
|
||
background: rgba(77, 127, 233, 0.14);
|
||
color: var(--theme-text-strong);
|
||
}
|
||
|
||
.guideModal__listIndex {
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
color: var(--theme-text-faint);
|
||
}
|
||
|
||
.guideModal__listLabel {
|
||
min-width: 0;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.guideModal__main {
|
||
min-width: 0;
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
padding: 24px 28px 28px;
|
||
min-height: 0;
|
||
}
|
||
|
||
|
||
.guideModal__close {
|
||
justify-self: end;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--theme-text-muted);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.guideModal__content {
|
||
min-width: 0;
|
||
min-height: 0;
|
||
display: grid;
|
||
grid-template-columns: 52px minmax(0, 1fr) 52px;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.guideModal__body {
|
||
min-width: 0;
|
||
min-height: 0;
|
||
display: grid;
|
||
gap: 18px;
|
||
}
|
||
|
||
.guideModal__media {
|
||
width: 100%;
|
||
}
|
||
|
||
.guideModal__mediaPlaceholder {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
border-radius: 24px;
|
||
border: 1px solid var(--theme-border);
|
||
background: radial-gradient(circle at top, rgba(77, 127, 233, 0.18), rgba(255, 255, 255, 0.02) 52%), rgba(255, 255, 255, 0.03);
|
||
display: grid;
|
||
align-content: center;
|
||
justify-items: center;
|
||
gap: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.guideModal__mediaBadge {
|
||
font-size: 11px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: rgba(255, 255, 255, 0.38);
|
||
}
|
||
|
||
.guideModal__mediaTitle {
|
||
font-size: 24px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.guideModal__mediaHint {
|
||
font-size: 13px;
|
||
color: var(--theme-text-faint);
|
||
}
|
||
|
||
.guideModal__text {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.guideModal__stepLabel {
|
||
font-size: 11px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--theme-text-soft);
|
||
}
|
||
|
||
.guideModal__stepTitle {
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
letter-spacing: -0.04em;
|
||
}
|
||
|
||
.guideModal__stepSummary {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--theme-text);
|
||
}
|
||
|
||
.guideModal__stepDescription {
|
||
margin: 0;
|
||
max-width: 720px;
|
||
line-height: 1.7;
|
||
color: var(--theme-text-soft);
|
||
}
|
||
|
||
.guideModal__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 18px;
|
||
}
|
||
|
||
.guideModal__pagination {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.guideModal__dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
border: 0;
|
||
background: var(--theme-surface-soft-3);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.guideModal__dot--active {
|
||
width: 26px;
|
||
background: rgba(77, 127, 233, 0.9);
|
||
}
|
||
|
||
.guideModal__next {
|
||
padding: 12px 18px;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(77, 127, 233, 0.96);
|
||
background: var(--theme-accent-bg);
|
||
color: var(--theme-accent-text);
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.guideModal__arrow {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--theme-border);
|
||
background: var(--theme-surface-soft);
|
||
color: var(--theme-text);
|
||
font-size: 28px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.guideModal__arrow:disabled {
|
||
opacity: 0.28;
|
||
cursor: default;
|
||
}
|
||
|
||
.collapsedSearchModal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 35;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
padding: 88px 20px 20px;
|
||
background: color-mix(in srgb, var(--theme-body-bg) 72%, transparent);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.collapsedSearchBar {
|
||
width: min(520px, calc(100vw - 32px));
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 18px 22px;
|
||
border-radius: 24px;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: var(--theme-main-bg);
|
||
box-shadow: 0 28px 60px rgba(0, 0, 0, 0.34);
|
||
}
|
||
|
||
.collapsedSearchBar__icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.collapsedSearchBar__icon img {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: block;
|
||
filter: var(--theme-icon-filter);
|
||
}
|
||
|
||
.collapsedSearchBar__input {
|
||
min-width: 0;
|
||
flex: 1;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--theme-text);
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
outline: none;
|
||
}
|
||
|
||
.collapsedSearchBar__input::placeholder {
|
||
color: var(--theme-text-faint);
|
||
}
|
||
|
||
.localRightRailRoot {
|
||
flex: 1 1 auto;
|
||
min-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.localRightRailRoot--hidden {
|
||
display: none;
|
||
}
|
||
|
||
.toastStack {
|
||
position: fixed;
|
||
top: 18px;
|
||
right: 20px;
|
||
z-index: 40;
|
||
display: grid;
|
||
gap: 10px;
|
||
width: min(360px, calc(100vw - 24px));
|
||
}
|
||
|
||
.toastStack--preview {
|
||
top: 12px;
|
||
}
|
||
|
||
.toast {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
padding: 12px 14px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--theme-border-strong);
|
||
background: color-mix(in srgb, var(--theme-main-bg) 94%, transparent);
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.28);
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
transition: opacity 220ms ease, transform 220ms ease;
|
||
}
|
||
|
||
.toast--closing {
|
||
opacity: 0;
|
||
transform: translateY(-6px);
|
||
}
|
||
|
||
.toast--success {
|
||
border-color: rgba(52, 211, 153, 0.38);
|
||
}
|
||
|
||
.toast--error {
|
||
border-color: rgba(239, 68, 68, 0.34);
|
||
}
|
||
|
||
.toast--info {
|
||
border-color: rgba(96, 165, 250, 0.34);
|
||
}
|
||
|
||
.toast__message {
|
||
line-height: 1.5;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.toast__count {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: var(--theme-text-muted);
|
||
}
|
||
|
||
.toast__close {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--theme-text-muted);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.guideModal__dialog {
|
||
grid-template-columns: 1fr;
|
||
height: min(860px, calc(100dvh - 40px));
|
||
}
|
||
|
||
.guideModal__sidebar {
|
||
border-right: 0;
|
||
border-bottom: 1px solid var(--theme-border);
|
||
}
|
||
|
||
.guideModal__content {
|
||
grid-template-columns: 40px minmax(0, 1fr) 40px;
|
||
}
|
||
|
||
.appShell {
|
||
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr);
|
||
}
|
||
|
||
.rightRailBackdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: block;
|
||
border: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
z-index: 29;
|
||
}
|
||
|
||
.rightRail--overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
width: min(360px, calc(100vw - 20px));
|
||
height: 100dvh;
|
||
z-index: 30;
|
||
border-left: 1px solid var(--theme-border);
|
||
background: var(--theme-shell-bg);
|
||
box-shadow: -18px 0 36px rgba(0, 0, 0, 0.34);
|
||
}
|
||
|
||
|
||
.appShell--rightClosed .rightRail--overlay {
|
||
transform: translateX(calc(100% + 24px));
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.guideModal {
|
||
padding: 20px 12px;
|
||
}
|
||
|
||
.guideModal__dialog {
|
||
width: min(100%, calc(100vw - 24px));
|
||
height: min(100%, calc(100dvh - 24px));
|
||
}
|
||
|
||
.guideModal__sidebar {
|
||
gap: 14px;
|
||
padding: 20px 18px;
|
||
}
|
||
|
||
.guideModal__mobilePicker {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.guideModal__list {
|
||
display: none;
|
||
}
|
||
|
||
.guideModal__main {
|
||
padding: 20px 18px 18px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.guideModal__content {
|
||
grid-template-columns: 1fr;
|
||
min-height: 0;
|
||
}
|
||
|
||
.guideModal__arrow {
|
||
display: none;
|
||
}
|
||
|
||
.guideModal__body {
|
||
align-content: start;
|
||
overflow: auto;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
.guideModal__mediaPlaceholder {
|
||
border-radius: 22px;
|
||
}
|
||
|
||
.guideModal__stepTitle {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.guideModal__stepSummary {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.guideModal__footer {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.guideModal__next {
|
||
width: 100%;
|
||
}
|
||
|
||
.guideDockButton {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.appShell {
|
||
grid-template-columns: 1fr;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
align-content: start;
|
||
min-height: 100dvh;
|
||
}
|
||
|
||
.railHeader {
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.leftRail {
|
||
min-height: auto;
|
||
height: auto;
|
||
border-right: 0;
|
||
border-bottom: 1px solid var(--theme-border);
|
||
}
|
||
|
||
.leftRail__top {
|
||
display: none;
|
||
}
|
||
|
||
.leftRail__body {
|
||
max-height: none;
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.appUserCard {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.appUserCard__button {
|
||
padding: 8px 6px;
|
||
}
|
||
|
||
.appUserCard__meta {
|
||
max-width: none;
|
||
}
|
||
|
||
.appUserCard__navToggle {
|
||
display: inline-flex;
|
||
}
|
||
|
||
.workspaceHead .ghostIcon--iconOnly,
|
||
.rightRail__top .ghostIcon--iconOnly {
|
||
width: 42px;
|
||
height: 42px;
|
||
min-width: 42px;
|
||
border: 1px solid var(--theme-border);
|
||
border-radius: 14px;
|
||
background: var(--theme-surface-soft);
|
||
color: var(--theme-text-soft);
|
||
}
|
||
|
||
.rightRail--overlay {
|
||
inset: 0;
|
||
width: 100vw;
|
||
min-width: 0;
|
||
height: 100dvh;
|
||
min-height: 100dvh;
|
||
border-left: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.appMain {
|
||
min-height: auto;
|
||
border-left: 0;
|
||
border-right: 0;
|
||
}
|
||
|
||
.workspace,
|
||
.workspaceBody,
|
||
.workspaceBody--localRail {
|
||
min-height: 0;
|
||
height: auto;
|
||
}
|
||
|
||
.leftRail__content {
|
||
overflow: visible;
|
||
}
|
||
|
||
.leftRail__mobileMenu {
|
||
max-height: 540px;
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
overflow: hidden;
|
||
transition:
|
||
max-height 260ms ease,
|
||
opacity 220ms ease,
|
||
transform 220ms ease,
|
||
margin-top 220ms ease;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftRail__top {
|
||
display: none;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__meta,
|
||
.appShell--leftCollapsed .leftNav__label,
|
||
.appShell--leftCollapsed .searchStub__input {
|
||
display: revert;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__button,
|
||
.appShell--leftCollapsed .appUserCard__guest,
|
||
.appShell--leftCollapsed .searchStub,
|
||
.appShell--leftCollapsed .leftNav__item {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.appShell--leftCollapsed .appUserCard__button,
|
||
.appShell--leftCollapsed .appUserCard__guest {
|
||
padding: 10px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .searchStub {
|
||
padding: 11px 12px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .leftNav__item {
|
||
padding: 11px 12px;
|
||
}
|
||
|
||
.appShell--leftCollapsed .searchStub__iconButton {
|
||
width: auto;
|
||
}
|
||
|
||
.workspaceBody {
|
||
padding: 0 0 calc(28px + env(safe-area-inset-bottom));
|
||
border-radius: 0;
|
||
background: transparent;
|
||
margin: 14px 14px 0;
|
||
}
|
||
|
||
.workspaceBody--localRail {
|
||
padding: 0 0 calc(28px + env(safe-area-inset-bottom));
|
||
border-radius: 0;
|
||
background: transparent;
|
||
margin: 14px 14px 0;
|
||
}
|
||
|
||
.appShell--mobileNavClosed .leftRail__mobileMenu {
|
||
max-height: 0;
|
||
margin-top: -8px;
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.appShell--mobileNavClosed .leftRail__bottom {
|
||
display: none;
|
||
}
|
||
|
||
.rightRail--overlay .rightRail__body {
|
||
padding: 14px 20px calc(32px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.collapsedSearchModal {
|
||
padding: 72px 16px 16px;
|
||
}
|
||
|
||
.collapsedSearchBar {
|
||
width: min(100%, calc(100vw - 24px));
|
||
padding: 16px 18px;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.collapsedSearchBar__input {
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
</style>
|