first commit

This commit is contained in:
Inshal
2024-05-29 22:34:28 +05:00
commit e63fc41a20
1470 changed files with 174828 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<script setup>
import {
HorizontalNavGroup,
HorizontalNavLink,
} from '@layouts/components'
const props = defineProps({
navItems: {
type: null,
required: true,
},
})
const resolveNavItemComponent = item => {
if ('children' in item)
return HorizontalNavGroup
return HorizontalNavLink
}
</script>
<template>
<ul class="nav-items">
<Component
:is="resolveNavItemComponent(item)"
v-for="(item, index) in navItems"
:key="index"
:item="item"
/>
</ul>
</template>
<style lang="scss">
.layout-wrapper.layout-nav-type-horizontal {
.nav-items {
display: flex;
flex-wrap: wrap;
}
}
</style>

View File

@@ -0,0 +1,117 @@
<script setup>
import { layoutConfig } from '@layouts'
import {
HorizontalNavLink,
HorizontalNavPopper,
} from '@layouts/components'
import { canViewNavMenuGroup } from '@layouts/plugins/casl'
import { useLayoutConfigStore } from '@layouts/stores/config'
import {
getDynamicI18nProps,
isNavGroupActive,
} from '@layouts/utils'
const props = defineProps({
item: {
type: null,
required: true,
},
childrenAtEnd: {
type: Boolean,
required: false,
default: false,
},
isSubItem: {
type: Boolean,
required: false,
default: false,
},
})
defineOptions({
name: 'HorizontalNavGroup',
})
const route = useRoute()
const router = useRouter()
const configStore = useLayoutConfigStore()
const isGroupActive = ref(false)
/*Watch for route changes, more specifically route path. Do note that this won't trigger if route's query is updated.
updates isActive & isOpen based on active state of group.
*/
watch(() => route.path, () => {
const isActive = isNavGroupActive(props.item.children, router)
isGroupActive.value = isActive
}, { immediate: true })
</script>
<template>
<HorizontalNavPopper
v-if="canViewNavMenuGroup(item)"
:is-rtl="configStore.isAppRTL"
class="nav-group"
tag="li"
content-container-tag="ul"
:class="[{
'active': isGroupActive,
'children-at-end': childrenAtEnd,
'sub-item': isSubItem,
'disabled': item.disable,
}]"
:popper-inline-end="childrenAtEnd"
>
<div class="nav-group-label">
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
class="nav-item-icon"
v-bind="item.icon || layoutConfig.verticalNav.defaultNavItemIconProps"
/>
<Component
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
v-bind="getDynamicI18nProps(item.title, 'span')"
class="nav-item-title"
>
{{ item.title }}
</Component>
<Component
v-bind="layoutConfig.icons.chevronDown"
:is="layoutConfig.app.iconRenderer || 'div'"
class="nav-group-arrow"
/>
</div>
<template #content>
<Component
:is="'children' in child ? 'HorizontalNavGroup' : HorizontalNavLink"
v-for="child in item.children"
:key="child.title"
:item="child"
children-at-end
is-sub-item
/>
</template>
</HorizontalNavPopper>
</template>
<style lang="scss">
.layout-horizontal-nav {
.nav-group {
.nav-group-label {
display: flex;
align-items: center;
cursor: pointer;
}
.popper-content {
z-index: 1;
> div {
overflow: hidden auto;
}
}
}
}
</style>

View File

