mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 19:03:49 +09:00
first
This commit is contained in:
366
src/components/layouts/auth/AuthLayout.vue
Normal file
366
src/components/layouts/auth/AuthLayout.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="auth-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.auth-wrapper-inner {
|
||||
overflow: hidden !important;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.is-gapless:not(:last-child) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&.is-single {
|
||||
background: var(--widget-grey);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
background: var(--widget-grey);
|
||||
|
||||
img {
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
position: relative;
|
||||
max-width: 360px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px 0 0;
|
||||
|
||||
.auth-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.dark-mode {
|
||||
transform: scale(0.6);
|
||||
z-index: 2;
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
.top-logo {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
&.is-white {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
.login {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-top: -40px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-family: var(--font);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form-wrapper {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
margin-top: 10px;
|
||||
|
||||
a {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
|
||||
.setting-meta {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-button {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.auth-wrapper-inner {
|
||||
.hero-banner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
}
|
||||
|
||||
.hero {
|
||||
&.is-white {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 4%);
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
h2 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-nav {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
z-index: 1;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
|
||||
.dark-mode {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
flex-grow: 2;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.single-form-wrap {
|
||||
min-height: 690px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.inner-wrap {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 40px auto 0;
|
||||
|
||||
.auth-head {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-family: var(--font);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
border-radius: 10px;
|
||||
padding: 50px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.v-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.auth-wrapper-inner {
|
||||
&.is-single {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
|
||||
.single-form-wrap {
|
||||
.inner-wrap {
|
||||
.auth-head {
|
||||
h2 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 4%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.avatar-carousel {
|
||||
&.resized-mobile {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.slick-custom {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.hero {
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.single-form-wrap {
|
||||
.inner-wrap {
|
||||
.form-card {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.modern-login {
|
||||
.top-logo {
|
||||
.iconify {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
top: -58px;
|
||||
inset-inline-end: 30%;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.hero {
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-columns {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
310
src/components/layouts/landing/LandingFooter.vue
Normal file
310
src/components/layouts/landing/LandingFooter.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<script setup lang="ts">
|
||||
import type { LandingFooterColumn, LandingSocialItem } from './landing.types'
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
subtitle?: string
|
||||
|
||||
links?: LandingFooterColumn[]
|
||||
social?: LandingSocialItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="vuero-footer is-relative">
|
||||
<div class="container">
|
||||
<div v-if="'action' in $slots || props.title || props.subtitle" class="footer-head">
|
||||
<div v-if="props.title || props.subtitle" class="head-text">
|
||||
<h3 v-if="props.title">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<p v-if="props.subtitle">
|
||||
{{ props.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="head-action">
|
||||
<div class="is-flex is-align-items-center">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="'default' in $slots || props.social?.length || props.links?.length" class="columns footer-body">
|
||||
<!-- Column -->
|
||||
<div v-if="'default' in $slots || props.social?.length" class="column is-4">
|
||||
<slot />
|
||||
<div v-if="props.social">
|
||||
<div class="social-links p-b-10">
|
||||
<VLink
|
||||
v-for="item in social"
|
||||
:key="item.icon"
|
||||
:to="item.link"
|
||||
>
|
||||
<VIcon :icon="item.icon" class="icon" />
|
||||
</VLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Column -->
|
||||
<div
|
||||
class="column"
|
||||
:class="'default' in $slots || props.social?.length ? 'is-6 is-offset-2' : 'is-12 has-text-centered'"
|
||||
>
|
||||
<div class="columns is-flex-tablet-p">
|
||||
<!-- Column -->
|
||||
<div
|
||||
v-for="column in props.links"
|
||||
:key="column.label"
|
||||
class="column"
|
||||
>
|
||||
<ul class="footer-column">
|
||||
<li class="column-header">
|
||||
{{ column.label }}
|
||||
</li>
|
||||
<li
|
||||
v-for="link in column.children"
|
||||
:key="link.label"
|
||||
class="column-item"
|
||||
>
|
||||
<VLink :to="link.to">
|
||||
{{ link.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-copyright has-text-centered">
|
||||
<slot name="copyright" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- /Simple light footer -->
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.vuero-footer {
|
||||
padding-bottom: 0 !important;
|
||||
padding-top: 4rem !important;
|
||||
background: var(--body-color);
|
||||
|
||||
.footer-head {
|
||||
padding-bottom: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
|
||||
.head-text {
|
||||
h3 {
|
||||
font-family: var(--font);
|
||||
font-size: 1.8rem;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.head-action {
|
||||
.buttons {
|
||||
.button {
|
||||
&.action-button {
|
||||
height: 36px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
&.chat-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
padding-top: 3rem;
|
||||
|
||||
.footer-column {
|
||||
padding-top: 20px;
|
||||
|
||||
.column-header {
|
||||
font-family: var(--font-alt);
|
||||
text-transform: uppercase;
|
||||
color: var(--dark-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
padding-bottom: 10px;
|
||||
|
||||
a {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
color: var(--light-text);
|
||||
font-size: 16px;
|
||||
margin-inline-end: 0.6rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.small-footer-logo {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
padding: 4rem 0 2rem;
|
||||
|
||||
a {
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.landing-page-wrapper {
|
||||
.vuero-footer {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%);
|
||||
|
||||
.footer-head {
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 18%);
|
||||
|
||||
.head-text {
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.head-action {
|
||||
.buttons {
|
||||
.button {
|
||||
&.action-button {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&.chat-button {
|
||||
color: var(--primary);
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
.footer-column {
|
||||
.column-header {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.column-item {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-links {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
a {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.vuero-footer {
|
||||
.footer-head {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.head-text {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.vuero-footer {
|
||||
.footer-head,
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.vuero-footer {
|
||||
.footer-head,
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/layouts/landing/LandingGrids.vue
Normal file
34
src/components/layouts/landing/LandingGrids.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const { x, y } = useMouse()
|
||||
|
||||
const maskPosition = computed(() => `${Math.round(x.value - 220)}px ${Math.round(y.value - 220)}px`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grids gridlines" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grids {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
will-change: mask-position;
|
||||
mask-image: radial-gradient(circle 220px at 220px 220px, black 0%, transparent 100%);
|
||||
mask-position: v-bind(maskPosition);
|
||||
mask-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gridlines {
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGcgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBmaWxsPSIjZWJlYmViIj48cGF0aCBvcGFjaXR5PSIuNSIgZD0iTTk2IDk1aDR2MWgtNHY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNEgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djl6bS0xIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6IiAvPjxwYXRoIGQ9Ik02IDVWMEg1djVIMHYxaDV2OTRoMVY2aDk0VjVINnoiIC8+PC9nPjwvZz48L3N2Zz4K');
|
||||
}
|
||||
|
||||
.is-dark .gridlines:not(.is-contrasted) {
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGcgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBmaWxsPSIjMjcyZTNlIj48cGF0aCBvcGFjaXR5PSIuNSIgZD0iTTk2IDk1aDR2MWgtNHY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNEgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djl6bS0xIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6IiBmaWxsPSIjMjcyZTNlIi8+PHBhdGggZD0iTTYgNVYwSDV2NUgwdjFoNXY5NGgxVjZoOTRWNUg2eiIgZmlsbD0iIzI3MmUzZSIvPjwvZz48L2c+PC9zdmc+');
|
||||
}
|
||||
|
||||
.is-dark .gridlines.is-contrasted {
|
||||
filter: brightness(0.30);
|
||||
}
|
||||
</style>
|
||||
51
src/components/layouts/landing/LandingLayout.vue
Normal file
51
src/components/layouts/landing/LandingLayout.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { LandingNavItem } from './landing.types'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
links: LandingNavItem[]
|
||||
}>(), {
|
||||
links: () => [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MinimalLayout theme="light">
|
||||
<main class="landing-page-wrapper">
|
||||
<LandingGrids />
|
||||
|
||||
<div class="hero">
|
||||
<LandingNavigation>
|
||||
<template #logo>
|
||||
<slot name="logo" />
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="nav-end" />
|
||||
</template>
|
||||
|
||||
<slot name="nav-links">
|
||||
<div
|
||||
v-for="link in props.links"
|
||||
:key="link.label"
|
||||
class="navbar-item"
|
||||
>
|
||||
<VLink
|
||||
:to="link.to"
|
||||
class="nav-link"
|
||||
:class="[link.active && 'is-active']"
|
||||
>
|
||||
{{ link.label }}
|
||||
</VLink>
|
||||
</div>
|
||||
</slot>
|
||||
</LandingNavigation>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</main>
|
||||
</MinimalLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/abstracts/all';
|
||||
@import '/@src/scss/layout/landing';
|
||||
</style>
|
||||
360
src/components/layouts/landing/LandingNavigation.vue
Normal file
360
src/components/layouts/landing/LandingNavigation.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<script setup lang="ts">
|
||||
const isMobileNavOpen = ref(false)
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const { isLargeScreen } = useScreenSize()
|
||||
|
||||
const isScrolling = computed(() => {
|
||||
return y.value > 30
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (isLargeScreen.value) {
|
||||
isMobileNavOpen.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero">
|
||||
<nav
|
||||
class="navbar is-fixed-top"
|
||||
:class="[!isScrolling && 'is-docked', isMobileNavOpen && 'is-solid']"
|
||||
aria-label="main navigation"
|
||||
>
|
||||
<div class="navbar-brand">
|
||||
<slot name="logo" />
|
||||
|
||||
<MobileBurger v-model="isMobileNavOpen" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="navbar-menu"
|
||||
:class="[isMobileNavOpen && 'is-active']"
|
||||
>
|
||||
<div class="navbar-start">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.hero {
|
||||
.navbar {
|
||||
top: 15px;
|
||||
height: 65px;
|
||||
max-width: 1140px;
|
||||
margin: 0 auto;
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
border: 1px solid var(--fade-grey);
|
||||
border-radius: 500px;
|
||||
font-family: var(--font);
|
||||
z-index: 99;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-docked {
|
||||
&:not(.is-solid) {
|
||||
top: 0;
|
||||
border-color: transparent;
|
||||
height: 110px;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
background: var(--white);
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
height: 65px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
background: var(--white) !important;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
img,
|
||||
.iconify {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border-radius: var(--radius-rounded);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
img,
|
||||
svg {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
.navbar-item {
|
||||
.dark-mode {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
text-transform: capitalize;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
inset-inline-start: 2px;
|
||||
width: 50%;
|
||||
transform-origin: right center;
|
||||
height: 3px;
|
||||
border-radius: 50px;
|
||||
background: var(--primary);
|
||||
transform: scale(0, 1);
|
||||
transition: -webkit-transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
-webkit-transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// Hover state
|
||||
&:hover,
|
||||
&.is-active {
|
||||
color: var(--dark-text);
|
||||
|
||||
&::before {
|
||||
transform-origin: left center;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::before {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
font-weight: 400 !important;
|
||||
height: 44px;
|
||||
min-width: 110px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.navbar {
|
||||
&:not(.is-docked) {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%);
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%);
|
||||
}
|
||||
|
||||
&.is-docked {
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
background: color-mix(in oklab, var(--landing-yyy), white 8%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 18%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
&.is-active {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
.nav-link {
|
||||
&:hover,
|
||||
&.is-active {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
|
||||
&.is-raised:hover {
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
|
||||
&.is-docked {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
top: 10px;
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
top: 73px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
width: calc(100% - 32px);
|
||||
position: fixed;
|
||||
top: 78px;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 0 0 10px 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--fade-grey);
|
||||
box-shadow: none;
|
||||
|
||||
.navbar-item {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
|
||||
&.is-docked {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&:not(.is-docked) {
|
||||
&.is-solid {
|
||||
.navbar-menu {
|
||||
top: 73px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
top: 10px;
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
width: calc(100% - 32px);
|
||||
position: fixed;
|
||||
top: 78px;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 0 0 10px 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--fade-grey);
|
||||
box-shadow: none;
|
||||
|
||||
.navbar-item {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.navbar {
|
||||
width: calc(100% - 40px);
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
src/components/layouts/landing/landing.types.ts
Normal file
18
src/components/layouts/landing/landing.types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface LandingNavItem {
|
||||
label: string
|
||||
to: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface LandingFooterColumn {
|
||||
label: string
|
||||
children: {
|
||||
label: string
|
||||
to: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface LandingSocialItem {
|
||||
icon: string
|
||||
link: string
|
||||
}
|
||||
53
src/components/layouts/minimal/MinimalLayout.vue
Normal file
53
src/components/layouts/minimal/MinimalLayout.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
export type MinimalTheme = 'darker' | 'light'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
theme?: MinimalTheme
|
||||
}>(),
|
||||
{
|
||||
theme: 'darker',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="minimal-wrapper"
|
||||
:class="[props.theme]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.minimal-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--lighter-grey);
|
||||
|
||||
&.light {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&.lighter {
|
||||
background: var(--smoke-white);
|
||||
}
|
||||
|
||||
&.darker {
|
||||
background: var(--background-grey);
|
||||
}
|
||||
|
||||
.minimal-wrap {
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.minimal-wrapper {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
656
src/components/layouts/navbar/Navbar.vue
Normal file
656
src/components/layouts/navbar/Navbar.vue
Normal file
@@ -0,0 +1,656 @@
|
||||
<script setup lang="ts">
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const { theme } = useNavbarLayoutContext()
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const isScrolling = computed(() => {
|
||||
return y.value > 30
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="navbar-navbar"
|
||||
:class="[
|
||||
isScrolling && 'is-scrolled',
|
||||
theme === 'fade' && 'is-transparent',
|
||||
theme === 'colored' && 'is-colored',
|
||||
]"
|
||||
>
|
||||
<div class="navbar-navbar-inner">
|
||||
<div class="left">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="center">
|
||||
<slot name="links" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
background: var(--white);
|
||||
transition: all 0.3s; // transition-all test
|
||||
border-bottom: 1px solid var(--fade-grey);
|
||||
z-index: 100;
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: var(--white);
|
||||
border-bottom-color: var(--fade-grey);
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.is-scrolled {
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-colored {
|
||||
background: var(--landing-yyy);
|
||||
border-bottom-color: var(--landing-yyy);
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 18%);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
// background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
// border-color: color-mix(in oklab, var(--landing-yyy), white 6%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-drops {
|
||||
.centered-drop {
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: var(--landing-yyy);
|
||||
color: var(--light-text);
|
||||
|
||||
.caret {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-button {
|
||||
.button {
|
||||
background: var(--landing-yyy);
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-search {
|
||||
.field {
|
||||
.control {
|
||||
.input {
|
||||
background: color-mix(in oklab, var(--primary), black 10%);
|
||||
border-color: color-mix(in oklab, var(--primary), black 6%);
|
||||
color: var(--smoke-white);
|
||||
|
||||
&::placeholder {
|
||||
color: color-mix(in oklab, var(--primary), white 2%);
|
||||
}
|
||||
|
||||
&:focus ~ .form-icon.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.form-icon.iconify {
|
||||
color: color-mix(in oklab, var(--primary), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.toolbar {
|
||||
.toolbar-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dots {
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
background: var(--landing-yyy);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 17%;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.95rem;
|
||||
color: var(--muted-grey);
|
||||
letter-spacing: 1px;
|
||||
max-width: 50px;
|
||||
line-height: 1.2;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 38px;
|
||||
width: 2px;
|
||||
border-inline-end: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
margin: 0 20px 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 2;
|
||||
width: 50%;
|
||||
|
||||
.left-links {
|
||||
padding-left: 20px;
|
||||
justify-content: left !important;
|
||||
}
|
||||
|
||||
.centered-links, .left-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
// max-width: 580px;
|
||||
|
||||
.centered-link {
|
||||
// flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
padding: 12px 24px;
|
||||
//border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
background: color-mix(in oklab, var(--fade-grey), white 4%);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
span {
|
||||
color: color-mix(in oklab, var(--primary), black 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 20px;
|
||||
color: color-mix(in oklab, var(--light-text), white 6%);
|
||||
stroke-width: 1.6px;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-family: var(--font);
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.6px;
|
||||
font-weight: 500;
|
||||
//color: var(--muted-grey);
|
||||
//color: var(--modal-text);
|
||||
color: black;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.router-link-exact-active {
|
||||
border-bottom: 3px solid var(--primary);
|
||||
|
||||
span{
|
||||
color: black;
|
||||
//color: var(--modal-text) ;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 25%;
|
||||
|
||||
.icon-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
font-size: 18px;
|
||||
border-radius: var(--radius-rounded);
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--fade-grey);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
stroke-width: 1.6px;
|
||||
color: var(--light-text);
|
||||
transition: stroke 0.3s;
|
||||
vertical-align: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. Webapp Navbar Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.navbar-navbar:not(.is-colored) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
a {
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
// background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
// border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
// &:hover,
|
||||
// &:focus {
|
||||
// background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
// }
|
||||
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.icon-link {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar {
|
||||
&.is-colored {
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.title {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-drops {
|
||||
.centered-drop {
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
.button {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
.button {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-mega-dropdown {
|
||||
.dropdown-menu {
|
||||
.dropdown-content {
|
||||
.category-selector {
|
||||
.title-wrap {
|
||||
h4 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
.category-selector-inner {
|
||||
.category-item {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
i,
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mega-menus {
|
||||
.dropdown-item-group {
|
||||
.column-heading {
|
||||
color: var(--dark-dark-text);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
|
||||
.column-content {
|
||||
.is-media {
|
||||
&:hover {
|
||||
.meta {
|
||||
span:first-child {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-button {
|
||||
.button {
|
||||
&:hover {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-search {
|
||||
.field {
|
||||
.control {
|
||||
.input {
|
||||
color: var(--smoke-white);
|
||||
|
||||
&:focus ~ .form-icon .iconify {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.toolbar {
|
||||
.toolbar-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.is-trigger {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
background-color:white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
src/components/layouts/navbar/NavbarDropdown.vue
Normal file
111
src/components/layouts/navbar/NavbarDropdown.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItemDropdown } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItemDropdown
|
||||
}>()
|
||||
|
||||
const tippyRef = ref<any>()
|
||||
|
||||
const {
|
||||
activeSubnavId,
|
||||
toggleSubnav,
|
||||
} = useNavbarLayoutContext()
|
||||
|
||||
watch(activeSubnavId, () => {
|
||||
if (activeSubnavId.value === props.link.id) {
|
||||
tippyRef.value?.show()
|
||||
}
|
||||
else {
|
||||
tippyRef.value?.hide()
|
||||
}
|
||||
})
|
||||
|
||||
function onHidden() {
|
||||
if (activeSubnavId.value === props.link.id) {
|
||||
activeSubnavId.value = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="is-flex">
|
||||
<Tippy
|
||||
ref="tippyRef"
|
||||
:key="`dropdown-${props.link.id}`"
|
||||
trigger="manual"
|
||||
class="is-flex mx-1"
|
||||
content-class="content-tet"
|
||||
interactive
|
||||
:offset="[0, 10]"
|
||||
:duration="[150, 100]"
|
||||
@hidden="onHidden"
|
||||
>
|
||||
<a
|
||||
:class="[
|
||||
activeSubnavId === props.link.id && 'is-active',
|
||||
]"
|
||||
class="centered-link centered-link-toggle"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleSubnav(props.link.id)"
|
||||
@click="toggleSubnav(props.link.id)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span>{{ props.link.label }}</span>
|
||||
</a>
|
||||
|
||||
<template #content>
|
||||
<ul class="centered-link-dropdown has-slimscroll">
|
||||
<li v-for="item of props.link.children" :key="item.to">
|
||||
<VLink :to="item.to">
|
||||
<VIcon
|
||||
v-if="item.icon"
|
||||
|
||||
:icon="item.icon"
|
||||
/>
|
||||
<span>{{ item.label }}</span>
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Tippy>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centered-link-dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: 300px;
|
||||
padding: 1rem 0;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: 0 4rem 0 1rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.2s;
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
color: color-mix(in oklab, var(--light-text), black 5%);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/components/layouts/navbar/NavbarItem.vue
Normal file
66
src/components/layouts/navbar/NavbarItem.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItem, NavbarItemMegamenu, NavbarItemAction } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItem
|
||||
}>()
|
||||
|
||||
const { activeSubnavId, toggleSubnav } = useNavbarLayoutContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavbarDropdown
|
||||
v-if="props.link.type === 'dropdown'"
|
||||
:key="`dropdown-${props.link.id}`"
|
||||
:link="props.link"
|
||||
/>
|
||||
<a
|
||||
v-if="props.link.type === 'megamenu'"
|
||||
:key="`megamenu-${props.link.id}`"
|
||||
:class="[
|
||||
activeSubnavId === props.link.id && 'is-active',
|
||||
]"
|
||||
class="centered-link centered-link-toggle"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
@click="toggleSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="props.link.type === 'action'"
|
||||
:key="`action-${props.link.label}`"
|
||||
class="centered-link centered-link-search"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(props.link as NavbarItemAction).onClick"
|
||||
@click="(props.link as NavbarItemAction).onClick"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</a>
|
||||
<VLink
|
||||
v-else-if="props.link.type === 'link'"
|
||||
:key="`link-${props.link.label}`"
|
||||
class="centered-link centered-link-search"
|
||||
:to="props.link.to"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</VLink>
|
||||
</template>
|
||||
58
src/components/layouts/navbar/NavbarItemMobile.vue
Normal file
58
src/components/layouts/navbar/NavbarItemMobile.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItem, NavbarItemDropdown, NavbarItemMegamenu, NavbarItemAction } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItem
|
||||
}>()
|
||||
|
||||
const { activeMobileSubsidebarId, toggleMobileSubnav } = useNavbarLayoutContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<a
|
||||
v-if="props.link.type === 'dropdown'"
|
||||
:class="[activeMobileSubsidebarId === props.link.id && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleMobileSubnav((props.link as NavbarItemDropdown).id)"
|
||||
@click="toggleMobileSubnav((props.link as NavbarItemDropdown).id)"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="props.link.type === 'megamenu'"
|
||||
:class="[activeMobileSubsidebarId === props.link.id && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleMobileSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
@click="toggleMobileSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="props.link.type === 'action'"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(props.link as NavbarItemAction).onClick"
|
||||
@click="(props.link as NavbarItemAction).onClick"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<VLink
|
||||
v-else-if="props.link.type === 'link'"
|
||||
:to="props.link.to"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</VLink>
|
||||
</li>
|
||||
</template>
|
||||
250
src/components/layouts/navbar/NavbarLayout.vue
Normal file
250
src/components/layouts/navbar/NavbarLayout.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
NavbarTheme,
|
||||
NavbarItem,
|
||||
NavbarItemMegamenu,
|
||||
NavbarItemDropdown,
|
||||
NavbarLayoutContext,
|
||||
} from './navbar.types'
|
||||
import { injectionKey } from './navbar.context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: NavbarItem[]
|
||||
theme?: NavbarTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
|
||||
const linksWithChildren = computed(() => {
|
||||
return props.links.filter(link => link.type === 'megamenu' || link.type === 'dropdown') as (NavbarItemMegamenu | NavbarItemDropdown)[]
|
||||
})
|
||||
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
const activeMobileSubsidebarId = ref<string>(linksWithChildren.value?.[0]?.id)
|
||||
const activeSubnavId = ref<string | undefined>()
|
||||
|
||||
const activeSubnav = computed(() => {
|
||||
return linksWithChildren.value.find(link => link.id === activeSubnavId.value)
|
||||
})
|
||||
const activeMobileSubsidebar = computed(() => {
|
||||
return linksWithChildren.value.find(link => link.id === activeMobileSubsidebarId.value)
|
||||
})
|
||||
|
||||
function toggleSubnav(id: string) {
|
||||
if (activeSubnavId.value === id) {
|
||||
activeSubnavId.value = undefined
|
||||
}
|
||||
else {
|
||||
activeSubnavId.value = id
|
||||
}
|
||||
}
|
||||
function toggleMobileSubnav(id: string) {
|
||||
if (activeMobileSubsidebarId.value === id) {
|
||||
isMobileSidebarOpen.value = false
|
||||
}
|
||||
else {
|
||||
activeMobileSubsidebarId.value = id
|
||||
isMobileSidebarOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// provide context to children
|
||||
const context: NavbarLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
theme: computed(() => props.theme),
|
||||
|
||||
isMobileSidebarOpen,
|
||||
activeMobileSubsidebarId,
|
||||
activeSubnavId,
|
||||
|
||||
activeSubnav,
|
||||
activeMobileSubsidebar,
|
||||
|
||||
toggleSubnav,
|
||||
toggleMobileSubnav,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
activeSubnavId.value = undefined
|
||||
isMobileSidebarOpen.value = false
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => Boolean(activeSubnav.value?.type === 'megamenu' || isMobileSidebarOpen.value), (value) => {
|
||||
if (value) {
|
||||
document.documentElement.classList.add('no-scroll')
|
||||
}
|
||||
else {
|
||||
document.documentElement.classList.remove('no-scroll')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-layout">
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSidebarOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<MobileSidebar
|
||||
:class="[isMobileSidebarOpen && 'is-active']"
|
||||
>
|
||||
<template #links>
|
||||
<slot name="navbar-links-mobile" v-bind="contextRx">
|
||||
<NavbarItemMobile
|
||||
v-for="link in props.links"
|
||||
:key="link.label"
|
||||
:link
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</MobileSidebar>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<KeepAlive>
|
||||
<NavbarSubsidebarMobile
|
||||
v-if="isMobileSidebarOpen && activeMobileSubsidebar?.children"
|
||||
:key="activeMobileSubsidebarId"
|
||||
:label="activeMobileSubsidebar.label"
|
||||
:items="activeMobileSubsidebar.children"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Navbar>
|
||||
<template #title>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div v-if="'logo' in $slots" class="separator" />
|
||||
<!-- <slot name="navbar-title" v-bind="contextRx">-->
|
||||
<!-- <h1 class="title is-5">-->
|
||||
<!-- {{ pageTitle }}-->
|
||||
<!-- </h1>-->
|
||||
<!-- </slot>-->
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<div class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<slot name="navbar-links" v-bind="contextRx">
|
||||
<div class="left-links">
|
||||
<NavbarItem
|
||||
v-for="link in props.links"
|
||||
:key="link.label"
|
||||
:link="link"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</Navbar>
|
||||
|
||||
<div
|
||||
class="navbar-subnavbar is-hidden-mobile"
|
||||
:class="[activeSubnav?.type === 'megamenu' && 'is-active']"
|
||||
>
|
||||
<NavbarMegamenu
|
||||
v-if="activeSubnav?.type === 'megamenu'"
|
||||
:key="activeSubnavId"
|
||||
:children="activeSubnav.children"
|
||||
class="is-active"
|
||||
>
|
||||
<template v-if="'megamenu-start' in $slots" #start>
|
||||
<slot name="megamenu-start" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-end' in $slots" #end>
|
||||
<slot name="megamenu-end" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-top' in $slots" #top>
|
||||
<slot name="megamenu-top" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-bottom' in $slots" #bottom>
|
||||
<slot name="megamenu-bottom" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarMegamenu>
|
||||
</div>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper full top-nav>
|
||||
<template v-if="props.size === 'full'">
|
||||
<div class="is-navbar-md">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<NavbarPageTitleMobile>
|
||||
<slot name="navbar-title-mobile" v-bind="contextRx">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent class="is-relative">
|
||||
<div class="is-navbar-md">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<NavbarPageTitleMobile>
|
||||
<slot name="navbar-title-mobile" v-bind="contextRx">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarPageTitleMobile>
|
||||
</slot>
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot name="extra" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/abstracts/all';
|
||||
@import '/@src/scss/layout/navbar';
|
||||
</style>
|
||||
245
src/components/layouts/navbar/NavbarMegamenu.vue
Normal file
245
src/components/layouts/navbar/NavbarMegamenu.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarMegamenu } from './navbar.types'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
children: NavbarMegamenu[]
|
||||
}>(), {
|
||||
children: () => [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-subnavbar-inner">
|
||||
<div v-if="'start' in $slots" class="menu-grid-start">
|
||||
<slot name="start" />
|
||||
</div>
|
||||
|
||||
<div class="menu-grid-wrapper">
|
||||
<div v-if="'top' in $slots" class="menu-grid-top">
|
||||
<slot name="top" />
|
||||
</div>
|
||||
<div class="menu-grid-container has-slimscroll">
|
||||
<div class="menu-grid">
|
||||
<div
|
||||
v-for="group in props.children"
|
||||
:key="group.id"
|
||||
class="menu-block"
|
||||
>
|
||||
<h4 class="block-heading">
|
||||
<VIcon
|
||||
:icon="group.icon"
|
||||
/>
|
||||
<span>{{ group.label }}</span>
|
||||
</h4>
|
||||
<ul class="block-links">
|
||||
<li v-for="link in group.children" :key="link.to">
|
||||
<VLink :to="link.to">
|
||||
<span>{{ link.label }}</span>
|
||||
<VTag
|
||||
v-if="link.tag"
|
||||
color="primary"
|
||||
size="tiny"
|
||||
outlined
|
||||
>
|
||||
{{ link.tag }}
|
||||
</VTag>
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="'bottom' in $slots" class="menu-grid-bottom">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="'end' in $slots" class="menu-grid-end">
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-subnavbar-inner {
|
||||
justify-content: space-between;
|
||||
|
||||
&.is-active {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-grid-start,
|
||||
.menu-grid-end {
|
||||
min-width: 260px;
|
||||
}
|
||||
// .menu-grid-wrapper-wrapper {
|
||||
// border-bottom: 4px solid green;
|
||||
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// flex-grow: 2;
|
||||
// // flex: 1 1 0;
|
||||
|
||||
// > div {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
.menu-grid-top,
|
||||
.menu-grid-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-grid-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// flex-wrap: wrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.menu-grid-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 4rem;
|
||||
padding: 1.5rem 0;
|
||||
|
||||
// display: grid;
|
||||
// grid-template-rows: 1fr 1fr 1fr;
|
||||
// grid-auto-flow: column dense;
|
||||
|
||||
&.is-horizontal {
|
||||
flex-direction: row;
|
||||
gap: 1rem 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-block {
|
||||
min-width: 170px;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.block-heading {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: top;
|
||||
gap: 0.5rem;
|
||||
|
||||
.iconify {
|
||||
font-size: 18px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.block-links {
|
||||
li {
|
||||
padding-inline-start: 26px;
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
height 0.3s,
|
||||
width 0.3s;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-radius: 0;
|
||||
|
||||
a {
|
||||
color: color-mix(in oklab, var(--primary), black 14%);
|
||||
|
||||
.iconify {
|
||||
opacity: 1;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.router-link-exact-active {
|
||||
color: color-mix(in oklab, var(--primary), black 14%);
|
||||
|
||||
.iconify {
|
||||
opacity: 1;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: color-mix(in oklab, var(--light-text), white 5%);
|
||||
gap: 0.225rem;
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir,
|
||||
.fas,
|
||||
.fal,
|
||||
.fab,
|
||||
.far {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
line-height: 1.6;
|
||||
height: 1.7em;
|
||||
font-size: 0.65rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-inline-start: 12px;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
stroke-width: 2px;
|
||||
fill: var(--primary);
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
height 0.3s,
|
||||
width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark .navbar-subnavbar-inner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
}
|
||||
</style>
|
||||
11
src/components/layouts/navbar/NavbarPageTitleMobile.vue
Normal file
11
src/components/layouts/navbar/NavbarPageTitleMobile.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="page-title has-text-centered">
|
||||
<div class="title-wrap">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="toolbar mobile-toolbar">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
43
src/components/layouts/navbar/NavbarSubsidebarMobile.vue
Normal file
43
src/components/layouts/navbar/NavbarSubsidebarMobile.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarDropdown, NavbarMegamenu } from './navbar.types'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
items: (NavbarDropdown | NavbarMegamenu)[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-subsidebar">
|
||||
<div class="inner">
|
||||
<div class="sidebar-title">
|
||||
<slot>
|
||||
<h3>{{ props.label }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="submenu has-slimscroll"
|
||||
>
|
||||
<template v-for="item of props.items">
|
||||
<VCollapseLinks
|
||||
v-if="'children' in item"
|
||||
:key="item.id"
|
||||
:links="item.children"
|
||||
>
|
||||
{{ item.label }}
|
||||
</VCollapseLinks>
|
||||
<li v-else-if="'to' in item" :key="item.label">
|
||||
<VLink :to="item.to">
|
||||
{{ item.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/mobile-subsidebar';
|
||||
</style>
|
||||
14
src/components/layouts/navbar/navbar.context.ts
Normal file
14
src/components/layouts/navbar/navbar.context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { NavbarLayoutContext } from './navbar.types'
|
||||
|
||||
export const injectionKey = Symbol('navbar-layout') as InjectionKey<NavbarLayoutContext>
|
||||
|
||||
export function useNavbarLayoutContext() {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useNavbarLayoutContext() is called outside of <NavbarLayout> tree.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
73
src/components/layouts/navbar/navbar.types.ts
Normal file
73
src/components/layouts/navbar/navbar.types.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export type NavbarTheme = 'default' | 'colored' | 'fade'
|
||||
|
||||
// -- Item
|
||||
export interface NavbarItemMegamenu {
|
||||
type: 'megamenu'
|
||||
label: string
|
||||
id: string
|
||||
icon: string
|
||||
|
||||
children: NavbarMegamenu[]
|
||||
}
|
||||
export interface NavbarItemDropdown {
|
||||
type: 'dropdown'
|
||||
label: string
|
||||
id: string
|
||||
icon: string
|
||||
|
||||
children: NavbarDropdown[]
|
||||
}
|
||||
export interface NavbarItemAction {
|
||||
type: 'action'
|
||||
label: string
|
||||
icon: string
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
export interface NavbarItemLink {
|
||||
type: 'link'
|
||||
label: string
|
||||
icon: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export type NavbarItem =
|
||||
| NavbarItemMegamenu
|
||||
| NavbarItemDropdown
|
||||
| NavbarItemAction
|
||||
| NavbarItemLink
|
||||
|
||||
// -- Item Megamenu
|
||||
export interface NavbarMegamenu {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
children: NavbarMegamenuLink[]
|
||||
}
|
||||
export interface NavbarMegamenuLink {
|
||||
label: string
|
||||
to: string
|
||||
tag?: string | number
|
||||
}
|
||||
|
||||
// -- Item Dropdown
|
||||
export interface NavbarDropdown {
|
||||
label: string
|
||||
to: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// -- Context
|
||||
export interface NavbarLayoutContext {
|
||||
theme: ComputedRef<NavbarTheme>
|
||||
links: ComputedRef<NavbarItem[]>
|
||||
|
||||
isMobileSidebarOpen: Ref<boolean>
|
||||
activeMobileSubsidebarId: Ref<string>
|
||||
activeSubnavId: Ref<string | undefined>
|
||||
|
||||
activeSubnav: ComputedRef<NavbarItem | undefined>
|
||||
activeMobileSubsidebar: ComputedRef<NavbarItem | undefined>
|
||||
|
||||
toggleSubnav: (id: string) => void
|
||||
toggleMobileSubnav: (id: string) => void
|
||||
}
|
||||
338
src/components/layouts/navsearch/Navsearch.vue
Normal file
338
src/components/layouts/navsearch/Navsearch.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div
|
||||
class="navbar-navbar-clean"
|
||||
>
|
||||
<div class="navbar-navbar-inner">
|
||||
<div class="left">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="center">
|
||||
<slot name="search" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="navbar-navbar-lower"
|
||||
:class="[
|
||||
'subtitle' in $slots && 'is-between',
|
||||
!('subtitle' in $slots) && 'is-centered',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="'subtitle' in $slots"
|
||||
class="left"
|
||||
>
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
!('subtitle' in $slots) && 'left',
|
||||
'subtitle' in $slots && 'center',
|
||||
]"
|
||||
>
|
||||
<slot name="links" />
|
||||
</div>
|
||||
<div
|
||||
v-if="'toolbar-bottom' in $slots"
|
||||
class="right"
|
||||
>
|
||||
<slot name="toolbar-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-navbar-clean {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
z-index: 15;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: var(--white);
|
||||
border-bottom-color: var(--fade-grey);
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.is-scrolled {
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
&:not(.is-scrolled) {
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button:not(:hover) {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25%;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.95rem;
|
||||
color: var(--muted-grey);
|
||||
letter-spacing: 1px;
|
||||
max-width: 50px;
|
||||
line-height: 1.2;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 38px;
|
||||
width: 2px;
|
||||
border-inline-end: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
margin: 0 20px 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 25%;
|
||||
margin-inline-start: auto;
|
||||
|
||||
.icon-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
border-radius: var(--radius-rounded);
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--fade-grey);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
font-size: 18px;
|
||||
stroke-width: 1.6px;
|
||||
color: var(--light-text);
|
||||
transition: stroke 0.3s;
|
||||
vertical-align: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
justify-content: space-between;
|
||||
|
||||
.left,
|
||||
.right, {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.left,
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.router-link-exact-active {
|
||||
background: color-mix(in oklab, var(--widget-grey), black 2%);
|
||||
color: var(--dark-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.avatar-stack {
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.button {
|
||||
&.is-circle {
|
||||
min-width: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
.left,
|
||||
.right {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.navbar-navbar-clean {
|
||||
&:not(.is-colored) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
|
||||
&:not(.is-scrolled) {
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button:not(:hover) {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.icon-link {
|
||||
background: var(--landing-yyy);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
stroke: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/components/layouts/navsearch/NavsearchInput.vue
Normal file
88
src/components/layouts/navsearch/NavsearchInput.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
const props = withDefaults(defineProps<{
|
||||
suggestions?: T[]
|
||||
}>(), {
|
||||
suggestions: () => [],
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
select: [item: T]
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="centered-search">
|
||||
<div class="field">
|
||||
<div class="control has-icon">
|
||||
<input
|
||||
v-model="modelValue"
|
||||
type="text"
|
||||
class="input search-input"
|
||||
placeholder="Search records..."
|
||||
>
|
||||
<div class="form-icon">
|
||||
<VIcon
|
||||
icon="lucide:search"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="form-icon is-right"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="modelValue = ''"
|
||||
@click="modelValue = ''"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:x"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.suggestions.length"
|
||||
class="search-results has-slimscroll is-active"
|
||||
>
|
||||
<a
|
||||
v-for="(item, key) in props.suggestions"
|
||||
:key="key"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="search-result"
|
||||
@click="() => emits('select', item)"
|
||||
@keydown.enter.prevent="() => emits('select', item)"
|
||||
>
|
||||
<slot v-bind="{ item }" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centered-search {
|
||||
width: 100%;
|
||||
|
||||
.field {
|
||||
margin-bottom: 0;
|
||||
|
||||
.control {
|
||||
.input {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
&.is-right {
|
||||
inset-inline-start: unset !important;
|
||||
inset-inline-end: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
247
src/components/layouts/navsearch/NavsearchLayout.vue
Normal file
247
src/components/layouts/navsearch/NavsearchLayout.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
NavsearchTheme,
|
||||
NavsearchItem,
|
||||
NavsearchScrollBehavior,
|
||||
NavsearchLayoutContext,
|
||||
} from './navsearch.types'
|
||||
import { injectionKey } from './navsearch.context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: NavsearchItem[]
|
||||
theme?: NavsearchTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
scrollBehavior?: NavsearchScrollBehavior
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
scrollBehavior: 'fixed',
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const threshold = 25 // half of the navbar height
|
||||
let lastScrollY = 0
|
||||
|
||||
const direction = ref<'up' | 'down'>('down')
|
||||
const isScrolled = computed(() => {
|
||||
return y.value > threshold
|
||||
})
|
||||
|
||||
// provide context to children
|
||||
const context: NavsearchLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
theme: computed(() => props.theme),
|
||||
scrollBehavior: computed(() => props.scrollBehavior),
|
||||
|
||||
isMobileSidebarOpen,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
watch(y, (value) => {
|
||||
if (lastScrollY < value) {
|
||||
direction.value = 'down'
|
||||
}
|
||||
else {
|
||||
direction.value = 'up'
|
||||
}
|
||||
|
||||
lastScrollY = value
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
isMobileSidebarOpen.value = false
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="navbar-layout navbar-layout-search"
|
||||
:class="[
|
||||
...(props.scrollBehavior === 'shrink' ? [
|
||||
'is-shrink',
|
||||
isScrolled ? 'is-scrolled' : '',
|
||||
] : []),
|
||||
|
||||
...(props.scrollBehavior === 'reveal' ? [
|
||||
'is-reveal',
|
||||
isScrolled && direction === 'down' ? 'is-scrolled' : '',
|
||||
] : []),
|
||||
]"
|
||||
>
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSidebarOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<NavsearchSubsidebarMobile
|
||||
v-if="isMobileSidebarOpen"
|
||||
:items="props.links"
|
||||
>
|
||||
<slot name="navbar-content" v-bind="contextRx" />
|
||||
|
||||
<template #links>
|
||||
<slot name="navbar-links-mobile" />
|
||||
</template>
|
||||
</NavsearchSubsidebarMobile>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Navsearch
|
||||
:class="[
|
||||
props.theme === 'fade' && 'is-transparent',
|
||||
props.theme === 'fade' && isScrolled && 'is-scrolled',
|
||||
]"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
<div v-if="'logo' in $slots" class="separator" />
|
||||
<slot name="navbar-title" v-bind="contextRx">
|
||||
<h1 class="title is-6">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #search>
|
||||
<slot name="navbar-content" v-bind="contextRx" />
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<div class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="'subnav-start' in $slots" #subtitle>
|
||||
<slot name="subnav-start" v-bind="contextRx" />
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<slot name="subnav-links" v-bind="contextRx">
|
||||
<div class="buttons">
|
||||
<VLink
|
||||
v-for="link in props.links"
|
||||
:key="link.to"
|
||||
:to="link.to"
|
||||
class="button"
|
||||
>
|
||||
{{ link.label }}
|
||||
</VLink>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="'subnav-end' in $slots" #toolbar-bottom>
|
||||
<slot name="subnav-end" v-bind="contextRx" />
|
||||
</template>
|
||||
</Navsearch>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper top-nav>
|
||||
<template v-if="props.size === 'full'">
|
||||
<div class="is-navbar-lg">
|
||||
<slot name="page-heading">
|
||||
<NavsearchPageTitleMobile>
|
||||
<slot v-bind="contextRx" name="title-mobile">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot v-bind="contextRx" name="toolbar" />
|
||||
</template>
|
||||
</NavsearchPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent
|
||||
class="is-relative"
|
||||
>
|
||||
<div class="is-navbar-lg">
|
||||
<slot name="page-heading">
|
||||
<NavsearchPageTitleMobile>
|
||||
<slot v-bind="contextRx" name="title-mobile">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot v-bind="contextRx" name="toolbar" />
|
||||
</template>
|
||||
</NavsearchPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot v-bind="contextRx" name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.view-wrapper.has-top-nav) {
|
||||
.is-stuck {
|
||||
top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-shrink,
|
||||
.is-reveal {
|
||||
.navbar-navbar-clean {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.is-shrink {
|
||||
&.is-scrolled {
|
||||
.navbar-navbar-clean:not(:has(.navbar-navbar-inner:focus-within)) {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-reveal {
|
||||
&.is-scrolled {
|
||||
.navbar-navbar-clean:not(:focus-within) {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="page-title has-text-centered">
|
||||
<!-- Mobile Page Title -->
|
||||
<div class="title-wrap">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="toolbar mobile-toolbar">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavsearchItem } from './navsearch.types'
|
||||
|
||||
const props = defineProps<{
|
||||
items?: NavsearchItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-subsidebar">
|
||||
<div class="inner">
|
||||
<div v-if="'default' in $slots" class="sidebar-title">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="'default' in $slots" class="navbar-divider" />
|
||||
|
||||
<ul
|
||||
class="submenu has-slimscroll"
|
||||
>
|
||||
<slot name="links">
|
||||
<li v-for="item of props.items" :key="item.to">
|
||||
<VLink :to="item.to">
|
||||
{{ item.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
</slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile-subsidebar .inner {
|
||||
margin-inline-start: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/mobile-subsidebar';
|
||||
</style>
|
||||
14
src/components/layouts/navsearch/navsearch.context.ts
Normal file
14
src/components/layouts/navsearch/navsearch.context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { NavsearchLayoutContext } from './navsearch.types'
|
||||
|
||||
export const injectionKey = Symbol('navsearch-layout') as InjectionKey<NavsearchLayoutContext>
|
||||
|
||||
export function useNavsearchLayoutContext() {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useNavsearchLayoutContext() is called outside of <NavsearchLayout> tree.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
17
src/components/layouts/navsearch/navsearch.types.ts
Normal file
17
src/components/layouts/navsearch/navsearch.types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// -- Navsearch
|
||||
export type NavsearchTheme = 'default' | 'fade'
|
||||
export type NavsearchScrollBehavior = 'reveal' | 'shrink' | 'fixed'
|
||||
|
||||
export interface NavsearchItem {
|
||||
to: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// -- Context
|
||||
export interface NavsearchLayoutContext {
|
||||
theme: ComputedRef<NavsearchTheme>
|
||||
scrollBehavior: ComputedRef<NavsearchScrollBehavior>
|
||||
links: ComputedRef<NavsearchItem[]>
|
||||
|
||||
isMobileSidebarOpen: Ref<boolean>
|
||||
}
|
||||
58
src/components/layouts/shared/PageContent.vue
Normal file
58
src/components/layouts/shared/PageContent.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* ==========================================================================
|
||||
3. Page Content
|
||||
========================================================================== */
|
||||
.page-content {
|
||||
padding: 0 40px;
|
||||
|
||||
&.is-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.kanban-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&.chat-content {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
&.card-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&.waterfall-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.projects-content {
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (device-width >= 768px) and (device-width <= 1024px) and (orientation: portrait) {
|
||||
.page-content {
|
||||
padding: 0 10px;
|
||||
|
||||
&.is-explore {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.page-content {
|
||||
padding: 0 10px !important;
|
||||
|
||||
&.projects-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/components/layouts/shared/PageContentWrapper.vue
Normal file
86
src/components/layouts/shared/PageContentWrapper.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
size?: 'default' | 'large' | 'wide'
|
||||
}>(), {
|
||||
size: 'default',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-content-wrapper" :class="[`is-size-${props.size}`]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* ==========================================================================
|
||||
3. Page Content
|
||||
========================================================================== */
|
||||
|
||||
.page-content-wrapper {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.is-size-default {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
&.is-size-large {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
&.is-size-wide {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 0 40px;
|
||||
|
||||
&.is-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.kanban-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&.chat-content {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
&.card-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&.waterfall-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.projects-content {
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable this setting if you want the dashboard to be larger
|
||||
@media only screen and (width >= 1408px) {
|
||||
.page-content-wrapper {
|
||||
&.is-size-large {
|
||||
max-width: 1380px;
|
||||
}
|
||||
|
||||
&.is-size-wide {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (device-width >= 768px) and (device-width <= 1024px) and (orientation: portrait) {
|
||||
.page-content-wrapper {
|
||||
.page-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
193
src/components/layouts/shared/ViewWrapper.vue
Normal file
193
src/components/layouts/shared/ViewWrapper.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
topNav?: boolean
|
||||
full?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="view-wrapper"
|
||||
:class="[
|
||||
props.topNav && 'has-top-nav',
|
||||
props.full && 'view-wrapper-full',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.view-wrapper {
|
||||
&.has-top-nav {
|
||||
margin-inline-start: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-top: 1px;
|
||||
|
||||
.is-stuck {
|
||||
position: fixed;
|
||||
top: 78px;
|
||||
inset-inline-start: 0;
|
||||
margin-inline-start: 0;
|
||||
border-inline-start: 0 !important;
|
||||
width: 100%;
|
||||
z-index: 14;
|
||||
|
||||
&.stuck-header {
|
||||
padding-inline-end: 20px !important;
|
||||
|
||||
.form-head-inner,
|
||||
.form-header-inner {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.has-navbar-spacing {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.is-navbar-md {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.is-navbar-lg {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.is-navbar-xl {
|
||||
margin-top: 130px;
|
||||
}
|
||||
|
||||
.page-content-wrapper {
|
||||
padding-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.view-wrapper-full {
|
||||
width: 100%;
|
||||
margin-inline-start: 0;
|
||||
|
||||
&.is-pushed-block {
|
||||
margin-inline-start: 280px;
|
||||
width: calc(100% - 280px);
|
||||
|
||||
.is-stuck {
|
||||
margin-inline-start: 280px;
|
||||
width: calc(100% - 280px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
width: calc(100% - 80px);
|
||||
// padding: 0 0 60px;
|
||||
margin-inline-start: 80px;
|
||||
background: var(--background-grey);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
// 25.05.07 수정 및 주석
|
||||
//&.is-pushed-full {
|
||||
// margin-inline-start: 320px;
|
||||
// width: calc(100% - 320px);
|
||||
//
|
||||
// .is-stuck {
|
||||
// margin-inline-start: 320px;
|
||||
// width: calc(100% - 320px);
|
||||
// }
|
||||
//}
|
||||
|
||||
.is-stuck {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
margin-inline-start: 80px;
|
||||
width: calc(100% - 80px);
|
||||
z-index: 14;
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.view-wrapper {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.view-wrapper {
|
||||
&.has-top-nav {
|
||||
.is-navbar-md,
|
||||
.is-navbar-lg,
|
||||
.is-navbar-xl {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.page-content-wrapper {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.view-wrapper {
|
||||
&.has-top-nav {
|
||||
.is-navbar-md,
|
||||
.is-navbar-lg,
|
||||
.is-navbar-xl {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.view-wrapper {
|
||||
width: calc(100% - 60px) !important;
|
||||
margin-inline-start: 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
// Layout
|
||||
.view-wrapper {
|
||||
width: 100% !important;
|
||||
margin-inline-start: 0 !important;
|
||||
margin-top: 60px !important;
|
||||
padding-inline-start: 40px;
|
||||
padding-inline-end: 40px;
|
||||
|
||||
&.is-explore {
|
||||
padding-inline-start: 0 !important;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
// Layout
|
||||
.view-wrapper {
|
||||
width: 100% !important;
|
||||
margin-inline-start: 0 !important;
|
||||
margin-top: 60px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
src/components/layouts/shared/mobile/MobileBurger.vue
Normal file
43
src/components/layouts/shared/mobile/MobileBurger.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<boolean>({
|
||||
default: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="navbar-burger burger is-hidden-desktop"
|
||||
:class="[modelValue && 'is-active']"
|
||||
role="button"
|
||||
aria-label="menu"
|
||||
aria-expanded="false"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="modelValue = !modelValue"
|
||||
@click="modelValue = !modelValue"
|
||||
>
|
||||
<span aria-hidden="true" />
|
||||
<span aria-hidden="true" />
|
||||
<span aria-hidden="true" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.navbar-burger {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
margin-inline-start: 0 !important;
|
||||
background: transparent !important;
|
||||
display: flex;
|
||||
|
||||
.is-active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: rgb(0 0 0 / 2%);
|
||||
}
|
||||
|
||||
span {
|
||||
height: 2px;
|
||||
background-color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
438
src/components/layouts/shared/mobile/MobileNavbar.vue
Normal file
438
src/components/layouts/shared/mobile/MobileNavbar.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<boolean>({
|
||||
default: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="navbar mobile-navbar is-hidden-desktop is-hidden-tablet">
|
||||
<div class="container">
|
||||
<!-- Brand -->
|
||||
<div class="navbar-brand">
|
||||
<!-- Mobile menu toggler icon -->
|
||||
<div class="brand-start">
|
||||
<MobileBurger v-model="modelValue" />
|
||||
</div>
|
||||
|
||||
<slot name="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.mobile-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
display: none;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 12%);
|
||||
transition: all 0.3s; // transition-all test
|
||||
margin: 0;
|
||||
|
||||
&.no-shadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
.is-brand {
|
||||
img {
|
||||
position: relative;
|
||||
height: 32px !important;
|
||||
max-height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-dropdown {
|
||||
.dropdown-menu {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
&.has-icon {
|
||||
padding: 0.75rem !important;
|
||||
border-bottom: 1px solid var(--fade-grey);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
.path {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-sidebar-toggler {
|
||||
.iconify {
|
||||
color: var(--muted-grey) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
|
||||
&.menu-badge {
|
||||
color: var(--primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: var(--radius-rounded);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 90%;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-notification {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
border-radius: var(--radius-rounded);
|
||||
margin-inline-end: 12px;
|
||||
|
||||
.navbar-link {
|
||||
padding: 0;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 18px;
|
||||
color: var(--muted-grey);
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
|
||||
.new-indicator {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
inset-inline-end: -9px;
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-rounded);
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
box-shadow: 0 3px 10px 4px rgb(0 0 0 / 7%);
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
position: fixed;
|
||||
padding-bottom: 15px;
|
||||
top: 68px;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
width: 96%;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.count,
|
||||
.view-all {
|
||||
font-size: 0.8rem;
|
||||
color: var(--danger);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 0.01rem solid var(--light-grey);
|
||||
|
||||
.heading-left {
|
||||
h6 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.heading-right {
|
||||
.notification-link {
|
||||
margin: 0.4rem 0;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 264px;
|
||||
overflow: auto;
|
||||
|
||||
.notification-list {
|
||||
list-style-type: none;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0 0.5rem;
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.65rem 0;
|
||||
|
||||
.img-left {
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 3rem;
|
||||
max-height: 3rem;
|
||||
width: 3rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
border-radius: var(--radius-rounded);
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-content {
|
||||
text-align: inset-inline-start;
|
||||
|
||||
.user-info {
|
||||
color: var(--dark-text);
|
||||
margin: 0.15rem 0 0;
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
margin: 0;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
background: var(--white);
|
||||
|
||||
.navbar-item,
|
||||
.navbar-link {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-inline-end: 10px !important;
|
||||
|
||||
&.is-active {
|
||||
.link-chevron {
|
||||
transform: rotate(calc(var(--transform-direction) * 90deg)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
max-height: 36px !important;
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 10px;
|
||||
|
||||
&.is-heading {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--dark);
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.is-subheading {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: var(--muted-grey);
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.is-block span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.link-chevron {
|
||||
margin-inline-start: auto;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.3s; // transition-all test
|
||||
transform: rotate(calc(var(--transform-direction) * 0));
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown .navbar-item {
|
||||
font-size: 95%;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
color: var(--muted-grey);
|
||||
|
||||
&.is-active,
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklab, var(--placeholder), white 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. Mobile Navbar Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.mobile-navbar {
|
||||
background: var(--dark-sidebar);
|
||||
|
||||
.navbar-menu.is-active {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 3%);
|
||||
|
||||
.navbar-link {
|
||||
.is-heading {
|
||||
color: color-mix(in oklab, var(--primary-grey), white 10%);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item.has-icon {
|
||||
border-bottom-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
}
|
||||
|
||||
.navbar-dropdown .navbar-item {
|
||||
color: color-mix(in oklab, var(--primary-grey), black 5%) !important;
|
||||
}
|
||||
|
||||
.is-search .control {
|
||||
input {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: var(--primary-grey);
|
||||
|
||||
&:focus {
|
||||
~ span .iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-notification {
|
||||
&.is-active {
|
||||
.navbar-link {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
background: var(--dark-sidebar) !important;
|
||||
border-color: var(--dark-sidebar) !important;
|
||||
|
||||
.heading {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
|
||||
.heading-right {
|
||||
.notification-link {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
.notification-list {
|
||||
li {
|
||||
.notification-item {
|
||||
.user-content {
|
||||
p {
|
||||
color: var(--dark-dark-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (device-width >= 768px) and (device-width <= 1024px) and (orientation: portrait) {
|
||||
.mobile-navbar {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.mobile-navbar {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
src/components/layouts/shared/mobile/MobileOverlay.vue
Normal file
16
src/components/layouts/shared/mobile/MobileOverlay.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mobile-overlay" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.mobile-overlay {
|
||||
background: rgb(0 0 0 / 30%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
z-index: 20;
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
</style>
|
||||
150
src/components/layouts/shared/mobile/MobileSidebar.vue
Normal file
150
src/components/layouts/shared/mobile/MobileSidebar.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-main-sidebar">
|
||||
<div class="inner">
|
||||
<ul class="icon-side-menu">
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
|
||||
<ul class="bottom-icon-side-menu">
|
||||
<slot name="links-bottom" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.mobile-main-sidebar {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
inset-inline-start: 0;
|
||||
height: calc(100% - 60px);
|
||||
width: 60px;
|
||||
background: var(--white);
|
||||
border-top: 1px solid var(--fade-grey);
|
||||
border-inline-end: 1px solid var(--fade-grey);
|
||||
z-index: 100;
|
||||
transform: translateX(calc(var(--transform-direction) * -100%));
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-active {
|
||||
transform: translateX(calc(var(--transform-direction) * 0));
|
||||
}
|
||||
|
||||
.inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.icon-side-menu,
|
||||
.bottom-icon-side-menu {
|
||||
li {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
transform: rotate(calc(var(--transform-direction) * 0));
|
||||
opacity: 1;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
> .iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--title-grey);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
|
||||
&:hover .iconify,
|
||||
&.is-active .iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.is-opened {
|
||||
transform: rotate(calc(var(--transform-direction) * 360deg));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
transform: rotate(calc(var(--transform-direction) * -360deg));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.is-selected,
|
||||
&.router-link-exact-active {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#open-filters {
|
||||
.iconify {
|
||||
transform: rotate(calc(var(--transform-direction) * 0));
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.iconify {
|
||||
transform: rotate(calc(var(--transform-direction) * 145deg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
a .iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-icon-side-menu {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.mobile-main-sidebar {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 6%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%) !important;
|
||||
|
||||
.inner {
|
||||
.icon-side-menu {
|
||||
li {
|
||||
a {
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
525
src/components/layouts/sidebar/Sidebar.vue
Normal file
525
src/components/layouts/sidebar/Sidebar.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebarLayoutContext } from './sidebar.context'
|
||||
|
||||
const {
|
||||
theme,
|
||||
isOpen,
|
||||
} = useSidebarLayoutContext()
|
||||
|
||||
const themeClasses = computed(() => {
|
||||
switch (theme.value) {
|
||||
case 'color':
|
||||
return 'is-colored'
|
||||
case 'labels':
|
||||
return 'has-labels'
|
||||
case 'labels-hover':
|
||||
return 'has-labels has-hover-labels'
|
||||
case 'float':
|
||||
return !isOpen.value ? 'is-float' : 'is-float is-bordered'
|
||||
case 'curved':
|
||||
return !isOpen.value ? 'is-curved' : ''
|
||||
case 'color-curved':
|
||||
return !isOpen.value ? 'is-colored is-curved' : 'is-colored'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="main-sidebar"
|
||||
:class="[themeClasses]"
|
||||
>
|
||||
<div v-if="'logo' in $slots" class="sidebar-brand">
|
||||
<slot name="logo" />
|
||||
</div>
|
||||
<div class="sidebar-inner">
|
||||
<div class="naver" />
|
||||
|
||||
<ul class="icon-menu has-slimscroll">
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
|
||||
<!-- User account -->
|
||||
<ul class="bottom-menu">
|
||||
<slot name="links-bottom" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.main-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
margin-inline-start: 0;
|
||||
height: 100vh;
|
||||
width: 80px;
|
||||
background-color: var(--body-color);
|
||||
box-shadow: none;
|
||||
z-index: 35;
|
||||
transition:
|
||||
border-radius 0.3s ease-in,
|
||||
background-color 0.3s ease-in,
|
||||
top 0.3s ease-in,
|
||||
margin-inline-start 0.3s ease-in,
|
||||
height 0.3s ease-in;
|
||||
|
||||
&.is-bordered {
|
||||
border-inline-end: 1px solid var(--fade-grey) !important;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
box-shadow: 2px 0 2px 0 rgb(0 0 0 / 2%);
|
||||
}
|
||||
|
||||
&.is-curved {
|
||||
&:not(.is-bordered) {
|
||||
border-start-end-radius: 20px;
|
||||
border-end-end-radius: 20px;
|
||||
border-inline-end: 1px solid color-mix(in oklab, var(--fade-grey), black 4%) !important;
|
||||
|
||||
.sidebar-brand {
|
||||
border-start-end-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-colored {
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 2%);
|
||||
background: var(--landing-yyy);
|
||||
|
||||
.sidebar-inner {
|
||||
.naver {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
a {
|
||||
&:hover,
|
||||
&.is-active,
|
||||
&.router-link-active {
|
||||
.sidebar-svg {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-svg {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-labels {
|
||||
&.has-hover-labels {
|
||||
.sidebar-inner {
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
&:hover {
|
||||
a {
|
||||
&::after {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&.is-active,
|
||||
&.router-link-active {
|
||||
&::after {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
overflow-x: hidden;
|
||||
|
||||
li {
|
||||
a {
|
||||
&.router-link-active {
|
||||
&::after {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: attr(data-content);
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
inset-inline-start: -29px;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
font-family: var(--font);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-float {
|
||||
border-radius: 1000px;
|
||||
overflow: hidden;
|
||||
width: 74px;
|
||||
margin-inline-start: 6px;
|
||||
top: 6px;
|
||||
height: calc(100vh - 12px);
|
||||
border: none !important;
|
||||
|
||||
&:not(.is-bordered) {
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
&.is-bordered {
|
||||
width: 80px;
|
||||
margin-inline-start: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
|
||||
.sidebar-brand {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
width: 74px;
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
width: 74px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-menu {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin-top: 5px;
|
||||
width: 36px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
height: calc(100% - 60px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.naver {
|
||||
position: absolute;
|
||||
top: -150px;
|
||||
inset-inline-start: 0;
|
||||
height: 64px;
|
||||
width: 4px;
|
||||
border-radius: 100px;
|
||||
background: var(--primary);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-search-results {
|
||||
margin-top: 240px;
|
||||
}
|
||||
|
||||
&.from-bottom {
|
||||
top: unset !important;
|
||||
bottom: -64px;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-active,
|
||||
&.router-link-active {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.is-messages-counter {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
inset-inline-end: 16px;
|
||||
display: block;
|
||||
line-height: 17px;
|
||||
background: var(--danger);
|
||||
color: var(--white);
|
||||
font-weight: 500;
|
||||
font-size: 0.6rem;
|
||||
border-radius: 100px;
|
||||
border: 1px solid var(--white);
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
position: relative;
|
||||
transform: rotate(calc(var(--transform-direction) * 0));
|
||||
opacity: 1;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover,
|
||||
&.is-selected,
|
||||
&.router-link-active {
|
||||
.sidebar-svg {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-svg {
|
||||
font-size: 20px;
|
||||
color: var(--title-grey);
|
||||
stroke-width: 1.6px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
|
||||
&:hover .iconify,
|
||||
&.is-active .iconify,
|
||||
&.router-link-exact-active .iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.is-opened {
|
||||
transform: rotate(calc(var(--transform-direction) * 360deg));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
transform: rotate(calc(var(--transform-direction) * -360deg));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-menu {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-rotate {
|
||||
a:hover {
|
||||
animation: rotating 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.dropdown-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 48px;
|
||||
width: 48px !important;
|
||||
|
||||
> img {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: var(--radius-rounded);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 2px solid var(--white);
|
||||
background: var(--success);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.main-sidebar:not(.is-colored) {
|
||||
background: var(--dark-sidebar);
|
||||
|
||||
&.is-bordered {
|
||||
border-inline-end: 1px solid var(--dark-sidebar) !important;
|
||||
}
|
||||
|
||||
&.is-curved {
|
||||
&:not(.is-bordered) {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.naver {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
a {
|
||||
&.is-selected,
|
||||
&.router-link-active {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.sidebar-icon .path {
|
||||
fill: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.sidebar-icon .path {
|
||||
fill: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-icon.is-active .path {
|
||||
fill: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: color-mix(in oklab, var(--primary-grey), white 3%);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
border-color: var(--dark-sidebar) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
a {
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-sidebar {
|
||||
&.is-colored {
|
||||
// background: color-mix(in oklab, var(--primary), black 2%);
|
||||
// border-color: color-mix(in oklab, var(--primary), black 2%) !important;
|
||||
|
||||
.sidebar-inner {
|
||||
.naver {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.icon-menu,
|
||||
.bottom-menu {
|
||||
li {
|
||||
a {
|
||||
&:hover,
|
||||
&.is-active {
|
||||
.sidebar-svg {
|
||||
color: var(--smoke-white);
|
||||
stroke: var(--smoke-white);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-svg {
|
||||
color: color-mix(in oklab, var(--smoke-white), white 2%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
src/components/layouts/sidebar/SidebarItem.vue
Normal file
60
src/components/layouts/sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import type { SidebarItem, SidebarItemAction } from './sidebar.types'
|
||||
import { useSidebarLayoutContext } from './sidebar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: SidebarItem
|
||||
}>()
|
||||
|
||||
const {
|
||||
activeSubsidebarId,
|
||||
toggleSubsidebar,
|
||||
} = useSidebarLayoutContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="props.link.type === 'subsidebar'"
|
||||
:class="[activeSubsidebarId === props.link.id && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:data-content="props.link.label"
|
||||
@keydown.enter.prevent="() => toggleSubsidebar(props.link.id)"
|
||||
@click.prevent="() => toggleSubsidebar(props.link.id)"
|
||||
>
|
||||
<VIcon
|
||||
class="sidebar-svg"
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<component
|
||||
:is="props.link.component"
|
||||
v-else-if="props.link.type === 'component'"
|
||||
:title="props.link.label"
|
||||
/>
|
||||
<VLink
|
||||
v-else-if="props.link.type === 'link'"
|
||||
:title="props.link.label"
|
||||
:to="props.link.to"
|
||||
:data-content="props.link.label"
|
||||
>
|
||||
<VIcon
|
||||
class="sidebar-svg"
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</VLink>
|
||||
<a
|
||||
v-else-if="props.link.type === 'action'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:title="props.link.label"
|
||||
:data-content="props.link.label"
|
||||
@click="(props.link as SidebarItemAction).onClick"
|
||||
@keydown.enter="(props.link as SidebarItemAction).onClick"
|
||||
>
|
||||
<VIcon
|
||||
class="sidebar-svg"
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
256
src/components/layouts/sidebar/SidebarLayout.vue
Normal file
256
src/components/layouts/sidebar/SidebarLayout.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<script setup lang="ts">
|
||||
import type { SidebarLayoutContext, SidebarItem, SidebarItemSubsidebar, SidebarTheme } from './sidebar.types'
|
||||
import { injectionKey } from './sidebar.context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: SidebarItem[]
|
||||
linksBottom?: SidebarItem[]
|
||||
theme?: SidebarTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
defaultSidebar?: string
|
||||
closeOnChange?: boolean
|
||||
openOnMounted?: boolean
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
linksBottom: () => [],
|
||||
defaultSidebar: '',
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
const isDesktopSidebarOpen = ref(props.openOnMounted)
|
||||
const activeSubsidebarId = ref(props.defaultSidebar)
|
||||
|
||||
const subsidebars = computed(() =>
|
||||
props.links.filter(item => item.type === 'subsidebar') as SidebarItemSubsidebar[],
|
||||
)
|
||||
|
||||
const activeSubsidebar = computed(() => {
|
||||
if (!activeSubsidebarId.value || subsidebars.value.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return subsidebars.value?.find(item => item.id === activeSubsidebarId.value)
|
||||
})
|
||||
const isOpen = computed(() => {
|
||||
return Boolean(activeSubsidebar.value && (isMobileSidebarOpen.value || isDesktopSidebarOpen.value))
|
||||
})
|
||||
|
||||
function toggleSubsidebar(id: string) {
|
||||
if (id === activeSubsidebarId.value) {
|
||||
isDesktopSidebarOpen.value = !isDesktopSidebarOpen.value
|
||||
}
|
||||
else {
|
||||
isDesktopSidebarOpen.value = true
|
||||
activeSubsidebarId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
// provide context to children
|
||||
const context: SidebarLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
linksBottom: computed(() => props.linksBottom),
|
||||
theme: computed(() => props.theme),
|
||||
defaultSidebar: computed(() => props.defaultSidebar),
|
||||
closeOnChange: computed(() => props.closeOnChange),
|
||||
openOnMounted: computed(() => props.openOnMounted),
|
||||
|
||||
isMobileSidebarOpen,
|
||||
isDesktopSidebarOpen,
|
||||
activeSubsidebarId,
|
||||
|
||||
subsidebars,
|
||||
activeSubsidebar,
|
||||
isOpen,
|
||||
|
||||
toggleSubsidebar,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
// close subsidebar when route changes
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
isMobileSidebarOpen.value = false
|
||||
|
||||
if (props.closeOnChange) {
|
||||
isDesktopSidebarOpen.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-layout">
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSidebarOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<MobileSidebar
|
||||
:class="[activeSubsidebar && isMobileSidebarOpen && 'is-active']"
|
||||
>
|
||||
<template v-if="props.links.length" #links>
|
||||
<slot name="sidebar-links-mobile" v-bind="contextRx">
|
||||
<li
|
||||
v-for="link in props.links"
|
||||
:key="link.id"
|
||||
:class="[link.hideMobile ? 'is-hidden-mobile' : '']"
|
||||
>
|
||||
<SidebarItem :link />
|
||||
</li>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="props.linksBottom.length" #links-bottom>
|
||||
<slot name="sidebar-links-bottom-mobile" v-bind="contextRx">
|
||||
<li
|
||||
v-for="link in props.linksBottom"
|
||||
:key="link.id"
|
||||
:class="[link.hideMobile ? 'is-hidden-mobile' : '']"
|
||||
>
|
||||
<SidebarItem :link />
|
||||
</li>
|
||||
</slot>
|
||||
</template>
|
||||
</MobileSidebar>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<KeepAlive>
|
||||
<SidebarSubsidebarMobile
|
||||
v-if="activeSubsidebar && isMobileSidebarOpen"
|
||||
:key="activeSubsidebar.id"
|
||||
:items="activeSubsidebar.subsidebar.items"
|
||||
:label="activeSubsidebar.subsidebar.label"
|
||||
>
|
||||
<slot name="subsidebar-title-mobile" v-bind="contextRx" />
|
||||
</SidebarSubsidebarMobile>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Sidebar
|
||||
:theme="props.theme"
|
||||
:is-open="activeSubsidebar && isDesktopSidebarOpen"
|
||||
>
|
||||
<template
|
||||
v-if="'logo' in $slots"
|
||||
#logo
|
||||
>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="props.links.length" #links>
|
||||
<slot name="sidebar-links" v-bind="contextRx">
|
||||
<li
|
||||
v-for="link in props.links"
|
||||
:key="link.id"
|
||||
>
|
||||
<SidebarItem :link />
|
||||
</li>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="props.linksBottom.length" #links-bottom>
|
||||
<slot name="sidebar-links-bottom" v-bind="contextRx">
|
||||
<li
|
||||
v-for="link in props.linksBottom"
|
||||
:key="link.id"
|
||||
>
|
||||
<SidebarItem :link />
|
||||
</li>
|
||||
</slot>
|
||||
</template>
|
||||
</Sidebar>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<KeepAlive>
|
||||
<SidebarSubsidebar
|
||||
v-if="activeSubsidebar && isDesktopSidebarOpen"
|
||||
:key="activeSubsidebar.id"
|
||||
:items="activeSubsidebar.subsidebar.items"
|
||||
:label="activeSubsidebar.subsidebar.label"
|
||||
@close="isDesktopSidebarOpen = false"
|
||||
>
|
||||
<slot name="subsidebar-title" v-bind="contextRx" />
|
||||
</SidebarSubsidebar>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper
|
||||
:class="[
|
||||
activeSubsidebar && isDesktopSidebarOpen && 'is-pushed-full',
|
||||
]"
|
||||
>
|
||||
<template v-if="props.size === 'full'">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<SidebarPageHeading
|
||||
:open="activeSubsidebar && isDesktopSidebarOpen"
|
||||
@toggle="isDesktopSidebarOpen = !isDesktopSidebarOpen"
|
||||
>
|
||||
<span>{{ pageTitle }}</span>
|
||||
|
||||
<template #toolbar>
|
||||
<slot
|
||||
name="toolbar"
|
||||
v-bind="contextRx"
|
||||
/>
|
||||
</template>
|
||||
</SidebarPageHeading>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent
|
||||
class="is-relative"
|
||||
>
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<SidebarPageHeading
|
||||
:open="activeSubsidebar && isDesktopSidebarOpen"
|
||||
@toggle="isDesktopSidebarOpen = !isDesktopSidebarOpen"
|
||||
>
|
||||
<span>{{ pageTitle }}</span>
|
||||
|
||||
<template #toolbar>
|
||||
<slot
|
||||
name="toolbar"
|
||||
v-bind="contextRx"
|
||||
/>
|
||||
</template>
|
||||
</SidebarPageHeading>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot
|
||||
name="extra"
|
||||
v-bind="contextRx"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
52
src/components/layouts/sidebar/SidebarPageHeading.vue
Normal file
52
src/components/layouts/sidebar/SidebarPageHeading.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
open?: boolean
|
||||
}>()
|
||||
const emits = defineEmits<{
|
||||
toggle: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-title has-text-centered">
|
||||
<div
|
||||
class="vuero-hamburger nav-trigger push-resize"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => emits('toggle')"
|
||||
@click="() => emits('toggle')"
|
||||
>
|
||||
<span class="menu-toggle has-chevron">
|
||||
<span
|
||||
:class="[props.open && 'active']"
|
||||
class="icon-box-toggle"
|
||||
>
|
||||
<span class="rotate">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-top"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-center"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-bottom"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="title-wrap">
|
||||
<h1 class="title is-4">
|
||||
<slot />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="'toolbar' in $slots" class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
64
src/components/layouts/sidebar/SidebarSubsidebar.vue
Normal file
64
src/components/layouts/sidebar/SidebarSubsidebar.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { SubsidebarItem } from './sidebar.types'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
items: SubsidebarItem[]
|
||||
}>()
|
||||
const emits = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-panel is-generic">
|
||||
<div class="subpanel-header">
|
||||
<slot>
|
||||
<h3>{{ props.label }}</h3>
|
||||
</slot>
|
||||
<div
|
||||
class="panel-close"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emits('close')"
|
||||
@click="emits('close')"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="inner has-slimscroll"
|
||||
>
|
||||
<ul>
|
||||
<template v-for="(item, idx) of props.items">
|
||||
<li
|
||||
v-if="item.type === 'divider'"
|
||||
:key="`divider-${idx}`"
|
||||
class="divider"
|
||||
:class="[item.label ? 'with-label' : '']"
|
||||
>
|
||||
<span v-if="item.label" class="divider-label">{{ item.label }}</span>
|
||||
</li>
|
||||
<li v-else-if="item.type === 'link'" :key="`link-${idx}`">
|
||||
<VLink :to="item.to">
|
||||
{{ item.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
<VCollapseLinks
|
||||
v-else-if="item.type === 'collapse'"
|
||||
:key="`collapse-${item.id}`"
|
||||
:links="item.children"
|
||||
>
|
||||
{{ item.label }}
|
||||
</VCollapseLinks>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/sidebar-panel';
|
||||
</style>
|
||||
51
src/components/layouts/sidebar/SidebarSubsidebarMobile.vue
Normal file
51
src/components/layouts/sidebar/SidebarSubsidebarMobile.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { SubsidebarItem } from './sidebar.types'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
items: SubsidebarItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-subsidebar">
|
||||
<div class="inner">
|
||||
<div class="sidebar-title">
|
||||
<slot>
|
||||
<h3>{{ props.label }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="submenu has-slimscroll"
|
||||
>
|
||||
<template v-for="(item, idx) of props.items">
|
||||
<li
|
||||
v-if="item.type === 'divider'"
|
||||
:key="`divider-${idx}`"
|
||||
class="divider"
|
||||
:class="[item.label ? 'with-label' : '']"
|
||||
>
|
||||
<span v-if="item.label" class="divider-label">{{ item.label }}</span>
|
||||
</li>
|
||||
<li v-else-if="item.type === 'link'" :key="`link-${idx}`">
|
||||
<VLink :to="item.to">
|
||||
{{ item.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
<VCollapseLinks
|
||||
v-else-if="item.type === 'collapse'"
|
||||
:key="`collapse-${item.id}`"
|
||||
:links="item.children"
|
||||
>
|
||||
{{ item.label }}
|
||||
</VCollapseLinks>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/mobile-subsidebar';
|
||||
</style>
|
||||
14
src/components/layouts/sidebar/sidebar.context.ts
Normal file
14
src/components/layouts/sidebar/sidebar.context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { SidebarLayoutContext } from './sidebar.types'
|
||||
|
||||
export const injectionKey = Symbol('sidebar-layout') as InjectionKey<SidebarLayoutContext>
|
||||
|
||||
export function useSidebarLayoutContext() {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSidebarLayoutContext() is called outside of <SidebarLayout> tree.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
105
src/components/layouts/sidebar/sidebar.types.ts
Normal file
105
src/components/layouts/sidebar/sidebar.types.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { VNode, Component } from 'vue'
|
||||
|
||||
// -- Sidebar
|
||||
export interface SidebarItemSubsidebar {
|
||||
type: 'subsidebar'
|
||||
id: string
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
subsidebar: {
|
||||
label: string
|
||||
items: SubsidebarItem[]
|
||||
}
|
||||
}
|
||||
export interface SidebarItemLink {
|
||||
type: 'link'
|
||||
id: string
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
to: string
|
||||
}
|
||||
export interface SidebarItemAction {
|
||||
type: 'action'
|
||||
id: string
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
export interface SidebarItemComponent {
|
||||
type: 'component'
|
||||
id: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
component: string | Component | (() => VNode)
|
||||
}
|
||||
|
||||
export type SidebarItem =
|
||||
| SidebarItemSubsidebar
|
||||
| SidebarItemLink
|
||||
| SidebarItemAction
|
||||
| SidebarItemComponent
|
||||
|
||||
// -- Subsidebar
|
||||
export interface SubsidebarItemCollapse {
|
||||
type: 'collapse'
|
||||
id: string
|
||||
label: string
|
||||
children: {
|
||||
label: string
|
||||
to: string
|
||||
icon?: string
|
||||
tag?: string | number
|
||||
}[]
|
||||
}
|
||||
export interface SubsidebarItemLink {
|
||||
type: 'link'
|
||||
label: string
|
||||
to: string
|
||||
tag?: string | number
|
||||
}
|
||||
export interface SubsidebarItemDivider {
|
||||
type: 'divider'
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type SubsidebarItem =
|
||||
| SubsidebarItemCollapse
|
||||
| SubsidebarItemLink
|
||||
| SubsidebarItemDivider
|
||||
|
||||
// -- Context
|
||||
export type SidebarTheme =
|
||||
| 'default'
|
||||
| 'color'
|
||||
| 'color-curved'
|
||||
| 'curved'
|
||||
| 'float'
|
||||
| 'labels'
|
||||
| 'labels-hover'
|
||||
|
||||
export interface SidebarLayoutContext {
|
||||
links: ComputedRef<SidebarItem[]>
|
||||
linksBottom: ComputedRef<SidebarItem[]>
|
||||
theme: ComputedRef<SidebarTheme>
|
||||
|
||||
defaultSidebar: ComputedRef<string>
|
||||
closeOnChange: ComputedRef<boolean>
|
||||
openOnMounted: ComputedRef<boolean>
|
||||
|
||||
isMobileSidebarOpen: Ref<boolean>
|
||||
isDesktopSidebarOpen: Ref<boolean>
|
||||
activeSubsidebarId: Ref<string>
|
||||
|
||||
isOpen: ComputedRef<boolean>
|
||||
subsidebars: ComputedRef<SidebarItemSubsidebar[]>
|
||||
activeSubsidebar: ComputedRef<SidebarItemSubsidebar | undefined>
|
||||
|
||||
toggleSubsidebar: (id: string) => void
|
||||
}
|
||||
656
src/components/layouts/sideblock/Sideblock.vue
Normal file
656
src/components/layouts/sideblock/Sideblock.vue
Normal file
@@ -0,0 +1,656 @@
|
||||
<script setup lang="ts">
|
||||
import { useSideblockLayoutContext } from './sideblock.context'
|
||||
|
||||
const { theme } = useSideblockLayoutContext()
|
||||
|
||||
const themeClasses = computed(() => {
|
||||
switch (theme.value) {
|
||||
case 'color':
|
||||
return 'is-colored'
|
||||
case 'curved':
|
||||
return 'is-curved'
|
||||
case 'color-curved':
|
||||
return 'is-colored is-curved'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-block is-active"
|
||||
:class="[themeClasses]"
|
||||
>
|
||||
<div class="sidebar-block-header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div class="sidebar-block-inner">
|
||||
<ul>
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-block-footer">
|
||||
<slot name="links-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-block {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100vh;
|
||||
width: 280px;
|
||||
background-color: var(--white);
|
||||
box-shadow: none;
|
||||
z-index: 35;
|
||||
transition:
|
||||
border-radius 0.3s,
|
||||
transform 0.3s;
|
||||
|
||||
&.is-curved {
|
||||
border-start-end-radius: 2rem;
|
||||
border-end-end-radius: 2rem;
|
||||
border-inline-end: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sidebar-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
padding: 0 2.5rem;
|
||||
|
||||
.sidebar-block-logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
margin-inline-end: 0.5rem;
|
||||
|
||||
img {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-alt);
|
||||
color: var(--dark-text);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.sidebar-block-close {
|
||||
margin-inline-start: auto;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
.iconify {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-inner {
|
||||
position: relative;
|
||||
height: calc(100vh - 160px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: var(--white);
|
||||
margin-top: 40px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background: rgba(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
border-inline-start: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-active {
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
> a {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
a.router-link-exact-active {
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
|
||||
> a {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.divider {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
height: 10px;
|
||||
margin: 5px 20px 10px;
|
||||
border-bottom: 1px solid rgba(0 0 0 / 15%);
|
||||
}
|
||||
|
||||
&.has-children {
|
||||
display: block;
|
||||
height: unset;
|
||||
min-height: 36px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.active {
|
||||
.collapse-wrap {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
> a {
|
||||
color: var(--dark-text);
|
||||
font-weight: 600;
|
||||
background: var(--widget-grey);
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
.icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
transform: rotate(calc(var(--transform-direction) * 90deg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.collapse-wrap > a {
|
||||
color: var(--dark-text);
|
||||
|
||||
.icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.tag {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 20px;
|
||||
border-radius: 100px;
|
||||
background: var(--danger);
|
||||
color: var(--white);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
> a {
|
||||
font-family: var(--font-alt);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
padding: 0.85rem;
|
||||
margin: 0 2rem;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
.icon {
|
||||
color: var(--light-text);
|
||||
font-size: 1.25rem;
|
||||
margin-inline-end: 1rem;
|
||||
|
||||
.iconify {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-inline-start: auto;
|
||||
transform: rotate(calc(var(--transform-direction) * 0));
|
||||
stroke: var(--light-text);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
height: 32px;
|
||||
font-family: var(--font);
|
||||
|
||||
&.is-active {
|
||||
.is-submenu {
|
||||
font-weight: 500;
|
||||
|
||||
.iconify {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-submenu {
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
font-family: var(--font);
|
||||
align-items: center;
|
||||
padding: 0 2rem 0 3.5rem;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&.is-active,
|
||||
&.router-link-exact-active {
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir {
|
||||
font-size: 1.2rem;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
display: none;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
max-width: 8px;
|
||||
min-width: 8px;
|
||||
stroke-width: 2px;
|
||||
margin-inline-end: 8px;
|
||||
|
||||
&.is-auto {
|
||||
margin-inline-start: auto;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
max-width: 15px;
|
||||
min-width: 15px;
|
||||
stroke-width: 2px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.single-link) {
|
||||
font-family: var(--font);
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
font-size: 0.95rem;
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.single-link {
|
||||
font-family: var(--font-alt);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
padding: 0.85rem;
|
||||
margin: 0 2rem 0.25rem;
|
||||
width: 100%;
|
||||
border-radius: 0.65rem;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
background: var(--widget-grey);
|
||||
color: var(--dark-text);
|
||||
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
stroke: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.25rem;
|
||||
margin-inline-end: 1rem;
|
||||
|
||||
.iconify {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-inline-start: auto;
|
||||
color: var(--white);
|
||||
background: var(--primary);
|
||||
height: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 0.6;
|
||||
border-radius: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-footer {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
padding: 0 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
a:not(.dropdown-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-link,
|
||||
.icon-link {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
color: var(--light-text);
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.iconify {
|
||||
font-size: 18px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--widget-grey);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 42px;
|
||||
width: 42px !important;
|
||||
|
||||
> img {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--white);
|
||||
background: var(--success);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. Sidebar Block Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.sidebar-block {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 5%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 5%) !important;
|
||||
|
||||
.panel-close {
|
||||
.iconify {
|
||||
stroke: var(--muted-grey) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-header {
|
||||
h3 {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-inner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 5%);
|
||||
|
||||
li {
|
||||
&.has-children {
|
||||
&.active {
|
||||
.collapse-wrap > a {
|
||||
color: var(--smoke-white);
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 8%);
|
||||
|
||||
.icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.collapse-wrap > a {
|
||||
color: var(--smoke-white);
|
||||
|
||||
.icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.single-link) {
|
||||
&:hover {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.single-link {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 8%);
|
||||
color: var(--smoke-white);
|
||||
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
stroke: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-footer {
|
||||
.search-link {
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 8%);
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.sidebar-block {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.sidebar-block {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. Sidebar Block Colored
|
||||
========================================================================== */
|
||||
|
||||
html:not(.is-dark) {
|
||||
.sidebar-block {
|
||||
&.is-colored {
|
||||
background: color-mix(in oklab, var(--dark), black 12%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark), white 5%) !important;
|
||||
|
||||
.panel-close {
|
||||
.iconify {
|
||||
stroke: var(--muted-grey) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-header {
|
||||
h3 {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-inner {
|
||||
background: color-mix(in oklab, var(--dark), black 12%) !important;
|
||||
|
||||
li {
|
||||
&.has-children {
|
||||
&.active {
|
||||
.collapse-wrap > a {
|
||||
color: var(--smoke-white) !important;
|
||||
background: color-mix(in oklab, var(--dark), black 7%) !important;
|
||||
|
||||
.icon {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.collapse-wrap > a {
|
||||
color: var(--smoke-white) !important;
|
||||
|
||||
.icon {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.single-link) {
|
||||
&:hover {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.single-link {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: color-mix(in oklab, var(--dark), black 7%) !important;
|
||||
color: var(--smoke-white) !important;
|
||||
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-block-footer {
|
||||
.search-link {
|
||||
color: var(--light-text) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark), black 7%) !important;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
src/components/layouts/sideblock/SideblockItem.vue
Normal file
61
src/components/layouts/sideblock/SideblockItem.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { SideblockItem, SideblockItemAction } from './sideblock.types'
|
||||
|
||||
const props = defineProps<{
|
||||
link: SideblockItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li v-if="props.link.type === 'link'">
|
||||
<VLink
|
||||
:to="props.link.to"
|
||||
class="single-link"
|
||||
>
|
||||
<span class="icon">
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</span>
|
||||
{{ props.link.label }}
|
||||
<span v-if="props.link.badge !== undefined" class="badge">{{ props.link.badge }}</span>
|
||||
</VLink>
|
||||
</li>
|
||||
<component
|
||||
:is="props.link.component"
|
||||
v-else-if="props.link.type === 'component'"
|
||||
:title="props.link.label"
|
||||
/>
|
||||
<li v-else-if="props.link.type === 'action'">
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="single-link"
|
||||
@click="(props.link as SideblockItemAction).onClick"
|
||||
@keydown.enter.prevent="(props.link as SideblockItemAction).onClick"
|
||||
>
|
||||
<span class="icon">
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</span>
|
||||
{{ props.link.label }}
|
||||
<span v-if="props.link.badge !== undefined" class="badge">{{ props.link.badge }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<VCollapseLinks
|
||||
v-else-if="props.link.type === 'collapse'"
|
||||
:links="props.link.children"
|
||||
>
|
||||
<div class="icon">
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</div>
|
||||
{{ props.link.label }}
|
||||
</VCollapseLinks>
|
||||
<li
|
||||
v-else-if="props.link.type === 'divider'"
|
||||
class="divider"
|
||||
/>
|
||||
</template>
|
||||
38
src/components/layouts/sideblock/SideblockItemMobile.vue
Normal file
38
src/components/layouts/sideblock/SideblockItemMobile.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { SideblockItem, SideblockItemAction } from './sideblock.types'
|
||||
|
||||
const props = defineProps<{
|
||||
link: SideblockItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
v-if="props.link.type === 'divider'"
|
||||
class="divider"
|
||||
:class="[props.link.label ? 'with-label' : '']"
|
||||
>
|
||||
<span v-if="props.link.label" class="divider-label">{{ props.link.label }}</span>
|
||||
</li>
|
||||
<li v-else-if="props.link.type === 'link'">
|
||||
<VLink :to="props.link.to">
|
||||
{{ props.link.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
<li v-else-if="props.link.type === 'action'">
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="(props.link as SideblockItemAction).onClick"
|
||||
@keydown.enter.prevent="(props.link as SideblockItemAction).onClick"
|
||||
>
|
||||
{{ props.link.label }}
|
||||
</a>
|
||||
</li>
|
||||
<VCollapseLinks
|
||||
v-else-if="props.link.type === 'collapse'"
|
||||
:links="props.link.children"
|
||||
>
|
||||
{{ props.link.label }}
|
||||
</VCollapseLinks>
|
||||
</template>
|
||||
172
src/components/layouts/sideblock/SideblockLayout.vue
Normal file
172
src/components/layouts/sideblock/SideblockLayout.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import type { SideblockLayoutContext, SideblockItem, SideblockTheme } from './sideblock.types'
|
||||
import { injectionKey } from './sideblock.context'
|
||||
import SideblockItemMobile from './SideblockItemMobile.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: SideblockItem[]
|
||||
theme?: SideblockTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
closeOnChange?: boolean
|
||||
openOnMounted?: boolean
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
|
||||
const isMobileSideblockOpen = ref(false)
|
||||
const isDesktopSideblockOpen = ref(props.openOnMounted)
|
||||
|
||||
// provide context to children
|
||||
const context: SideblockLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
theme: computed(() => props.theme),
|
||||
closeOnChange: computed(() => props.closeOnChange),
|
||||
openOnMounted: computed(() => props.openOnMounted),
|
||||
|
||||
isMobileSideblockOpen,
|
||||
isDesktopSideblockOpen,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
isMobileSideblockOpen.value = false
|
||||
|
||||
if (props.closeOnChange && isDesktopSideblockOpen.value) {
|
||||
isDesktopSideblockOpen.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-layout">
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSideblockOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<SideblockSubsidebarMobile
|
||||
v-if="isMobileSideblockOpen"
|
||||
:items="props.links"
|
||||
>
|
||||
<template #default>
|
||||
<slot name="sideblock-title-mobile" />
|
||||
</template>
|
||||
<template #links>
|
||||
<slot name="sideblock-links-mobile" v-bind="contextRx">
|
||||
<SideblockItemMobile
|
||||
v-for="(link, key) in props.links"
|
||||
:key
|
||||
:link
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</SideblockSubsidebarMobile>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSideblockOpen"
|
||||
@click="isMobileSideblockOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Transition name="slide-x">
|
||||
<Sideblock
|
||||
v-if="isDesktopSideblockOpen"
|
||||
:theme="props.theme"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
</template>
|
||||
<template #links>
|
||||
<slot name="sideblock-links" v-bind="contextRx">
|
||||
<SideblockItem
|
||||
v-for="(link, key) in props.links"
|
||||
:key
|
||||
:link
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #links-bottom>
|
||||
<slot name="sideblock-end" v-bind="contextRx" />
|
||||
</template>
|
||||
</Sideblock>
|
||||
</Transition>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper
|
||||
full
|
||||
:class="[
|
||||
isDesktopSideblockOpen && 'is-pushed-block',
|
||||
]"
|
||||
>
|
||||
<template v-if="props.size === 'full'">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<SideblockPageHeading
|
||||
:open="isDesktopSideblockOpen"
|
||||
@toggle="isDesktopSideblockOpen = !isDesktopSideblockOpen"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
|
||||
<template #toolbar>
|
||||
<slot
|
||||
name="toolbar"
|
||||
v-bind="contextRx"
|
||||
/>
|
||||
</template>
|
||||
</SideblockPageHeading>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent
|
||||
class="is-relative"
|
||||
>
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<SideblockPageHeading
|
||||
:open="isDesktopSideblockOpen"
|
||||
@toggle="isDesktopSideblockOpen = !isDesktopSideblockOpen"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
|
||||
<template #toolbar>
|
||||
<slot
|
||||
name="toolbar"
|
||||
v-bind="contextRx"
|
||||
/>
|
||||
</template>
|
||||
</SideblockPageHeading>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
52
src/components/layouts/sideblock/SideblockPageHeading.vue
Normal file
52
src/components/layouts/sideblock/SideblockPageHeading.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
open?: boolean
|
||||
}>()
|
||||
const emits = defineEmits<{
|
||||
toggle: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-title has-text-centered">
|
||||
<div
|
||||
class="vuero-hamburger nav-trigger push-resize"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => emits('toggle')"
|
||||
@click="() => emits('toggle')"
|
||||
>
|
||||
<span class="menu-toggle has-chevron">
|
||||
<span
|
||||
:class="[props.open && 'active']"
|
||||
class="icon-box-toggle"
|
||||
>
|
||||
<span class="rotate">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-top"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-center"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="icon-line-bottom"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="title-wrap">
|
||||
<h1 class="title is-4">
|
||||
<slot />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="'toolbar' in $slots" class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { SideblockItem } from './sideblock.types'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
items: SideblockItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-subsidebar">
|
||||
<div class="inner">
|
||||
<div v-if="props.label" class="sidebar-title">
|
||||
<slot>
|
||||
<h3>{{ props.label }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="submenu has-slimscroll"
|
||||
>
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile-subsidebar .inner {
|
||||
margin-inline-start: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/mobile-subsidebar';
|
||||
</style>
|
||||
14
src/components/layouts/sideblock/sideblock.context.ts
Normal file
14
src/components/layouts/sideblock/sideblock.context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { SideblockLayoutContext } from './sideblock.types'
|
||||
|
||||
export const injectionKey = Symbol('sideblock-layout') as InjectionKey<SideblockLayoutContext>
|
||||
|
||||
export function useSideblockLayoutContext() {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSideblockLayoutContext() is called outside of <SideblockLayout> tree.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
73
src/components/layouts/sideblock/sideblock.types.ts
Normal file
73
src/components/layouts/sideblock/sideblock.types.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { VNode, Component } from 'vue'
|
||||
|
||||
// -- Sideblock
|
||||
export interface SideblockItemCollapse {
|
||||
type: 'collapse'
|
||||
id: string
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
children: {
|
||||
label: string
|
||||
to: string
|
||||
icon?: string
|
||||
tag?: string
|
||||
}[]
|
||||
}
|
||||
export interface SideblockItemLink {
|
||||
type: 'link'
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
badge?: string | number
|
||||
|
||||
to: string
|
||||
}
|
||||
export interface SideblockItemAction {
|
||||
type: 'action'
|
||||
id: string
|
||||
icon: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
badge?: string | number
|
||||
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
export interface SideblockItemComponent {
|
||||
type: 'component'
|
||||
id: string
|
||||
hideMobile?: boolean
|
||||
label?: string
|
||||
|
||||
component: string | Component | (() => VNode)
|
||||
}
|
||||
export interface SideblockItemDivider {
|
||||
type: 'divider'
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type SideblockItem =
|
||||
| SideblockItemCollapse
|
||||
| SideblockItemLink
|
||||
| SideblockItemAction
|
||||
| SideblockItemComponent
|
||||
| SideblockItemDivider
|
||||
|
||||
// -- Context
|
||||
export type SideblockTheme =
|
||||
| 'default'
|
||||
| 'curved'
|
||||
| 'color'
|
||||
| 'color-curved'
|
||||
|
||||
export interface SideblockLayoutContext {
|
||||
links: ComputedRef<SideblockItem[]>
|
||||
theme: ComputedRef<SideblockTheme>
|
||||
|
||||
closeOnChange: ComputedRef<boolean>
|
||||
openOnMounted: ComputedRef<boolean>
|
||||
|
||||
isMobileSideblockOpen: Ref<boolean>
|
||||
isDesktopSideblockOpen: Ref<boolean>
|
||||
}
|
||||
Reference in New Issue
Block a user