v0.0.55: 모바일 슬라이드 메뉴·우측 사이드 하단 배치
lg 미만에서 좌측 내비를 오버레이 슬라이드로 전환하고, 본문 아래에 우측 사이드를 두며 헤더·패널 여백을 보정했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,10 @@
|
||||
background: var(--site-bg);
|
||||
}
|
||||
|
||||
html.site-mobile-nav-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
margin: 0;
|
||||
|
||||
@@ -22,8 +22,11 @@ const { data: navigation } = await useFetch('/api/navigation', {
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="left-sidebar site-sidebar hidden overflow-hidden border-r border-[var(--site-line)] transition-[width,opacity,border-color] duration-300 ease-out lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:self-start lg:flex lg:flex-col"
|
||||
:class="menuOpen ? 'w-[287px] opacity-100' : 'w-0 opacity-0 border-transparent'"
|
||||
id="menu"
|
||||
class="left-sidebar site-sidebar flex flex-col overflow-hidden border-r border-[var(--site-line)] transition-[width,opacity,transform,border-color] duration-300 ease-out max-lg:fixed max-lg:left-0 max-lg:top-[57px] max-lg:z-[60] max-lg:h-[calc(100dvh-57px)] max-lg:max-h-[calc(100dvh-57px)] max-lg:w-[min(287px,calc(100vw-24px))] max-lg:shadow-[0_16px_48px_rgba(0,0,0,0.18)] lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:self-start"
|
||||
:class="menuOpen
|
||||
? 'max-lg:translate-x-0 max-lg:pointer-events-auto lg:w-[287px] lg:opacity-100'
|
||||
: 'max-lg:-translate-x-full max-lg:pointer-events-none lg:w-0 lg:opacity-0 lg:border-transparent'"
|
||||
>
|
||||
<div class="left-sidebar__scroll site-sidebar-scroll min-h-0 flex-1">
|
||||
<div class="left-sidebar__block site-sidebar-section py-3 pl-4 pr-3 sm:pl-5 xl:pl-0">
|
||||
|
||||
@@ -19,9 +19,9 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="right-sidebar site-sidebar hidden w-[287px] overflow-hidden border-l border-[var(--site-line)] lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:self-start lg:flex lg:flex-col">
|
||||
<aside class="right-sidebar site-sidebar flex w-full flex-col overflow-hidden border-[var(--site-line)] max-lg:border-l-0 max-lg:border-t max-lg:px-4 max-lg:pb-10 lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:w-[287px] lg:self-start lg:border-l lg:border-t-0 lg:px-0">
|
||||
<div class="right-sidebar__scroll site-sidebar-scroll min-h-0 flex-1">
|
||||
<div class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0">
|
||||
<div class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0 max-lg:px-0">
|
||||
<div class="right-sidebar__profile flex items-center gap-3">
|
||||
<div class="right-sidebar__logo grid h-12 w-12 place-items-center rounded-2xl bg-[var(--site-invert)] text-2xl font-bold text-[var(--site-invert-text)]">
|
||||
{{ siteSettings.logoText }}
|
||||
@@ -35,9 +35,9 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form class="right-sidebar__subscribe mt-4 flex gap-2">
|
||||
<input class="right-sidebar__input min-w-0 flex-1 rounded-lg px-3 py-2 text-sm site-input" placeholder="Your email">
|
||||
<button class="right-sidebar__button rounded-lg px-4 py-2 text-sm font-semibold site-button" type="button">
|
||||
<form class="right-sidebar__subscribe mt-4 flex flex-col gap-2 sm:flex-row sm:items-stretch">
|
||||
<input class="right-sidebar__input min-w-0 w-full flex-1 rounded-lg px-3 py-2 text-sm site-input sm:min-w-0" placeholder="Your email">
|
||||
<button class="right-sidebar__button shrink-0 rounded-lg px-4 py-2 text-sm font-semibold site-button sm:self-auto" type="button">
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
@@ -179,7 +179,7 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="right-sidebar__footer shrink-0 py-4 pl-5 pr-0 text-xs site-muted">
|
||||
<footer class="right-sidebar__footer shrink-0 py-4 pl-5 pr-0 text-xs site-muted max-lg:px-0">
|
||||
{{ siteSettings.copyrightText }}
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
const { menuOpen, toggleMenu } = useMenuState()
|
||||
const { menuOpen, toggleMenu, closeMenu } = useMenuState()
|
||||
const menuUserOpen = ref(false)
|
||||
const userMenuRef = ref(null)
|
||||
const userMenuToggleRef = ref(null)
|
||||
@@ -46,19 +46,42 @@ const onDocumentClick = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape로 열린 패널을 닫는다(사용자 메뉴 우선, 이어서 모바일 좌측 메뉴).
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGlobalKeydown = (event) => {
|
||||
if (event.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
if (menuUserOpen.value) {
|
||||
closeUserMenu()
|
||||
return
|
||||
}
|
||||
if (!menuOpen.value) {
|
||||
return
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
document.addEventListener('keydown', onGlobalKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
document.removeEventListener('keydown', onGlobalKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="site-header sticky top-0 z-20 backdrop-blur">
|
||||
<div class="site-header__inner mx-auto flex h-full max-w-[1294px] items-center justify-between px-4 lg:px-0">
|
||||
<NuxtLink class="site-header__brand flex items-center gap-2 text-[18px] font-semibold tracking-normal" to="/">
|
||||
<div class="site-header__inner mx-auto flex h-full max-w-[1294px] items-center justify-between gap-2 px-4 sm:gap-3 lg:px-0">
|
||||
<NuxtLink class="site-header__brand flex min-w-0 flex-1 items-center gap-2 text-[15px] font-semibold tracking-normal sm:text-[18px]" to="/">
|
||||
<button
|
||||
class="site-header__menu-toggle group flex h-7 w-7 items-center justify-center rounded-full transition-transform"
|
||||
type="button"
|
||||
@@ -89,15 +112,15 @@ onBeforeUnmount(() => {
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
{{ siteSettings.title }}
|
||||
<span class="min-w-0 truncate">{{ siteSettings.title }}</span>
|
||||
</NuxtLink>
|
||||
<div class="site-header__search hidden h-9 w-[470px] items-center rounded-lg px-3 text-sm md:flex site-input">
|
||||
<span class="site-header__search-icon mr-2 text-lg leading-none">⌕</span>
|
||||
<span class="site-header__search-text site-soft">Search</span>
|
||||
<span class="site-header__search-key ml-auto rounded-md px-2 text-xs site-soft site-input">/</span>
|
||||
</div>
|
||||
<nav class="site-header__nav flex items-center gap-3 text-sm">
|
||||
<NuxtLink class="site-header__buy site-accent-button rounded-lg px-4 py-2 font-semibold" to="/pages/about">
|
||||
<nav class="site-header__nav flex shrink-0 items-center gap-2 text-sm sm:gap-3">
|
||||
<NuxtLink class="site-header__buy site-accent-button shrink-0 rounded-lg px-3 py-1.5 text-xs font-semibold sm:px-4 sm:py-2 sm:text-sm" to="/pages/about">
|
||||
Subscribe
|
||||
</NuxtLink>
|
||||
<div class="site-header__user-menu relative">
|
||||
@@ -127,7 +150,7 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
v-if="menuUserOpen"
|
||||
ref="userMenuRef"
|
||||
class="site-header__user-dropdown absolute top-12 right-0 z-30 flex min-w-[200px] max-w-xs flex-col overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-3 pb-2 text-sm font-medium shadow-[0_12px_30px_rgba(0,0,0,0.12)]"
|
||||
class="site-header__user-dropdown absolute top-12 right-2 z-30 flex min-w-[200px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-3 pb-2 text-sm font-medium shadow-[0_12px_30px_rgba(0,0,0,0.12)] sm:right-0 sm:max-w-xs"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2 border-b border-[var(--site-line)] pb-3">
|
||||
<div class="site-header__avatar-wrap flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-[var(--site-panel)] md:h-10 md:w-10">
|
||||
|
||||
@@ -2,7 +2,7 @@ const menuStorageKey = 'MENU_STATE'
|
||||
|
||||
/**
|
||||
* 좌측 메뉴 열림 상태 관리
|
||||
* @returns {{menuOpen: import('vue').Ref<boolean>, toggleMenu: Function}} 메뉴 상태와 토글 함수
|
||||
* @returns {{menuOpen: import('vue').Ref<boolean>, toggleMenu: Function, closeMenu: Function}} 메뉴 상태와 토글·닫기
|
||||
*/
|
||||
export const useMenuState = () => {
|
||||
const menuOpen = useState('site-menu-open', () => true)
|
||||
@@ -24,8 +24,21 @@ export const useMenuState = () => {
|
||||
localStorage.setItem(menuStorageKey, menuOpen.value ? 'open' : 'closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* 좌측 메뉴를 닫는다(모바일 오버레이·백드롭에서 사용).
|
||||
* @returns {void}
|
||||
*/
|
||||
const closeMenu = () => {
|
||||
if (!menuOpen.value) {
|
||||
return
|
||||
}
|
||||
menuOpen.value = false
|
||||
localStorage.setItem(menuStorageKey, 'closed')
|
||||
}
|
||||
|
||||
return {
|
||||
menuOpen,
|
||||
toggleMenu
|
||||
toggleMenu,
|
||||
closeMenu
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-11 v0.0.55
|
||||
|
||||
### 공개 레이아웃 모바일 분기
|
||||
|
||||
`lg` 미만에서는 3열 그리드 대신 세로 흐름으로 본문을 먼저 보여 주고, 오른쪽 사이드는 본문 아래로 내린다. 왼쪽 내비는 화면 폭을 줄였을 때 본문을 밀어내지 않도록 고정 슬라이드 패널로 띄우고, 백드롭 클릭·Escape·헤더 토글로 닫을 수 있게 했다. 데스크톱에서는 기존처럼 그리드 3열과 스티키 사이드바를 유지한다.
|
||||
|
||||
## 2026-05-11 v0.0.54
|
||||
|
||||
### 공개 인증 화면 가독성과 입력 피드백 보정
|
||||
|
||||
10
docs/map.md
10
docs/map.md
@@ -6,8 +6,8 @@
|
||||
|
||||
| 파일 | 화면 |
|
||||
|------|------|
|
||||
| layouts/default.vue | 메인·목록·태그 — 문서 스크롤 + 스티키 사이드, 사이드 내부는 무스크롤바 스크롤 |
|
||||
| layouts/post.vue | 개별 게시물 — 동일 |
|
||||
| layouts/default.vue | 메인·목록·태그 — 문서 스크롤 + 스티키 사이드(`lg+`), 모바일은 본문→우측 사이드·좌측 슬라이드 메뉴 |
|
||||
| layouts/post.vue | 개별 게시물 — `default`와 동일한 반응형 셸 |
|
||||
| layouts/admin.vue | 관리자 전체, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금 |
|
||||
| layouts/page.vue | 고정 페이지 전체 화면 |
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
| 파일 | 화면 위치 |
|
||||
|------|-----------|
|
||||
| components/site/SiteHeader.vue | 모든 공개 페이지 상단, 우측 사용자 아바타 드롭다운(Anonymous/Sign up/Sign in) |
|
||||
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `sticky`+`h/max-h: calc(100vh-57px)`+내부 무스크롤바 스크롤, 하단 푸터 고정 |
|
||||
| components/site/RightSidebar.vue | 오른쪽 사이드바, 동일 패턴(고정 열 높이), 카피라이트 하단 고정 |
|
||||
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+`는 `sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널 |
|
||||
| components/site/RightSidebar.vue | 오른쪽 사이드바, `lg+`는 고정 열 높이·스티키, 모바일은 본문 아래 전체 너비 |
|
||||
| components/site/MainColumn.vue | 메인 화면 중앙 |
|
||||
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일, 카드 hover 인터랙션 |
|
||||
| components/site/TagHeader.vue | 태그 페이지 헤더 |
|
||||
@@ -169,7 +169,7 @@
|
||||
| nuxt.config.js | Nuxt 앱 설정, Tailwind 모듈 연결, 관리자 QA를 위한 개발 도구 비활성화 |
|
||||
| tailwind.config.js | Tailwind 테마 설정 |
|
||||
| assets/css/main.css | 전역 스타일, `#fcfcfc` 단일 배경 기준, 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과 |
|
||||
| composables/useMenuState.js | 좌측 메뉴 열림 상태 관리 |
|
||||
| composables/useMenuState.js | 좌측 메뉴 열림 상태·`closeMenu`(모바일 백드롭 등) |
|
||||
| composables/useThemeMode.js | 사용자 화면 라이트/다크 테마 상태 관리 |
|
||||
| middleware/admin-auth.global.js | 관리자 페이지 접근 인증 |
|
||||
| scripts/dev-server.js | 로컬 개발 서버 링크 요약 출력 |
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
| Header | 높이 57px, `sticky top-0`, `shrink-0` |
|
||||
| Shell | `min-height: 100vh`, `flex` 세로 컬럼 |
|
||||
| 그리드(데스크톱 `lg+`) | `items-start`, 본문(중앙) 높이에 맞춰 행이 늘어남 — **문서(`html`/`body`) 스크롤**로 긴 본문 처리(스크롤바는 브라우저 오른쪽) |
|
||||
| 그리드(모바일 `lg` 미만) | 단일 세로 흐름: **본문 → 오른쪽 사이드** 순. 왼쪽 사이드는 레이아웃 흐름에서 분리된 고정 슬라이드 패널로 표시 |
|
||||
| Left Aside | 너비 287px, `sticky top-[57px]`, `h-[calc(100vh-57px)]`와 `max-h` 동일(뷰포트 기준 고정 높이), 내부 상단은 `.site-sidebar-scroll`(스크롤바 숨김), 하단 푸터 `shrink-0`·상단 보더로 스크롤 영역과 구분 |
|
||||
| Left Aside(모바일) | `fixed` 좌측 패널, 열림 시 `translate-x-0`, 닫힘 시 화면 밖으로 이동. 열린 동안 백드롭과 `html.site-mobile-nav-open`으로 문서 스크롤 잠금 |
|
||||
| Main | 너비 720px, 별도 `overflow-y` 없음 — 뷰포트와 동일한 문서 스크롤에 포함 |
|
||||
| Right Aside | Left와 동일 패턴(스티키·고정 높이·내부 무스크롤바 스크롤·하단 카피라이트) |
|
||||
| Right Aside(모바일) | 본문 아래에 전체 너비로 배치, 상단 보더로 본문과 구분, 좌우 안전 여백(`px-4`) 적용 |
|
||||
|
||||
### 메뉴 토글
|
||||
|
||||
@@ -30,6 +33,8 @@
|
||||
- 메뉴 상태는 Nuxt/Vue 상태로 관리
|
||||
- 브라우저에서는 `localStorage.MENU_STATE`에 `open` 또는 `closed` 저장
|
||||
- 닫힘 상태에서는 왼쪽 사이드바 폭을 0으로 줄이는 전환 애니메이션을 적용
|
||||
- `lg` 미만에서는 왼쪽 사이드바를 화면 좌측 고정 슬라이드 패널로 표시하고, 열린 동안 백드롭을 탭하면 `closeMenu`로 닫는다.
|
||||
- `Escape` 키는 사용자 드롭다운이 열려 있으면 우선 닫고, 그렇지 않으면 모바일에서만 좌측 슬라이드 메뉴를 닫는다.
|
||||
- 헤더 우측 사용자 아이콘 버튼은 비로그인 기준 사용자 메뉴(Anonymous, Sign up, Sign in)만 표시한다.
|
||||
|
||||
### 공개 화면 색상
|
||||
@@ -512,6 +517,6 @@ APP_PORT=43118
|
||||
|
||||
## 버전 관리
|
||||
|
||||
- 현재 버전: v0.0.54
|
||||
- 현재 버전: v0.0.55
|
||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
## 프론트엔드 개발
|
||||
|
||||
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정
|
||||
- [ ] Thred 참고 화면 기준 시각 QA
|
||||
- [ ] 사이드바 토글 애니메이션 세부 조정
|
||||
- [ ] 사용자 화면 테마 전환 초기 로드 깜빡임(FART) 최소화
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v0.0.55
|
||||
|
||||
- 모바일(`lg` 미만)에서 좌측 사이드바를 고정 슬라이드 패널+백드롭으로 표시하고, 닫기는 `closeMenu`/백드롭/Escape로 처리.
|
||||
- 모바일에서 본문 아래에 오른쪽 사이드바를 배치하고, 좁은 폭에서 헤더·우측 패널이 화면 가장자리에 붙지 않도록 여백·구독 폼 레이아웃을 보정.
|
||||
- 좌측 메뉴 열림 시 모바일에서 문서 스크롤 잠금(`html.site-mobile-nav-open`) 적용.
|
||||
- 데스크톱에서만 메뉴 닫힘 시 `max-width`가 줄어들도록 보정(`lg:max-w-[1007px]`).
|
||||
|
||||
## v0.0.54
|
||||
|
||||
- 회원가입 화면(`signup`)을 모바일 우선 여백·카드 패널 구조로 보정해 가독성 개선.
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
<script setup>
|
||||
const { menuOpen } = useMenuState()
|
||||
const { menuOpen, closeMenu } = useMenuState()
|
||||
|
||||
/**
|
||||
* 모바일에서 좌측 슬라이드 메뉴가 열려 있을 때 문서 스크롤을 잠근다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const syncMobileNavScrollLock = () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const isNarrow = window.matchMedia('(max-width: 1023px)').matches
|
||||
document.documentElement.classList.toggle('site-mobile-nav-open', Boolean(menuOpen.value && isNarrow))
|
||||
}
|
||||
|
||||
watch(menuOpen, syncMobileNavScrollLock)
|
||||
|
||||
onMounted(() => {
|
||||
syncMobileNavScrollLock()
|
||||
window.addEventListener('resize', syncMobileNavScrollLock)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncMobileNavScrollLock)
|
||||
if (import.meta.client) {
|
||||
document.documentElement.classList.remove('site-mobile-nav-open')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-shell public-layout">
|
||||
<SiteHeader class="shrink-0" />
|
||||
<div
|
||||
class="public-layout__grid mx-auto grid w-full max-w-[1294px] flex-1 grid-cols-1 bg-[var(--site-bg)] px-4 transition-[grid-template-columns,max-width] duration-300 ease-out lg:grid lg:items-start lg:px-0 lg:[grid-template-columns:287px_minmax(0,720px)_287px]"
|
||||
:class="menuOpen ? '' : 'max-w-[1007px] lg:[grid-template-columns:0_minmax(0,720px)_287px]'"
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200 ease-out"
|
||||
leave-active-class="transition-opacity duration-200 ease-in"
|
||||
enter-from-class="opacity-0"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-show="menuOpen"
|
||||
class="public-layout__nav-backdrop fixed inset-x-0 top-[57px] bottom-0 z-40 bg-black/35 backdrop-blur-[2px] lg:hidden"
|
||||
aria-hidden="true"
|
||||
@click="closeMenu"
|
||||
/>
|
||||
</Transition>
|
||||
<div
|
||||
class="public-layout__grid mx-auto flex w-full max-w-[1294px] flex-1 flex-col bg-[var(--site-bg)] px-4 lg:grid lg:grid-cols-[287px_minmax(0,720px)_287px] lg:items-start lg:px-0 lg:transition-[grid-template-columns,max-width] lg:duration-300 lg:ease-out"
|
||||
:class="menuOpen ? '' : 'lg:max-w-[1007px] lg:[grid-template-columns:0_minmax(0,720px)_287px]'"
|
||||
>
|
||||
<LeftSidebar :menu-open="menuOpen" />
|
||||
<main
|
||||
class="site-main w-full overflow-x-hidden lg:w-[720px]"
|
||||
class="site-main min-w-0 w-full overflow-x-hidden lg:col-start-2 lg:row-start-1 lg:w-[720px]"
|
||||
:class="{ 'site-main--menu-closed': !menuOpen }"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
<RightSidebar />
|
||||
<RightSidebar class="lg:col-start-3 lg:row-start-1" />
|
||||
<LeftSidebar :menu-open="menuOpen" class="lg:col-start-1 lg:row-start-1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
<script setup>
|
||||
const { menuOpen } = useMenuState()
|
||||
const { menuOpen, closeMenu } = useMenuState()
|
||||
|
||||
/**
|
||||
* 모바일에서 좌측 슬라이드 메뉴가 열려 있을 때 문서 스크롤을 잠근다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const syncMobileNavScrollLock = () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const isNarrow = window.matchMedia('(max-width: 1023px)').matches
|
||||
document.documentElement.classList.toggle('site-mobile-nav-open', Boolean(menuOpen.value && isNarrow))
|
||||
}
|
||||
|
||||
watch(menuOpen, syncMobileNavScrollLock)
|
||||
|
||||
onMounted(() => {
|
||||
syncMobileNavScrollLock()
|
||||
window.addEventListener('resize', syncMobileNavScrollLock)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncMobileNavScrollLock)
|
||||
if (import.meta.client) {
|
||||
document.documentElement.classList.remove('site-mobile-nav-open')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-shell post-layout">
|
||||
<SiteHeader class="shrink-0" />
|
||||
<div
|
||||
class="post-layout__grid mx-auto grid w-full max-w-[1294px] flex-1 grid-cols-1 bg-[var(--site-bg)] px-4 transition-[grid-template-columns,max-width] duration-300 ease-out lg:grid lg:items-start lg:px-0 lg:[grid-template-columns:287px_minmax(0,720px)_287px]"
|
||||
:class="menuOpen ? '' : 'max-w-[1007px] lg:[grid-template-columns:0_minmax(0,720px)_287px]'"
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200 ease-out"
|
||||
leave-active-class="transition-opacity duration-200 ease-in"
|
||||
enter-from-class="opacity-0"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-show="menuOpen"
|
||||
class="post-layout__nav-backdrop fixed inset-x-0 top-[57px] bottom-0 z-40 bg-black/35 backdrop-blur-[2px] lg:hidden"
|
||||
aria-hidden="true"
|
||||
@click="closeMenu"
|
||||
/>
|
||||
</Transition>
|
||||
<div
|
||||
class="post-layout__grid mx-auto flex w-full max-w-[1294px] flex-1 flex-col bg-[var(--site-bg)] px-4 lg:grid lg:grid-cols-[287px_minmax(0,720px)_287px] lg:items-start lg:px-0 lg:transition-[grid-template-columns,max-width] lg:duration-300 lg:ease-out"
|
||||
:class="menuOpen ? '' : 'lg:max-w-[1007px] lg:[grid-template-columns:0_minmax(0,720px)_287px]'"
|
||||
>
|
||||
<LeftSidebar :menu-open="menuOpen" />
|
||||
<main
|
||||
class="site-main w-full overflow-x-hidden lg:w-[720px]"
|
||||
class="site-main min-w-0 w-full overflow-x-hidden lg:col-start-2 lg:row-start-1 lg:w-[720px]"
|
||||
:class="{ 'site-main--menu-closed': !menuOpen }"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
<RightSidebar />
|
||||
<RightSidebar class="lg:col-start-3 lg:row-start-1" />
|
||||
<LeftSidebar :menu-open="menuOpen" class="lg:col-start-1 lg:row-start-1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.54",
|
||||
"version": "0.0.55",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user