@@ -0,0 +1,153 @@
<script setup>
import { HorizontalNav } from '@layouts/components'
// Using import from `@layouts` causing build to hangup
// import { useLayouts } from '@layouts'
import { useLayoutConfigStore } from '@layouts/stores/config'
const props = defineProps({
navItems: {
type: null,
required: true,
},
})
const configStore = useLayoutConfigStore()
</script>
<template>
<div
class="layout-wrapper"
:class="configStore._layoutClasses"
>
<div
class="layout-navbar-and-nav-container"
:class="configStore.isNavbarBlurEnabled && 'header-blur'"
>
<!-- 👉 Navbar -->
<div class="layout-navbar">
<div class="navbar-content-container">
<slot name="navbar" />
</div>
</div>
<!-- 👉 Navigation -->
<div class="layout-horizontal-nav">
<div class="horizontal-nav-content-container">
<HorizontalNav :nav-items="navItems" />
</div>
</div>
</div>
<main class="layout-page-content">
<slot />
</main>
<!-- 👉 Footer -->
<footer class="layout-footer">
<div class="footer-content-container">
<slot name="footer" />
</div>
</footer>
</div>
</template>
<style lang="scss">
@use "@configured-variables" as variables;
@use "@layouts/styles/placeholders";
@use "@layouts/styles/mixins";
.layout-wrapper {
&.layout-nav-type-horizontal {
display: flex;
flex-direction: column;
// // TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
// min-height: 100%;
min-block-size: 100dvh;
.layout-navbar-and-nav-container {
z-index: 1;
}
.layout-navbar {
z-index: variables.$layout-horizontal-nav-layout-navbar-z-index;
block-size: variables.$layout-horizontal-nav-navbar-height;
// For now we are not independently managing navbar and horizontal nav so we won't use below style to avoid conflicting with combo style of navbar and horizontal nav
// If we add independent style of navbar & horizontal nav then we have to add :not for avoiding conflict with combo styles
// .layout-navbar-sticky & {
// @extend %layout-navbar-sticky;
// }
// For now we are not independently managing navbar and horizontal nav so we won't use below style to avoid conflicting with combo style of navbar and horizontal nav
// If we add independent style of navbar & horizontal nav then we have to add :not for avoiding conflict with combo styles
// .layout-navbar-hidden & {
// @extend %layout-navbar-hidden;
// }
}
// 👉 Navbar
.navbar-content-container {
@include mixins.boxed-content;
}
// 👉 Content height fixed
&.layout-content-height-fixed {
max-block-size: 100dvh;
.layout-page-content {
overflow: hidden;
> :first-child {
max-block-size: 100%;
overflow-y: auto;
}
}
}
// 👉 Footer
// Boxed content
.layout-footer {
.footer-content-container {
@include mixins.boxed-content;
}
}
}
// If both navbar & horizontal nav sticky
&.layout-navbar-sticky.horizontal-nav-sticky {
.layout-navbar-and-nav-container {
position: sticky;
inset-block-start: 0;
will-change: transform;
}
}
&.layout-navbar-hidden.horizontal-nav-hidden {
.layout-navbar-and-nav-container {
display: none;
}
}
}
// 👉 Horizontal nav nav
.layout-horizontal-nav {
z-index: variables.$layout-horizontal-nav-z-index;
// .horizontal-nav-sticky & {
// width: 100%;
// will-change: transform;
// position: sticky;
// top: 0;
// }
// .horizontal-nav-hidden & {
// display: none;
// }
.horizontal-nav-content-container {
@include mixins.boxed-content(true);
}
}
</style>

View File

