mirror of
https://git.hmsn.ink/kospo/svcm/dmz.git
synced 2026-03-20 03:52:24 +09:00
불필요한 파일 제거
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
<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>
|
||||
@@ -1,338 +0,0 @@
|
||||
<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>
|
||||
@@ -1,88 +0,0 @@
|
||||
<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>
|
||||
@@ -1,247 +0,0 @@
|
||||
<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>
|
||||
@@ -1,12 +0,0 @@
|
||||
<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>
|
||||
@@ -1,42 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// -- 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>
|
||||
}
|
||||
@@ -1,659 +0,0 @@
|
||||
<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;
|
||||
border-right: 1px solid #eee;
|
||||
|
||||
&.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.7rem;
|
||||
letter-spacing: 1px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.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: 1.2rem;
|
||||
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.4rem;
|
||||
margin-inline-end: 1rem;
|
||||
|
||||
.iconify {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -1,61 +0,0 @@
|
||||
<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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,173 +0,0 @@
|
||||
<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',
|
||||
openOnMounted: true,
|
||||
},
|
||||
)
|
||||
|
||||
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 = true
|
||||
|
||||
if (props.closeOnChange && isDesktopSideblockOpen.value) {
|
||||
isDesktopSideblockOpen.value = true
|
||||
}
|
||||
},
|
||||
)
|
||||
</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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<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>
|
||||
@@ -1,37 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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 {
|
||||
id: string
|
||||
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>
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* This is a store that hold the messaging-v1 state
|
||||
* It uses the useFetch composition component to make the api calls
|
||||
*
|
||||
* @see /src/pages/messaging-v1.vue
|
||||
* @see /src/composable/useFetch.ts
|
||||
* @see /src/components/partials/chat/*.vue
|
||||
* @see /src/utils/api/chat
|
||||
*/
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import type { $Fetch } from 'ofetch'
|
||||
|
||||
export interface Conversation {
|
||||
id: number
|
||||
name: string
|
||||
lastMessage: string
|
||||
unreadMessages: boolean
|
||||
avatar: string
|
||||
}
|
||||
export interface Message {
|
||||
id: number
|
||||
conversationId: number
|
||||
messageId: number
|
||||
type: 'msg' | 'image' | 'imagelink' | 'system'
|
||||
sender: string | null
|
||||
avatar: string | null
|
||||
content: {
|
||||
time: string | null
|
||||
text?: string
|
||||
subtext?: string
|
||||
image_url?: string
|
||||
link_image?: string
|
||||
link_badge?: string
|
||||
}
|
||||
}
|
||||
|
||||
const defaultConversation: Conversation = {
|
||||
id: 0,
|
||||
name: '',
|
||||
lastMessage: '',
|
||||
unreadMessages: false,
|
||||
avatar: '/images/avatars/placeholder.jpg',
|
||||
}
|
||||
|
||||
export const useChat = defineStore('chat', () => {
|
||||
const $fetch = useApiFetch()
|
||||
const conversations = ref<Conversation[]>([])
|
||||
const messages = ref<Message[]>([])
|
||||
const selectedConversationId = ref(0)
|
||||
const addConversationOpen = ref(false)
|
||||
const mobileConversationDetailsOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const selectedConversation = computed(() => {
|
||||
const conversation = conversations.value?.find(
|
||||
item => item.id === selectedConversationId.value,
|
||||
)
|
||||
|
||||
if (!conversation) {
|
||||
return defaultConversation
|
||||
}
|
||||
else {
|
||||
return conversation
|
||||
}
|
||||
})
|
||||
|
||||
async function loadConversations(start = 0, limit = 10) {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await fetchConversations($fetch, start, limit)
|
||||
conversations.value = response.conversations ?? []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectConversastion(conversationId: number) {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await fetchMessages($fetch, conversationId)
|
||||
selectedConversationId.value = conversationId
|
||||
messages.value = response.messages
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function unselectConversation() {
|
||||
selectedConversationId.value = 0
|
||||
messages.value = []
|
||||
}
|
||||
|
||||
function setAddConversationOpen(value: boolean) {
|
||||
addConversationOpen.value = value
|
||||
}
|
||||
|
||||
function setMobileConversationDetailsOpen(value: boolean) {
|
||||
mobileConversationDetailsOpen.value = value
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
messages,
|
||||
selectedConversation,
|
||||
selectedConversationId,
|
||||
addConversationOpen,
|
||||
mobileConversationDetailsOpen,
|
||||
loading,
|
||||
loadConversations,
|
||||
setAddConversationOpen,
|
||||
setMobileConversationDetailsOpen,
|
||||
selectConversastion,
|
||||
unselectConversation,
|
||||
} as const
|
||||
})
|
||||
|
||||
async function fetchConversations(
|
||||
$fetch: $Fetch,
|
||||
start = 0,
|
||||
limit = 20,
|
||||
): Promise<{ conversations: Conversation[], count: number }> {
|
||||
let count = 0
|
||||
|
||||
const { _data: conversations = [], headers } = await $fetch.raw<Conversation[]>(
|
||||
`/api/conversations`,
|
||||
{
|
||||
query: {
|
||||
_start: start,
|
||||
_limit: limit,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (headers.has('X-Total-Count')) {
|
||||
count = parseInt(headers.get('X-Total-Count') ?? '0')
|
||||
}
|
||||
|
||||
return { conversations, count }
|
||||
}
|
||||
|
||||
async function fetchMessages(
|
||||
$fetch: $Fetch,
|
||||
conversationId: number,
|
||||
start = 0,
|
||||
limit = 20,
|
||||
): Promise<{ messages: Message[], count: number }> {
|
||||
let count = 0
|
||||
|
||||
const { _data: messages = [], headers } = await $fetch.raw<Message[]>(
|
||||
`/api/conversations/${conversationId}/messages?_start=${start}&_limit=${limit}`,
|
||||
)
|
||||
|
||||
if (headers.has('X-Total-Count')) {
|
||||
count = parseInt(headers.get('X-Total-Count') ?? '0')
|
||||
}
|
||||
|
||||
return { messages, count }
|
||||
}
|
||||
|
||||
/**
|
||||
* Pinia supports Hot Module replacement so you can edit your stores and
|
||||
* interact with them directly in your app without reloading the page.
|
||||
*
|
||||
* @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
|
||||
* @see https://vitejs.dev/guide/api-hmr.html
|
||||
*/
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useChat, import.meta.hot))
|
||||
}
|
||||
Reference in New Issue
Block a user