사이드바 푸터 링크 줄바꿈·상단 네비 호버 행 전체 너비 (v0.0.98)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -191,19 +191,22 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="left-sidebar__footer flex shrink-0 items-center justify-between px-4 py-4 text-xs sm:px-5">
|
||||
<nav class="left-sidebar__footer-nav flex gap-4">
|
||||
<footer class="left-sidebar__footer flex shrink-0 flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-4 text-xs sm:px-5">
|
||||
<nav
|
||||
class="left-sidebar__footer-nav flex min-w-0 flex-1 flex-wrap items-center gap-x-4 gap-y-1"
|
||||
aria-label="하단 링크"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="item in navigation.footer"
|
||||
:key="item.id"
|
||||
class="site-interactive"
|
||||
class="left-sidebar__footer-link site-interactive shrink-0"
|
||||
:to="item.url"
|
||||
>
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
<button
|
||||
class="left-sidebar__theme-dot site-panel-hover site-interactive grid h-7 w-7 place-items-center rounded-full"
|
||||
class="left-sidebar__theme-dot site-panel-hover site-interactive grid h-7 w-7 shrink-0 place-items-center rounded-full"
|
||||
type="button"
|
||||
:aria-label="isDarkMode ? '라이트 모드로 전환' : '다크 모드로 전환'"
|
||||
:title="isDarkMode ? '라이트 모드' : '다크 모드'"
|
||||
|
||||
@@ -7,9 +7,26 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const expandedSet = inject('sidebarPrimaryNavExpandedSet')
|
||||
const toggleBranch = inject('sidebarPrimaryNavToggle')
|
||||
|
||||
/** 세로바·호버 시 원형으로 바뀌는 공통 before 스타일(리프 링크와 동일) */
|
||||
const navBarBeforeBase =
|
||||
"before:pointer-events-none before:content-[''] before:h-4 before:w-1 before:flex-none before:rounded-sm before:rounded-l-none before:transition-[width,height,border-radius,background-color] before:duration-200 hover:px-3 hover:before:h-2 hover:before:w-2 hover:before:rounded-full"
|
||||
|
||||
/** 비활성 경로: 기본 회색 막대, 호버 시 원형·믹스 색 */
|
||||
const navBarBeforeInactive =
|
||||
`${navBarBeforeBase} before:bg-[var(--site-line)] hover:before:bg-[color:color-mix(in_srgb,var(--site-text)_28%,var(--site-line))]`
|
||||
|
||||
/** 현재 페이지와 일치하는 내부 링크: 브랜드(`--site-accent`) 막대·호버 원형 */
|
||||
const navBarBeforeActive =
|
||||
`${navBarBeforeBase} before:bg-[var(--site-accent)] hover:before:bg-[var(--site-accent)]`
|
||||
|
||||
/** 행 공통: site-panel-hover, flex, 패딩 전환(가로 전체 호버 배경) */
|
||||
const navRowShell =
|
||||
'site-panel-hover flex w-full min-w-0 max-w-full items-center gap-2 rounded-[10px] py-1.5 pr-3 pl-0 leading-tight transition-[padding,background-color] duration-200'
|
||||
|
||||
/**
|
||||
* 노드가 펼쳐져 있는지
|
||||
* @param {string} id - 노드 id
|
||||
@@ -25,6 +42,53 @@ const isExpanded = (id) => expandedSet?.value?.has(String(id)) ?? true
|
||||
const onBranchClick = (id) => {
|
||||
toggleBranch(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 URL 여부
|
||||
* @param {string} raw - 네비 URL
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isExternalUrl = (raw) => /^https?:\/\//i.test(String(raw || '').trim())
|
||||
|
||||
/**
|
||||
* 내부 링크이고 현재 경로와 일치하는지(쿼리 무시, 끝 슬래시 정규화)
|
||||
* @param {string} raw - 네비 URL
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isInternalNavActive = (raw) => {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === '#' || !u.startsWith('/') || u.startsWith('//')) {
|
||||
return false
|
||||
}
|
||||
if (isExternalUrl(u)) {
|
||||
return false
|
||||
}
|
||||
const path = (route.path || '/').split('?')[0] || '/'
|
||||
/**
|
||||
* 경로 정규화
|
||||
* @param {string} s - 경로
|
||||
* @returns {string}
|
||||
*/
|
||||
const norm = (s) => {
|
||||
let x = s || '/'
|
||||
if (x.length > 1 && x.endsWith('/')) {
|
||||
x = x.slice(0, -1)
|
||||
}
|
||||
return x || '/'
|
||||
}
|
||||
return norm(path) === norm(u)
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프 `NuxtLink`용 클래스
|
||||
* @param {string} url - 노드 URL
|
||||
* @returns {string}
|
||||
*/
|
||||
const navLinkClass = (url) => {
|
||||
const active = isInternalNavActive(url)
|
||||
const bar = active ? navBarBeforeActive : navBarBeforeInactive
|
||||
return `sidebar-primary-nav-list__nav-link ${navRowShell} ${bar}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,12 +100,12 @@ const onBranchClick = (id) => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-primary-nav-list__branch-toggle site-panel-hover group flex w-full max-w-full items-center gap-2 rounded-[10px] py-1.5 pr-2 pl-0 text-left leading-tight text-[var(--site-text)] transition-[padding,background-color] duration-200 hover:px-3"
|
||||
class="sidebar-primary-nav-list__branch-toggle group flex w-full max-w-full text-left text-[var(--site-text)]"
|
||||
:class="`${navRowShell} ${navBarBeforeInactive}`"
|
||||
:aria-expanded="isExpanded(node.id)"
|
||||
:aria-label="isExpanded(node.id) ? `${node.label} 하위 메뉴 접기` : `${node.label} 하위 메뉴 펼치기`"
|
||||
@click="onBranchClick(node.id)"
|
||||
>
|
||||
<span class="sidebar-primary-nav-list__dot h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--site-muted)]" />
|
||||
<span class="sidebar-primary-nav-list__branch-label min-w-0 flex-1 truncate font-medium transition-transform duration-200 group-hover:translate-x-[3px]">{{ node.label }}</span>
|
||||
<span
|
||||
class="sidebar-primary-nav-list__chevron-wrap grid h-5 w-5 shrink-0 place-items-center text-[var(--site-muted)] transition-transform duration-200 ease-out group-hover:text-[var(--site-text)]"
|
||||
@@ -70,17 +134,18 @@ const onBranchClick = (id) => {
|
||||
>
|
||||
<NuxtLink
|
||||
v-if="node.url && String(node.url).trim() !== '' && String(node.url).trim() !== '#'"
|
||||
class="sidebar-primary-nav-list__nav-link site-panel-hover flex min-w-0 max-w-full flex-1 items-center gap-2 rounded-[10px] py-1.5 pr-3 pl-0 leading-tight transition-[padding,background-color] duration-200 before:h-4 before:w-1 before:flex-none before:rounded-sm before:rounded-l-none before:bg-[var(--site-line)] before:transition-[width,height,border-radius,background-color] before:duration-200 hover:px-3 hover:before:h-2 hover:before:w-2 hover:before:rounded-full hover:before:bg-[color:color-mix(in_srgb,var(--site-text)_28%,var(--site-line))]"
|
||||
:class="navLinkClass(node.url)"
|
||||
:to="node.url"
|
||||
:aria-current="isInternalNavActive(node.url) ? 'page' : undefined"
|
||||
>
|
||||
<span class="sidebar-primary-nav-list__label min-w-0 flex-1 truncate transition-transform duration-200 group-hover:translate-x-[3px]">{{ node.label }}</span>
|
||||
</NuxtLink>
|
||||
<span
|
||||
v-else
|
||||
class="sidebar-primary-nav-list__leaf-static flex min-w-0 max-w-full flex-1 items-center gap-2 rounded-[10px] py-1.5 pr-3 pl-2 text-[var(--site-text)]"
|
||||
class="sidebar-primary-nav-list__leaf-static group text-[var(--site-text)]"
|
||||
:class="`${navRowShell} ${navBarBeforeInactive}`"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--site-muted)]" />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ node.label }}</span>
|
||||
<span class="min-w-0 flex-1 truncate font-medium transition-transform duration-200 group-hover:translate-x-[3px]">{{ node.label }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user