@@ -0,0 +1,60 @@
<script setup>
import { layoutConfig } from '@layouts'
import { can } from '@layouts/plugins/casl'
import {
getComputedNavLinkToProp,
getDynamicI18nProps,
isNavLinkActive,
} from '@layouts/utils'
const props = defineProps({
item: {
type: null,
required: true,
},
isSubItem: {
type: Boolean,
required: false,
default: false,
},
})
</script>
<template>
<li
v-if="can(item.action, item.subject)"
class="nav-link"
:class="[{
'sub-item': props.isSubItem,
'disabled': item.disable,
}]"
>
<Component
:is="item.to ? 'RouterLink' : 'a'"
v-bind="getComputedNavLinkToProp(item)"
:class="{ 'router-link-active router-link-exact-active': isNavLinkActive(item, $router) }"
>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
class="nav-item-icon"
v-bind="item.icon || layoutConfig.verticalNav.defaultNavItemIconProps"
/>
<Component
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
class="nav-item-title"
v-bind="getDynamicI18nProps(item.title, 'span')"
>
{{ item.title }}
</Component>
</Component>
</li>
</template>
<style lang="scss">
.layout-horizontal-nav {
.nav-link a {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,208 @@
<script setup>
import {
computePosition,
flip,
offset,
shift,
} from '@floating-ui/dom'
import { useLayoutConfigStore } from '@layouts/stores/config'
import { themeConfig } from '@themeConfig'
const props = defineProps({
popperInlineEnd: {
type: Boolean,
required: false,
default: false,
},
tag: {
type: String,
required: false,
default: 'div',
},
contentContainerTag: {
type: String,
required: false,
default: 'div',
},
isRtl: {
type: Boolean,
required: false,
},
})
const configStore = useLayoutConfigStore()
const refPopperContainer = ref()
const refPopper = ref()
const popperContentStyles = ref({
left: '0px',
top: '0px',
/* Why we are not using fixed positioning?
`position: fixed` doesn't work as expected when some CSS properties like `transform` is applied on its parent element.
Docs: https://developer.mozilla.org/en-US/docs/Web/CSS/position#values <= See `fixed` value description
Hence, when we use transitions where transition apply `transform` on its parent element, fixed positioning will not work.
(Popper content moves away from the element when parent element transition)
To avoid this, we use `position: absolute` instead of `position: fixed`.
NOTE: This issue starts from third level children (Top Level > Sub item > Sub item).
*/
// strategy: 'fixed',
})
const updatePopper = async () => {
if (refPopperContainer.value !== undefined && refPopper.value !== undefined) {
const { x, y } = await computePosition(refPopperContainer.value, refPopper.value, {
placement: props.popperInlineEnd ? props.isRtl ? 'left-start' : 'right-start' : 'bottom-start',
middleware: [
...configStore.horizontalNavPopoverOffset ? [offset(configStore.horizontalNavPopoverOffset)] : [],
flip({ boundary: document.querySelector('body') }),
shift({ boundary: document.querySelector('body') }),
],
/* Why we are not using fixed positioning?
`position: fixed` doesn't work as expected when some CSS properties like `transform` is applied on its parent element.
Docs: https://developer.mozilla.org/en-US/docs/Web/CSS/position#values <= See `fixed` value description
Hence, when we use transitions where transition apply `transform` on its parent element, fixed positioning will not work.
(Popper content moves away from the element when parent element transition)
To avoid this, we use `position: absolute` instead of `position: fixed`.
NOTE: This issue starts from third level children (Top Level > Sub item > Sub item).
*/
// strategy: 'fixed',
})
popperContentStyles.value.left = `${ x }px`
popperContentStyles.value.top = `${ y }px`
}
}
until(() => configStore.horizontalNavType).toMatch(type => type === 'static').then(() => {
useEventListener('scroll', updatePopper)
/* Why we are not using fixed positioning?
`position: fixed` doesn't work as expected when some CSS properties like `transform` is applied on its parent element.
Docs: https://developer.mozilla.org/en-US/docs/Web/CSS/position#values <= See `fixed` value description
Hence, when we use transitions where transition apply `transform` on its parent element, fixed positioning will not work.
(Popper content moves away from the element when parent element transition)
To avoid this, we use `position: absolute` instead of `position: fixed`.
NOTE: This issue starts from third level children (Top Level > Sub item > Sub item).
*/
// strategy: 'fixed',
})
const isContentShown = ref(false)
const showContent = () => {
isContentShown.value = true
updatePopper()
}
const hideContent = () => {
isContentShown.value = false
}
onMounted(updatePopper)
// Recalculate popper position when it's triggerer changes its position
watch([
() => configStore.isAppRTL,
() => configStore.appContentWidth,
], updatePopper)
// Watch for route changes and close popper content if route is changed
const route = useRoute()
watch(() => route.fullPath, hideContent)
</script>
<template>
<div
class="nav-popper"
:class="[{
'popper-inline-end': popperInlineEnd,
'show-content': isContentShown,
}]"
>
<div
ref="refPopperContainer"
class="popper-triggerer"
@mouseenter="showContent"
@mouseleave="hideContent"
>
<slot />
</div>
<!-- SECTION Popper Content -->
<!-- 👉 Without transition -->
<template v-if="!themeConfig.horizontalNav.transition">
<div
ref="refPopper"
class="popper-content"
:style="popperContentStyles"
@mouseenter="showContent"
@mouseleave="hideContent"
>
<div>
<slot name="content" />
</div>
</div>
</template>
<!-- 👉 CSS Transition -->
<template v-else-if="typeof themeConfig.horizontalNav.transition === 'string'">
<Transition :name="themeConfig.horizontalNav.transition">
<div
v-show="isContentShown"
ref="refPopper"
class="popper-content"
:style="popperContentStyles"
@mouseenter="showContent"
@mouseleave="hideContent"
>
<div>
<slot name="content" />
</div>
</div>
</Transition>
</template>
<!-- 👉 Transition Component -->
<template v-else>
<Component :is="themeConfig.horizontalNav.transition">
<div
v-show="isContentShown"
ref="refPopper"
class="popper-content"
:style="popperContentStyles"
@mouseenter="showContent"
@mouseleave="hideContent"
>
<div>
<slot name="content" />
</div>
</div>
</Component>
</template>
<!-- !SECTION -->
</div>
</template>
<style lang="scss">
.popper-content {
position: absolute;
}
</style>

