불필요한 파일 제거

This commit is contained in:
2025-05-30 19:04:19 +09:00
parent 14281e97ac
commit ca84be25dc
319 changed files with 1 additions and 18132 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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))
}