diff --git a/assets/css/main.css b/assets/css/main.css index 8005a4c..49650ac 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -60,6 +60,10 @@ overflow: hidden; } + html.site-search-open { + overflow: hidden; + } + body { min-width: 320px; margin: 0; @@ -75,6 +79,18 @@ } @layer components { + @keyframes site-search-modal-in { + from { + opacity: 0; + transform: translateY(-6px) scale(0.99); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + .site-shell { display: flex; flex-direction: column; @@ -96,6 +112,10 @@ @apply px-6 py-4; } + .site-search-modal__panel--animate { + animation: site-search-modal-in 0.18s ease-out; + } + .post-prose { @apply max-w-none text-[17px] leading-8; color: var(--site-text); diff --git a/components/site/SiteHeader.vue b/components/site/SiteHeader.vue index 0c09baa..1cc58e7 100644 --- a/components/site/SiteHeader.vue +++ b/components/site/SiteHeader.vue @@ -3,6 +3,7 @@ const { menuOpen, toggleMenu, closeMenu } = useMenuState() const menuUserOpen = ref(false) const userMenuRef = ref(null) const userMenuToggleRef = ref(null) +const searchOpen = ref(false) const { data: siteSettings } = await useFetch('/api/site-settings', { default: () => ({ @@ -18,6 +19,37 @@ const closeUserMenu = () => { menuUserOpen.value = false } +/** + * 통합 검색 모달을 연다. + * @returns {void} + */ +const openSearchModal = () => { + searchOpen.value = true +} + +/** + * 입력 필드에 포커스가 있으면 `/` 검색 단축키를 무시한다. + * @param {KeyboardEvent} event - 키보드 이벤트 + * @returns {boolean} 무시할 때 true + */ +const shouldIgnoreSearchHotkey = (event) => { + if (event.ctrlKey || event.metaKey || event.altKey) { + return true + } + const target = event.target + if (!(target instanceof HTMLElement)) { + return false + } + const tag = target.tagName + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') { + return true + } + if (target.isContentEditable) { + return true + } + return false +} + /** * 사용자 메뉴를 토글한다. * @returns {void} @@ -47,24 +79,37 @@ const onDocumentClick = (event) => { } /** - * Escape로 열린 패널을 닫는다(사용자 메뉴 우선, 이어서 모바일 좌측 메뉴). + * Escape·`/` 키로 패널을 제어한다(검색 모달 → 사용자 메뉴 → 모바일 좌측 메뉴, `/`는 검색 열기). * @param {KeyboardEvent} event - 키보드 이벤트 * @returns {void} */ const onGlobalKeydown = (event) => { - if (event.key !== 'Escape') { + if (event.key === 'Escape') { + if (searchOpen.value) { + searchOpen.value = false + event.preventDefault() + return + } + if (menuUserOpen.value) { + closeUserMenu() + return + } + if (!menuOpen.value) { + return + } + if (typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) { + closeMenu() + } return } - if (menuUserOpen.value) { - closeUserMenu() + if (event.key !== '/') { return } - if (!menuOpen.value) { + if (shouldIgnoreSearchHotkey(event)) { return } - if (typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) { - closeMenu() - } + event.preventDefault() + openSearchModal() } onMounted(() => { @@ -114,11 +159,16 @@ onBeforeUnmount(() => { {{ siteSettings.title }} -
+ ++ 검색어를 입력하면 태그와 게시물이 섹션별로 표시됩니다. +
+ + ++ 일치하는 결과가 없습니다. +
+ + +