View File

@@ -0,0 +1,87 @@
<!-- Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ -->
<script>
import { Transition } from 'vue'
export default defineComponent({
name: 'TransitionExpand',
setup(_, { slots }) {
const onEnter = element => {
const width = getComputedStyle(element).width
element.style.width = width
element.style.position = 'absolute'
element.style.visibility = 'hidden'
element.style.height = 'auto'
const height = getComputedStyle(element).height
element.style.width = ''
element.style.position = ''
element.style.visibility = ''
element.style.height = '0px'
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height
// Trigger the animation.
// We use `requestAnimationFrame` because we need
// to make sure the browser has finished
// painting after setting the `height`
// to `0` in the line above.
requestAnimationFrame(() => {
element.style.height = height
})
}
const onAfterEnter = element => {
element.style.height = 'auto'
}
const onLeave = element => {
const height = getComputedStyle(element).height
element.style.height = height
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height
requestAnimationFrame(() => {
element.style.height = '0px'
})
}
return () => h(h(Transition), {
name: 'expand',
onEnter,
onAfterEnter,
onLeave,
}, () => slots.default?.())
},
})
</script>
<style>
.expand-enter-active,
.expand-leave-active {
overflow: hidden;
transition: block-size var(--expand-transition-duration, 0.25s) ease;
}
.expand-enter-from,
.expand-leave-to {
block-size: 0;
}
</style>
<style scoped>
* {
backface-visibility: hidden;
perspective: 1000px;
transform: translateZ(0);
will-change: block-size;
}
</style>

View File

@@ -0,0 +1,12 @@
export const VNodeRenderer = defineComponent({
name: 'VNodeRenderer',
props: {
nodes: {
type: [Array, Object],
required: true,
},
},
setup(props) {
return () => props.nodes
},
})

View File

@@ -0,0 +1,246 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { VNodeRenderer } from './VNodeRenderer'
import { layoutConfig } from '@layouts'
import {
VerticalNavGroup,
VerticalNavLink,
VerticalNavSectionTitle,
} from '@layouts/components'
import { useLayoutConfigStore } from '@layouts/stores/config'
import { injectionKeyIsVerticalNavHovered } from '@layouts/symbols'
const props = defineProps({
tag: {
type: null,
required: false,
default: 'aside',
},
navItems: {
type: null,
required: true,
},
isOverlayNavActive: {
type: Boolean,
required: true,
},
toggleIsOverlayNavActive: {
type: Function,
required: true,
},
})
const refNav = ref()
const isHovered = useElementHover(refNav)
provide(injectionKeyIsVerticalNavHovered, isHovered)
const configStore = useLayoutConfigStore()
const resolveNavItemComponent = item => {
if ('heading' in item)
return VerticalNavSectionTitle
if ('children' in item)
return VerticalNavGroup
return VerticalNavLink
}
/* Close overlay side when route is changed
Close overlay vertical nav when link is clicked
*/
const route = useRoute()
watch(() => route.name, () => {
props.toggleIsOverlayNavActive(false)
})
const isVerticalNavScrolled = ref(false)
const updateIsVerticalNavScrolled = val => isVerticalNavScrolled.value = val
const handleNavScroll = evt => {
isVerticalNavScrolled.value = evt.target.scrollTop > 0
}
const hideTitleAndIcon = configStore.isVerticalNavMini(isHovered)
</script>
<template>
<Component
:is="props.tag"
ref="refNav"
class="layout-vertical-nav"
:class="[
{
'overlay-nav': configStore.isLessThanOverlayNavBreakpoint,
'hovered': isHovered,
'visible': isOverlayNavActive,
'scrolled': isVerticalNavScrolled,
},
]"
>
<!-- 👉 Header -->
<div class="nav-header">
<slot name="nav-header">
<RouterLink
to="/"
class="app-logo app-title-wrapper"
>
<VNodeRenderer :nodes="layoutConfig.app.logo" />
<Transition name="vertical-nav-app-title">
<h1
v-show="!hideTitleAndIcon"
class="app-logo-title leading-normal"
>
{{ layoutConfig.app.title }}
</h1>
</Transition>
</RouterLink>
<!-- 👉 Vertical nav actions -->
<!-- Show toggle collapsible in >md and close button in <md -->
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
v-show="configStore.isVerticalNavCollapsed"
class="header-action d-none nav-unpin"
:class="configStore.isVerticalNavCollapsed && 'd-lg-block'"
v-bind="layoutConfig.icons.verticalNavUnPinned"
@click="configStore.isVerticalNavCollapsed = !configStore.isVerticalNavCollapsed"
/>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
v-show="!configStore.isVerticalNavCollapsed"
class="header-action d-none nav-pin"
:class="!configStore.isVerticalNavCollapsed && 'd-lg-block'"
v-bind="layoutConfig.icons.verticalNavPinned"
@click="configStore.isVerticalNavCollapsed = !configStore.isVerticalNavCollapsed"
/>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
class="header-action d-lg-none"
v-bind="layoutConfig.icons.close"
@click="toggleIsOverlayNavActive(false)"
/>
</slot>
</div>
<slot name="before-nav-items">
<div class="vertical-nav-items-shadow" />
</slot>
<slot
name="nav-items"
:update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled"
>
<PerfectScrollbar
:key="configStore.isAppRTL"
tag="ul"
class="nav-items"
:options="{ wheelPropagation: false }"
@ps-scroll-y="handleNavScroll"
>
<Component
:is="resolveNavItemComponent(item)"
v-for="(item, index) in navItems"
:key="index"
:item="item"
/>
</PerfectScrollbar>
</slot>
<slot name="after-nav-items" />
</Component>
</template>
<style lang="scss" scoped>
.app-logo {
display: flex;
align-items: center;
column-gap: 0.75rem;
.app-logo-title {
font-size: 1.25rem;
font-weight: 600;
line-height: 1.75rem;
text-transform: uppercase;
}
}
</style>
<style lang="scss">
@use "@configured-variables" as variables;
@use "@layouts/styles/mixins";
// 👉 Vertical Nav
.layout-vertical-nav {
position: fixed;
z-index: variables.$layout-vertical-nav-z-index;
display: flex;
flex-direction: column;
block-size: 100%;
inline-size: variables.$layout-vertical-nav-width;
inset-block-start: 0;
inset-inline-start: 0;
transition: inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
will-change: transform, inline-size;
.nav-header {
display: flex;
align-items: center;
.header-action {
cursor: pointer;
@at-root {
#{variables.$selector-vertical-nav-mini} .nav-header .header-action {
&.nav-pin,
&.nav-unpin {
display: none !important;
}
}
}
}
}
.app-title-wrapper {
margin-inline-end: auto;
}
.nav-items {
block-size: 100%;
// We no loner needs this overflow styles as perfect scrollbar applies it
// overflow-x: hidden;
// // We used `overflow-y` instead of `overflow` to mitigate overflow x. Revert back if any issue found.
// overflow-y: auto;
}
.nav-item-title {
overflow: hidden;
margin-inline-end: auto;
text-overflow: ellipsis;
white-space: nowrap;
}
// 👉 Collapsed
.layout-vertical-nav-collapsed & {
&:not(.hovered) {
inline-size: variables.$layout-vertical-nav-collapsed-width;
}
}
}
// Small screen vertical nav transition
@media (max-width:1279px) {
.layout-vertical-nav {
&:not(.visible) {
transform: translateX(-#{variables.$layout-vertical-nav-width});
@include mixins.rtl {
transform: translateX(variables.$layout-vertical-nav-width);
}
}
transition: transform 0.25s ease-in-out;
}
}
</style>

View File

@@ -0,0 +1,218 @@
<script setup>
import { TransitionGroup } from 'vue'
import { layoutConfig } from '@layouts'
import {
TransitionExpand,
VerticalNavLink,
} from '@layouts/components'
import { canViewNavMenuGroup } from '@layouts/plugins/casl'
import { useLayoutConfigStore } from '@layouts/stores/config'
import { injectionKeyIsVerticalNavHovered } from '@layouts/symbols'
import {
getDynamicI18nProps,
isNavGroupActive,
openGroups,
} from '@layouts/utils'
const props = defineProps({
item: {
type: null,
required: true,
},
})
defineOptions({
name: 'VerticalNavGroup',
})
const route = useRoute()
const router = useRouter()
const configStore = useLayoutConfigStore()
const hideTitleAndBadge = configStore.isVerticalNavMini()
/* We provided default value `ref(false)` because inject will return `T | undefined`
Docs: https://vuejs.org/api/composition-api-dependency-injection.html#inject
*/
const isVerticalNavHovered = inject(injectionKeyIsVerticalNavHovered, ref(false))
// isGroupOpen.value = value ? false : isGroupActive.value
// })
const isGroupActive = ref(false)
const isGroupOpen = ref(false)
const isAnyChildOpen = children => {
return children.some(child => {
let result = openGroups.value.includes(child.title)
if ('children' in child)
result = isAnyChildOpen(child.children) || result
return result
})
}
const collapseChildren = children => {
children.forEach(child => {
if ('children' in child)
collapseChildren(child.children)
openGroups.value = openGroups.value.filter(group => group !== child.title)
})
}
/*Watch for route changes, more specifically route path. Do note that this won't trigger if route's query is updated.
updates isActive & isOpen based on active state of group.
*/
watch(() => route.path, () => {
const isActive = isNavGroupActive(props.item.children, router)
// Don't open group if vertical nav is collapsed and window size is more than overlay nav breakpoint
isGroupOpen.value = isActive && !configStore.isVerticalNavMini(isVerticalNavHovered).value
isGroupActive.value = isActive
}, { immediate: true })
watch(isGroupOpen, val => {
// Find group index for adding/removing group from openGroups array
const grpIndex = openGroups.value.indexOf(props.item.title)
// update openGroups array for addition/removal of current group
// If group is opened => Add it to `openGroups` array
if (val && grpIndex === -1) {
openGroups.value.push(props.item.title)
} else if (!val && grpIndex !== -1) {
openGroups.value.splice(grpIndex, 1)
collapseChildren(props.item.children)
}
}, { immediate: true })
/*Watch for openGroups
It will help in making vertical nav adapting the behavior of accordion.
If we open multiple groups without navigating to any route we must close the inactive or temporarily opened groups.
😵‍💫 Gotchas:
* If we open inactive group then it will auto close that group because we close groups based on active state.
Goal of this watcher is auto close groups which are not active when openGroups array is updated.
So, we have to find a way to do not close recently opened inactive group.
For this we will fetch recently added group in openGroups array and won't perform closing operation if recently added group is current group
*/
watch(openGroups, val => {
// Prevent closing recently opened inactive group.
const lastOpenedGroup = val.at(-1)
if (lastOpenedGroup === props.item.title)
return
const isActive = isNavGroupActive(props.item.children, router)
// Goal of this watcher is to close inactive groups. So don't do anything for active groups.
if (isActive)
return
// We won't close group if any of child group is open in current group
if (isAnyChildOpen(props.item.children))
return
isGroupOpen.value = isActive
isGroupActive.value = isActive
}, { deep: true })
// Previously instead of below watcher we were using two individual watcher for `isVerticalNavHovered`, `isVerticalNavCollapsed` & `isLessThanOverlayNavBreakpoint`
watch(configStore.isVerticalNavMini(isVerticalNavHovered), val => {
isGroupOpen.value = val ? false : isGroupActive.value
})
// isGroupOpen.value = value ? false : isGroupActive.value
// })
const isMounted = useMounted()
</script>
<template>
<li
v-if="canViewNavMenuGroup(item)"
class="nav-group"
:class="[
{
active: isGroupActive,
open: isGroupOpen,
disabled: item.disable,
},
]"
>
<div
class="nav-group-label"
@click="isGroupOpen = !isGroupOpen"
>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
v-bind="item.icon || layoutConfig.verticalNav.defaultNavItemIconProps"
class="nav-item-icon"
/>
<!--
isMounted is workaround of nuxt's hydration issue:
https://github.com/vuejs/core/issues/6715
-->
<Component
:is="isMounted ? TransitionGroup : 'div'"
name="transition-slide-x"
v-bind="!isMounted ? { class: 'd-flex align-center flex-grow-1' } : undefined"
>
<!-- 👉 Title -->
<Component
:is=" layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
v-bind="getDynamicI18nProps(item.title, 'span')"
v-show="!hideTitleAndBadge"
key="title"
class="nav-item-title"
>
{{ item.title }}
</Component>
<!-- 👉 Badge -->
<Component
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
v-bind="getDynamicI18nProps(item.badgeContent, 'span')"
v-show="!hideTitleAndBadge"
v-if="item.badgeContent"
key="badge"
class="nav-item-badge"
:class="item.badgeClass"
>
{{ item.badgeContent }}
</Component>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
v-show="!hideTitleAndBadge"
v-bind="layoutConfig.icons.chevronRight"
key="arrow"
class="nav-group-arrow"
/>
</Component>
</div>
<TransitionExpand>
<ul
v-show="isGroupOpen"
class="nav-group-children"
>
<Component
:is="'children' in child ? 'VerticalNavGroup' : VerticalNavLink"
v-for="child in item.children"
:key="child.title"
:item="child"
/>
</ul>
</TransitionExpand>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-group {
&-label {
display: flex;
align-items: center;
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,193 @@
<script>
import { VerticalNav } from '@layouts/components'
import { useLayoutConfigStore } from '@layouts/stores/config'
export default defineComponent({
props: {
navItems: {
type: Array,
required: true,
},
verticalNavAttrs: {
type: Object,
default: () => ({}),
},
},
setup(props, { slots }) {
const { width: windowWidth } = useWindowSize()
const configStore = useLayoutConfigStore()
const isOverlayNavActive = ref(false)
const isLayoutOverlayVisible = ref(false)
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
// This is alternative to below two commented watcher
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
// watch(isOverlayNavActive, value => {
// // Sync layout overlay with overlay nav
// isLayoutOverlayVisible.value = value
// })
// watch(isLayoutOverlayVisible, value => {
// // If overlay is closed via click, close hide overlay nav
// if (!value) isOverlayNavActive.value = false
// })
// Hide overlay if user open overlay nav in <md and increase the window width without closing overlay nav
watch(windowWidth, () => {
if (!configStore.isLessThanOverlayNavBreakpoint && isLayoutOverlayVisible.value)
isLayoutOverlayVisible.value = false
})
return () => {
const verticalNavAttrs = toRef(props, 'verticalNavAttrs')
const { wrapper: verticalNavWrapper, wrapperProps: verticalNavWrapperProps, ...additionalVerticalNavAttrs } = verticalNavAttrs.value
// 👉 Vertical nav
const verticalNav = h(VerticalNav, { isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive, navItems: props.navItems, ...additionalVerticalNavAttrs }, {
'nav-header': () => slots['vertical-nav-header']?.(),
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
})
// 👉 Navbar
const navbar = h('header', { class: ['layout-navbar', { 'navbar-blur': configStore.isNavbarBlurEnabled }] }, [
h('div', { class: 'navbar-content-container' }, slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
})),
])
// 👉 Content area
const main = h('main', { class: 'layout-page-content' }, h('div', { class: 'page-content-container' }, slots.default?.()))
// 👉 Footer
const footer = h('footer', { class: 'layout-footer' }, [
h('div', { class: 'footer-content-container' }, slots.footer?.()),
])
// 👉 Overlay
const layoutOverlay = h('div', {
class: ['layout-overlay', { visible: isLayoutOverlayVisible.value }],
onClick: () => { isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value },
})
return h('div', { class: ['layout-wrapper', ...configStore._layoutClasses] }, [
verticalNavWrapper ? h(verticalNavWrapper, verticalNavWrapperProps, { default: () => verticalNav }) : verticalNav,
h('div', { class: 'layout-content-wrapper' }, [
navbar,
main,
footer,
]),
layoutOverlay,
])
}
},
})
</script>
<style lang="scss">
@use "@configured-variables" as variables;
@use "@layouts/styles/placeholders";
@use "@layouts/styles/mixins";
.layout-wrapper.layout-nav-type-vertical {
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
block-size: 100%;
.layout-content-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
min-block-size: 100dvh;
transition: padding-inline-start 0.2s ease-in-out;
will-change: padding-inline-start;
@media screen and (min-width: 1280px) {
padding-inline-start: variables.$layout-vertical-nav-width;
}
}
.layout-navbar {
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
.navbar-content-container {
block-size: variables.$layout-vertical-nav-navbar-height;
}
@at-root {
.layout-wrapper.layout-nav-type-vertical {
.layout-navbar {
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.boxed-content;
} @else {
.navbar-content-container {
@include mixins.boxed-content;
}
}
}
}
}
}
&.layout-navbar-sticky .layout-navbar {
@extend %layout-navbar-sticky;
}
&.layout-navbar-hidden .layout-navbar {
@extend %layout-navbar-hidden;
}
// 👉 Footer
.layout-footer {
@include mixins.boxed-content;
}
// 👉 Layout overlay
.layout-overlay {
position: fixed;
z-index: variables.$layout-overlay-z-index;
background-color: rgb(0 0 0 / 60%);
cursor: pointer;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease-in-out;
will-change: transform;
&.visible {
opacity: 1;
pointer-events: auto;
}
}
// Adjust right column pl when vertical nav is collapsed
&.layout-vertical-nav-collapsed .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
}
// 👉 Content height fixed
&.layout-content-height-fixed {
.layout-content-wrapper {
max-block-size: 100dvh;
}
.layout-page-content {
display: flex;
overflow: hidden;
.page-content-container {
inline-size: 100%;
> :first-child {
max-block-size: 100%;
overflow-y: auto;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,74 @@
<script setup>
import { layoutConfig } from '@layouts'
import { can } from '@layouts/plugins/casl'
import { useLayoutConfigStore } from '@layouts/stores/config'
import {
getComputedNavLinkToProp,
getDynamicI18nProps,
isNavLinkActive,
} from '@layouts/utils'
const props = defineProps({
item: {
type: null,
required: true,
},
})
const configStore = useLayoutConfigStore()
const hideTitleAndBadge = configStore.isVerticalNavMini()
</script>
<template>
<li
v-if="can(item.action, item.subject)"
class="nav-link"
:class="{ disabled: item.disable }"
>
<Component
:is="item.to ? 'RouterLink' : 'a'"
v-bind="getComputedNavLinkToProp(item)"
:class="{ 'router-link-active router-link-exact-active': isNavLinkActive(item, $router) }"
>
<Component
:is="layoutConfig.app.iconRenderer || 'div'"
v-bind="item.icon || layoutConfig.verticalNav.defaultNavItemIconProps"
class="nav-item-icon"
/>
<TransitionGroup name="transition-slide-x">
<!-- 👉 Title -->
<Component
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
v-show="!hideTitleAndBadge"
key="title"
class="nav-item-title"
v-bind="getDynamicI18nProps(item.title, 'span')"
>
{{ item.title }}
</Component>
<!-- 👉 Badge -->
<Component
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
v-if="item.badgeContent"
v-show="!hideTitleAndBadge"
key="badge"
class="nav-item-badge"
:class="item.badgeClass"
v-bind="getDynamicI18nProps(item.badgeContent, 'span')"
>
{{ item.badgeContent }}
</Component>
</TransitionGroup>
</Component>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-link a {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<script setup>
import { layoutConfig } from '@layouts'
import { can } from '@layouts/plugins/casl'
import { useLayoutConfigStore } from '@layouts/stores/config'
import { getDynamicI18nProps } from '@layouts/utils'
const props = defineProps({
item: {
type: null,
required: true,
},
})
const configStore = useLayoutConfigStore()
const shallRenderIcon = configStore.isVerticalNavMini()
</script>
<template>
<li
v-if="can(item.action, item.subject)"
class="nav-section-title"
>
<div class="title-wrapper">
<Transition
name="vertical-nav-section-title"
mode="out-in"
>
<Component
:is="shallRenderIcon ? layoutConfig.app.iconRenderer : layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
:key="shallRenderIcon"
:class="shallRenderIcon ? 'placeholder-icon' : 'title-text'"
v-bind="{ ...layoutConfig.icons.sectionTitlePlaceholder, ...getDynamicI18nProps(item.heading, 'span') }"
>
{{ !shallRenderIcon ? item.heading : null }}
</Component>
</Transition>
</div>
</li>
</template>