feat(theme-yun): add more animations for post/links/girls & abdjust layout

This commit is contained in:
YunYouJun 2024-10-04 12:47:55 +08:00
parent f3ff676182
commit 4478573c41
61 changed files with 1132 additions and 510 deletions

View File

@ -5,7 +5,6 @@
],
"rules": {
"no-descending-specificity": null,
"alpha-value-notation": "number",
"color-function-notation": "legacy"
"alpha-value-notation": "number"
}
}

View File

@ -13,7 +13,7 @@ const siteConfig = useSiteConfig()
<main>
<div w="full" flex="~">
<slot name="main">
<div class="content" flex="~ col grow" w="full" p="l-4 lt-md:0">
<div class="content" flex="~ col grow" w="full" p="x-4 lt-md:0">
<slot name="main-header" />
<slot name="main-header-after" />

View File

@ -4,4 +4,6 @@ categories:
- 中文
- 分类
- 测试
date: 2021-07-01 00:00:00
updated: 2024-07-01 00:00:00
---

View File

@ -6,7 +6,7 @@ export default defineSiteConfig({
},
lang: 'zh-CN',
title: '自定义博客标题',
title: '自定义博客名称',
timezone: 'Asia/Shanghai',
url: 'https://yun.valaxy.site/',
author: {

View File

@ -1,28 +1,29 @@
<script lang="ts" setup>
import { useAppStore, useFrontmatter } from 'valaxy'
import { useFrontmatter } from 'valaxy'
import { usePressAppStore } from '../stores/app'
import PressOutline from './PressOutline.vue'
const frontmatter = useFrontmatter()
const app = useAppStore()
const press = usePressAppStore()
</script>
<template>
<button
class="toc-btn shadow-lg fixed press-icon-btn z-99 lt-md:hidden! xl:hidden!"
right="5" bottom="24"
@click="app.toggleRightSidebar()"
@click="press.rightSidebar.toggle()"
>
<div i-ri-file-list-line />
</button>
<ValaxyOverlay :show="app.isRightSidebarOpen" @click="app.toggleRightSidebar()" />
<ValaxyOverlay :show="press.rightSidebar.isOpen" @click="press.rightSidebar.toggle()" />
<aside
class="press-aside lt-xl:fixed shadow xl:(shadow-none hover:shadow-none) hover:shadow-lg"
flex="~ col grow"
p="l-0 xl:l-8" text="center"
z="$"
:class="app.isRightSidebarOpen && 'open'"
:class="press.rightSidebar.isOpen && 'open'"
>
<div class="aside-curtain" />
<div class="aside-container lt-xl:fixed" flex="~ col">

View File

@ -0,0 +1,17 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useToggle } from '@vueuse/core'
export const usePressAppStore = defineStore('press-app', () => {
// 右侧边栏
const [isRightSidebarOpen, toggleRightSidebar] = useToggle()
return {
rightSidebar: {
isOpen: isRightSidebarOpen,
toggle: toggleRightSidebar,
},
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(usePressAppStore, import.meta.hot))

View File

@ -0,0 +1 @@
export * from './app'

View File

@ -3,6 +3,7 @@ import { useHead } from '@unhead/vue'
import { useAppStore } from 'valaxy'
import { onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useCssVar, useScroll } from '@vueuse/core'
import { useThemeConfig } from './composables'
import { useYunAppStore } from './stores'
@ -48,9 +49,21 @@ watch(
onMounted(() => {
app.showLoading = false
})
// for scroll animation
const scrollY = useCssVar('--scroll-y')
const { y } = useScroll(window, {
onScroll: () => {
scrollY.value = y.value.toString()
},
})
const isDev = import.meta.env.DEV
</script>
<template>
<YunDebug v-if="isDev" />
<YunPageHeaderGradient />
<YunNavMenu />
<YunFullscreenMenu />

View File

@ -37,64 +37,60 @@ onContentUpdated(() => {
</script>
<template>
<main class="yun-main md:pl-$va-sidebar-width lt-md:ml-0" flex="~">
<div w="full" flex="~">
<slot name="main">
<div class="content" :class="!aside && 'no-aside'" flex="~ col grow" w="full" p="l-4 lt-md:0">
<YunCard :cover="frontmatter.cover" m="0" class="relative" :style="styles as StyleValue">
<slot name="main-header">
<YunPageHeader
class="mb-2"
:title="title"
:icon="frontmatter.icon || icon"
:color="frontmatter.color || color"
:cover="frontmatter.cover"
:page-title-class="frontmatter.pageTitleClass"
/>
<main class="yun-main lt-md:w-full" flex="~ center">
<slot name="main">
<div
class="content w-full md:w-3xl lg:w-2xl xl:w-4xl" :class="{
'no-aside': !aside,
}" flex="~ col grow"
p="lt-md:0"
>
<YunCard :cover="frontmatter.cover" m="0" class="relative" :style="styles as StyleValue">
<slot name="main-header">
<YunPageHeader
class="mb-2 mt-8"
:title="title"
:icon="frontmatter.icon || icon"
:color="frontmatter.color || color"
:cover="frontmatter.cover"
:page-title-class="frontmatter.pageTitleClass"
/>
</slot>
<slot name="main-header-after" />
<div p="x-4 b-8" class="sm:px-6 lg:px-12 xl:px-16" w="full">
<slot name="main-content">
<!-- <Transition appear> -->
<ValaxyMd :frontmatter="frontmatter">
<YunAiExcerpt v-if="frontmatter.excerpt_type === 'ai' && frontmatter.excerpt" />
<YunMdTimeWarning />
<slot name="main-content-md" />
<slot />
</ValaxyMd>
<!-- </Transition> -->
</slot>
<slot name="main-header-after" />
<div p="x-4 b-8" class="sm:px-6 lg:px-12 xl:px-16" w="full">
<slot name="main-content">
<!-- <Transition appear> -->
<ValaxyMd :frontmatter="frontmatter">
<YunAiExcerpt v-if="frontmatter.excerpt_type === 'ai' && frontmatter.excerpt" />
<YunMdTimeWarning />
<slot name="main-content-after" />
</div>
</YunCard>
<slot name="main-content-md" />
<slot />
</ValaxyMd>
<!-- </Transition> -->
</slot>
<slot name="main-nav-before" />
<slot name="main-content-after" />
</div>
</YunCard>
<slot name="main-nav">
<YunPostNav v-if="frontmatter.nav !== false" />
</slot>
<slot name="main-nav-before" />
<slot name="main-nav-after" />
<slot name="main-nav">
<YunPostNav v-if="frontmatter.nav !== false" />
</slot>
<slot v-if="siteConfig.comment.enable && frontmatter.comment !== false" name="comment">
<YunComment :class="frontmatter.nav === false ? 'mt-4' : 0" />
</slot>
<slot name="main-nav-after" />
<slot v-if="siteConfig.comment.enable && frontmatter.comment !== false" name="comment">
<YunComment :class="frontmatter.nav === false ? 'mt-4' : 0" />
</slot>
<slot name="main-footer-before" />
<YunFooter />
<slot name="main-footer-after" />
</div>
</slot>
<slot name="aside">
<YunAside v-if="aside">
<slot name="aside-custom" />
</YunAside>
</slot>
</div>
<slot name="main-footer-before" />
<slot name="main-footer-after" />
</div>
</slot>
</main>
</template>
@ -112,7 +108,7 @@ onContentUpdated(() => {
@include screen('xl') {
.content{
// 8px scrollbar width
max-width: calc(100vw - 2 * var(--va-sidebar-width) - 1rem - 8px);
// max-width: calc(100vw - 2 * var(--va-sidebar-width) - 1rem - 8px);
&.no-aside {
max-width: calc(100vw - var(--va-sidebar-width));

View File

@ -1,56 +1,57 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { useAppStore, useFrontmatter } from 'valaxy'
import { useFrontmatter } from 'valaxy'
import { useYunAppStore } from '../stores'
const frontmatter = useFrontmatter()
const { t } = useI18n()
const app = useAppStore()
const yun = useYunAppStore()
</script>
<template>
<button
class="xl:hidden toc-btn shadow fixed yun-icon-btn z-350"
opacity="75" right="2" bottom="19"
@click="app.toggleRightSidebar()"
@click="yun.rightSidebar.toggle()"
>
<div i-ri-file-list-line />
</button>
<ValaxyOverlay :show="app.isRightSidebarOpen" @click="app.toggleRightSidebar()" />
<ValaxyOverlay :show="yun.rightSidebar.isOpen" @click="yun.rightSidebar.toggle()" />
<!-- -->
<aside
class="va-card yun-aside"
:class="app.isRightSidebarOpen && 'open'" m="l-4" text="center"
overflow="auto"
>
<div class="aside-container" flex="~ col">
<h2 v-if="frontmatter.toc !== false" m="t-6 b-2" font="serif black">
{{ t('sidebar.toc') }}
</h2>
<Transition>
<aside
v-if="yun.rightSidebar.isOpen"
flex="~ col"
class="va-card yun-aside sticky top-68px min-h-sm w-80"
:class="yun.rightSidebar.isOpen && 'open'"
text="center"
overflow="auto"
>
<div class="w-full" flex="~ col">
<h2 v-if="frontmatter.toc !== false" m="t-6 b-2" font="serif black">
{{ t('sidebar.toc') }}
</h2>
<YunOutline v-if="frontmatter.toc !== false" />
<YunOutline v-if="frontmatter.toc !== false" />
<div class="flex-grow" />
<div class="flex-grow" />
<div v-if="$slots.default" class="custom-container">
<slot />
<div v-if="$slots.default" class="custom-container">
<slot />
</div>
</div>
</div>
</aside>
</aside>
</Transition>
</template>
<style lang="scss">
@use 'valaxy/client/styles/mixins/index.scss' as *;
.yun-aside {
position: fixed;
right: 0;
top: 0;
bottom: 0;
// need fixed width
width: var(--va-sidebar-width, 300px);
// width: var(--va-sidebar-width, 300px);
transform: translateX(100%);
transition: box-shadow var(--va-transition-duration),
background-color var(--va-transition-duration), opacity 0.25s,
@ -62,12 +63,6 @@ const app = useAppStore()
z-index: 10;
transform: translateX(0);
}
&-container {
position: sticky;
top: 0;
height: 100vh;
}
}
@include screen('xl') {

View File

@ -38,6 +38,8 @@ if (typeof themeConfig.value.bg_image.url !== 'undefined') {
width: 100%;
height: 100%;
z-index: -1;
top: 0;
left: 0;
background-image: var(--yun-bg-img);
background-size: cover;
background-position: center;

View File

@ -24,14 +24,20 @@ const categoryList = computed(() => {
</script>
<template>
<ul v-for="category in categories.values()" :key="category.name" class="category-list" m="l-4">
<YunCategory
:parent-key="category.name"
:category="category"
:level="level + 1"
:collapsable="!categoryList.includes(category.name)"
/>
</ul>
<div flex="~ col">
<ul
v-for="category in categories.values()"
:key="category.name"
class="category-list"
>
<YunCategory
:parent-key="category.name"
:category="category"
:level="level + 1"
:collapsable="!categoryList.includes(category.name)"
/>
</ul>
</div>
</template>
<style lang="scss">
@ -51,11 +57,5 @@ const categoryList = computed(() => {
color: var(--va-c-primary);
}
}
.category-name {
&:hover {
color: var(--va-c-primary);
}
}
}
</style>

View File

@ -45,6 +45,8 @@ const { show } = useInvisibleElement(postCollapseElRef)
* @param category
*/
function jumpToDisplayCategory(category: string) {
collapse.value = false
router.push({
query: {
category,
@ -62,19 +64,39 @@ onMounted(() => {
</script>
<template>
<li class="category-list-item inline-flex items-center cursor-pointer">
<li
class="category-list-item inline-flex items-center cursor-pointer w-full gap-2 transition px-3 py-2 rounded"
hover="bg-black/5"
>
<span class="folder-action inline-flex" @click="collapse = !collapse">
<div v-if="collapse" i-ri-folder-add-line />
<div v-else style="color:var(--va-c-primary)" i-ri-folder-reduce-line />
</span>
<span class="category-name" m="l-1" @click="jumpToDisplayCategory(parentKey)">
{{ category.name === 'Uncategorized' ? t('category.uncategorized') : category.name }} [{{ category.total }}]
<span
class="category-name inline-flex items-center gap-2 w-full"
@click="jumpToDisplayCategory(parentKey)"
>
<span>
{{ category.name === 'Uncategorized' ? t('category.uncategorized') : category.name }}
</span>
<span class="rounded-full px-1.5 bg-black/5 shadow-sm" text="xs black/55">
{{ category.total }}
</span>
</span>
</li>
<template v-if="!collapse">
<ul>
<li v-for="categoryItem, i in category.children.values()" :key="i" class="post-list-item" m="l-4">
<Transition
enter-active-class="v-enter-active"
enter-from-class="v-enter-from"
leave-active-class="v-leave-active"
leave-to-class="v-leave-to"
:duration="{ enter: 200, leave: 0 }"
>
<ul v-if="!collapse">
<li
v-for="categoryItem, i in category.children.values()" :key="i"
class="post-list-item" m="l-4"
>
<template v-if="isCategoryList(categoryItem)">
<YunCategory
:parent-key="parentKey ? `${parentKey}/${categoryItem.name}` : categoryItem.name"
@ -84,12 +106,16 @@ onMounted(() => {
</template>
<template v-else>
<RouterLink v-if="categoryItem.title" :to="categoryItem.path || ''" class="inline-flex items-center">
<RouterLink
v-if="categoryItem.title" :to="categoryItem.path || ''"
class="inline-flex items-center gap-2 px-3 py-2 w-full rounded transition"
hover="bg-black/5"
>
<div i-ri-file-text-line />
<span m="l-1" font="serif black">{{ getTitle(categoryItem) }}</span>
<span font="serif black">{{ getTitle(categoryItem) }}</span>
</RouterLink>
</template>
</li>
</ul>
</template>
</Transition>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { useMotion } from '@vueuse/motion'
import Popover from 'primevue/popover'
import { onMounted, ref } from 'vue'
const op = ref<typeof Popover>()
const pContentRef = ref<HTMLElement>()
const motion = useMotion(pContentRef, {
initial: {
opacity: 0,
translateY: 30,
},
enter: {
opacity: 1,
translateY: 0,
transition: {
type: 'spring',
duration: 200,
damping: 9,
},
},
})
onMounted(() => {
motion.variant.value = 'initial'
//
window.addEventListener('scroll', () => {
op.value?.hide()
motion.variant.value = 'initial'
})
})
function toggle(event: Event) {
op.value?.toggle(event)
motion.variant.value = op.value?.visible ? 'enter' : 'initial'
}
</script>
<template>
<YunNavMenuItem icon="i-ri-mind-map" @click="toggle" />
<Popover
ref="op"
:auto-z-index="false"
:base-z-index="1"
>
<div
ref="pContentRef"
class="p-4 shadow-xl z--1"
bg="$va-c-bg-light"
>
<YunSiteInfo class="text-center" />
<YunPostsInfo />
</div>
</Popover>
</template>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useYunAppStore } from '../stores'
const yun = useYunAppStore()
const infoList = computed(() => {
return [
{
label: 'xs',
value: yun.size.isXs,
},
{
label: 'sm',
value: yun.size.isSm,
},
{
label: 'md',
value: yun.size.isMd,
},
{
label: 'lg',
value: yun.size.isLg,
},
{
label: 'xl',
value: yun.size.isXl,
},
{
label: '2xl',
value: yun.size.is2xl,
},
]
})
</script>
<template>
<div
class="bg-black/50 fixed bottom-2 left-2 p-2 gap-1 rounded z-9999"
text="xs white" flex="~ col"
>
<div v-for="item in infoList" :key="item.label" class="gap-2 inline-flex">
<span class="w-6" font="bold">{{ item.label }}: </span>
<span>{{ item.value ? '✅' : '❌' }}</span>
</div>
</div>
</template>

View File

@ -38,12 +38,15 @@ const fullscreenMenuRef = ref<HTMLElement>()
.slide-down-enter-active,
.slide-down-leave-active {
transition: transform 0.4s map.get($cubic-bezier, 'ease-in-out');
opacity: 1;
transition: transform 0.4s map.get($cubic-bezier, 'ease-in-out'),
opacity 0.2s map.get($cubic-bezier, 'ease-in-out');
transform: translateY(0);
}
.slide-down-enter-from,
.slide-down-leave-to {
opacity: 0;
transform: translateY(-100%);
}
</style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useMotion } from '@vueuse/motion'
import { onImgError } from '../utils'
import type { GirlType } from '../types'
const props = defineProps<{
i: number
girl: GirlType
}>()
const itemRef = ref()
useMotion(itemRef, {
initial: {
opacity: 0,
translateY: 40,
},
enter: {
opacity: 1,
translateY: 0,
transition: {
type: 'spring',
duration: 400,
damping: 8,
delay: props.i * 50,
},
},
})
</script>
<template>
<li ref="itemRef" class="girl-item">
<a
class="girl-item-link"
:href="girl.url || `https://zh.moegirl.org/${girl.name}`"
:title="girl.reason" alt="portrait" target="_blank" rel="noopener"
>
<figure class="girl-info">
<img class="girl-avatar" loading="lazy" :src="girl.avatar" :alt="girl.name" :onError="onImgError">
<figcaption class="girl-name" :title="(i + 1).toString()">{{ girl.name }}</figcaption>
<figcaption class="girl-from">{{ girl.from }}</figcaption>
</figure>
</a>
</li>
</template>
<style lang="scss">
.girl-item {
display: inline-flex;
text-align: center;
justify-content: center;
width: 8rem;
margin: 1rem;
.girl {
&-info {
width: 100%;
padding: 0;
margin: 0;
}
&-avatar {
object-fit: cover;
object-position: center top;
width: 4rem;
height: 4rem;
border-radius: 50%;
padding: 0.2rem;
background-color: #fff;
box-shadow: 0 0 1rem rgb(0 0 0 / 0.12);
transition: 0.5s;
&:hover {
box-shadow: 0 0 2rem rgb(0 0 0 / 0.12);
}
}
&-name {
font-size: 0.9rem;
}
&-from {
font-size: 12px;
font-family: var(--va-font-serif);
font-weight: bold;
color: var(--va-c-text-light);
&::before {
content: '「';
}
&::after {
content: '」';
}
}
}
}
</style>

View File

@ -1,14 +1,6 @@
<script lang="ts" setup>
import { onImgError } from '../utils'
import { useRandomData } from '../composables'
interface GirlType {
name: string
url: string
avatar: string
from?: string
reason?: string
}
import type { GirlType } from '../types'
const props = defineProps<{
girls: GirlType[] | string
@ -21,19 +13,7 @@ const { data } = useRandomData(props.girls, props.random)
<template>
<div class="girls">
<ul class="girl-items">
<li v-for="girl, i in data" :key="girl.name" class="girl-item">
<a
class="girl-item-link"
:href="girl.url || `https://zh.moegirl.org/${girl.name}`"
:title="girl.reason" alt="portrait" target="_blank" rel="noopener"
>
<figure class="girl-info">
<img class="girl-avatar" loading="lazy" :src="girl.avatar" :alt="girl.name" :onError="onImgError">
<figcaption class="girl-name" :title="(i + 1).toString()">{{ girl.name }}</figcaption>
<figcaption class="girl-from">{{ girl.from }}</figcaption>
</figure>
</a>
</li>
<YunGirlItem v-for="girl, i in data" :key="i" :i="i" :girl="girl" />
</ul>
</div>
</template>
@ -53,55 +33,4 @@ const { data } = useRandomData(props.girls, props.random)
.girls-number {
color: white;
}
.girl-item {
display: inline-flex;
text-align: center;
justify-content: center;
width: 8rem;
margin: 1rem;
.girl {
&-info {
width: 100%;
padding: 0;
margin: 0;
}
&-avatar {
object-fit: cover;
object-position: center top;
width: 4rem;
height: 4rem;
border-radius: 50%;
padding: 0.2rem;
background-color: #fff;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.12);
transition: 0.5s;
&:hover {
box-shadow: 0 0 2rem rgba(0, 0, 0, 0.12);
}
}
&-name {
font-size: 0.9rem;
}
&-from {
font-size: 12px;
font-family: var(--va-font-serif);
font-weight: bold;
color: var(--va-c-text-light);
&::before {
content: '「';
}
&::after {
content: '」';
}
}
}
}
</style>

View File

@ -1,24 +1,5 @@
<script lang="ts" setup>
import { gsap } from 'gsap'
import ScrollToPlugin from 'gsap/ScrollToPlugin'
gsap.registerPlugin(ScrollToPlugin)
function goDown() {
const banner = document.getElementById('yun-banner')
if (banner) {
// nav menu height
const offset = 50
gsap.to(window, {
duration: 1,
scrollTo: {
y: banner.clientHeight - offset,
},
ease: 'power3.inOut',
})
}
}
import { goDown } from '../utils'
</script>
<template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { useMotion } from '@vueuse/motion'
import { ref } from 'vue'
import { onImgError } from '../utils'
import type { LinkType } from '../types'
const props = defineProps<{
i: number
errorImg?: string
link: LinkType
}>()
function onError(e: Event) {
onImgError(e, props.errorImg)
}
const itemRef = ref()
useMotion(itemRef, {
initial: {
opacity: 0,
translateY: 40,
},
enter: {
opacity: 1,
translateY: 0,
transition: {
type: 'spring',
duration: 400,
damping: 8,
delay: props.i * 50,
},
},
})
</script>
<template>
<li
ref="itemRef"
class="yun-link-item inline-flex"
:style="{
'--primary-color': link.color,
}"
>
<a
class="yun-link-url" p="x-4 y-2" :href="link.url" :title="link.name"
alt="portrait" rel="friend" target="_blank"
>
<div class="yun-link-left">
<img
class="yun-link-avatar" width="64" height="64" w="16" h="16"
loading="lazy"
:src="link.avatar" :alt="link.name"
@error="onError"
>
</div>
<div class="yun-link-info" m="l-2">
<div class="yun-link-blog" font="serif black">{{ link.blog }}</div>
<div class="yun-link-desc">{{ link.desc }}</div>
</div>
</a>
</li>
</template>

View File

@ -1,15 +1,6 @@
<script lang="ts" setup>
import { useRandomData } from '../composables'
import { onImgError } from '../utils'
interface LinkType {
avatar: string
name: string
url: string
color: string
blog: string
desc: string
}
import type { LinkType } from '../types'
const props = defineProps<{
links: string | LinkType[]
@ -21,50 +12,27 @@ const props = defineProps<{
}>()
const { data } = useRandomData(props.links, props.random)
function onError(e: Event) {
onImgError(e, props.errorImg)
}
</script>
<template>
<div class="links">
<ul class="link-items">
<li v-for="link, i in data" :key="i" class="link-item" :style="`--primary-color: ${link.color}`">
<a class="link-url" p="x-4 y-2" :href="link.url" :title="link.name" alt="portrait" rel="friend" target="_blank">
<div class="link-left">
<img
class="link-avatar" width="64" height="64" w="16" h="16"
loading="lazy" :src="link.avatar" :alt="link.name"
@error="onError"
>
</div>
<div class="link-info" m="l-2">
<div class="link-blog" font="serif black">{{ link.blog }}</div>
<div class="link-desc">{{ link.desc }}</div>
</div>
</a>
</li>
<div class="yun-links">
<ul class="yun-link-items" flex="center wrap">
<YunLinkItem
v-for="link, i in data"
:key="i"
:i="i" :link="link" :error-img="errorImg"
/>
</ul>
</div>
</template>
<style lang="scss" scoped>
.link-item {
display: inline-flex;
}
.links {
.link-items {
display: flex;
text-align: center;
justify-content: center;
flex-wrap: wrap;
<style lang="scss">
.yun-links {
.yun-link-items {
padding-left: 0;
}
.link-url {
.yun-link-url {
--smc-link-color: var(--primary-color);
display: inline-flex;
@ -82,7 +50,7 @@ function onError(e: Event) {
box-shadow: 0 2px 20px var(--primary-color, gray);
}
.link {
.yun-link {
&-left {
line-height: 0;
}
@ -97,7 +65,7 @@ function onError(e: Event) {
transition: 0.5s;
&:hover {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 20px rgb(0 0 0 / 0.1);
}
}
@ -111,7 +79,7 @@ function onError(e: Event) {
}
}
.link-info {
.yun-link-info {
display: inline-flex;
flex-direction: column;
justify-content: center;

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { useSiteConfig } from 'valaxy'
import { onMounted, ref } from 'vue'
import { onMounted, ref, watch } from 'vue'
import { useAppStore, useSiteConfig } from 'valaxy'
import { useRoute } from 'vue-router'
import { useYunAppStore } from '../stores'
const app = useAppStore()
const yunApp = useYunAppStore()
const siteConfig = useSiteConfig()
@ -19,38 +20,86 @@ onMounted(() => {
showMenu.value = true
}
})
const playAnimation = ref(false)
watch(() => yunApp.scrollY, () => {
if (yunApp.scrollY > 10)
playAnimation.value = true
else
playAnimation.value = false
})
const navMenu = ref([
{
title: '项目',
url: 'https://sponsors.yunyoujun.cn/projects',
},
{
title: '赞助者',
url: 'https://sponsors.yunyoujun.cn',
},
])
</script>
<template>
<Transition name="fade">
<div
v-if="showMenu"
class="yun-nav-menu shadow-xl"
class="yun-nav-menu z-99999"
border="~ solid $va-c-text"
:class="{
play: playAnimation,
}"
>
<!-- -->
<ValaxyHamburger
:active="yunApp.fullscreenMenu.isOpen"
class="menu-btn sidebar-toggle leading-4 size-12 bg-$va-c-bg"
class="menu-btn sidebar-toggle leading-4 size-12"
inline-flex cursor="pointer"
hover="bg-$va-c-bg-soft"
hover="bg-white/80 dark:bg-black/80"
z="$yun-z-menu-btn"
@click="yunApp.fullscreenMenu.toggle()"
/>
<NavMenuTitle />
<YunSearchTrigger v-if="siteConfig.search.enable" />
<div class="flex flex-1 px-2">
<YunNavMenuTitle />
</div>
<div class="inline-flex-center">
<template v-if="!app.isMobile">
<AppLink
v-for="item in navMenu"
:key="item.url"
:to="item.url"
class="menu-btn inline-flex-center p-2 transition text-$va-c-text"
inline-flex cursor="pointer"
hover="bg-white/80 dark:bg-black/80"
z="$yun-z-menu-btn"
>
{{ item.title }}
</AppLink>
</template>
<YunClassifyPopover v-if="!yunApp.size.isLg" />
<YunSearchTrigger v-if="siteConfig.search.enable" />
</div>
</div>
</Transition>
</template>
<style lang="scss">
@use 'sass:map';
@use 'valaxy-theme-yun/styles/vars.scss' as *;
@use "valaxy/client/styles/mixins/index.scss" as *;
.yun-nav-menu {
position: fixed;
animation: nav-menu-appear 0.3s linear forwards;
animation-timeline: scroll();
animation-range: 0 calc(30vh);
// safari not support
// animation-timeline: scroll();
// animation-range: 0 calc(30vh), exit;
background-color: transparent;
box-shadow: none;
z-index: var(--yun-z-menu-btn);
display: flex;
top: var(--rect-margin);
@ -59,27 +108,23 @@ onMounted(() => {
align-items: center;
justify-content: space-between;
height: 50px;
transition: all 0.5s map.get($cubic-bezier, 'ease-in');
&.play {
top: 0;
left: 0;
right: 0;
background-color: var(--va-c-bg);
border-color: rgb(0 0 0 / 0.1);
--un-shadow: var(--un-shadow-inset) 0 20px 25px -5px var(--un-shadow-color, rgb(0 0 0 / 0.1)), var(--un-shadow-inset) 0 8px 10px -6px var(--un-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow),
var(--un-shadow);
}
.vt-hamburger-top, .vt-hamburger-middle, .vt-hamburger-bottom {
background-color: var(--va-c-text);
}
}
@keyframes nav-menu-appear {
from {
top: var(--rect-margin);
left: var(--rect-margin);
right: var(--rect-margin);
background-color: transparent;
box-shadow: none;
}
to {
top: 0;
left: 0;
right: 0;
background-color: var(--va-c-bg);
border-color: rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@ -20,27 +20,6 @@
<style lang="scss">
@use "valaxy/client/styles/mixins/index.scss" as *;
.site-author-avatar {
display: inline-block;
line-height: 0;
position: relative;
img {
height: 96px;
width: 96px;
max-width: 100%;
margin: 0;
padding: 4px;
background-color: white;
box-shadow: 0 0 10px rgba(black, 0.2);
transition: 0.4s;
&:hover {
box-shadow: 0 0 30px rgba(var(--va-c-primary-rgb), 0.2);
}
}
}
.site-author-status {
position: absolute;
height: 1.8rem;
@ -49,9 +28,9 @@
right: 0;
line-height: 1.8rem;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
background-color: var(--va-c-bg-light);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid rgb(255 255 255 / 0.1);
}
.site-name {

View File

@ -11,7 +11,7 @@ defineProps<{
</script>
<template>
<header class="post-header" m="t-20">
<header class="post-header">
<h1
class="post-title flex-center" p="2" text="2xl center"
font="serif black"

View File

@ -14,7 +14,13 @@ const { icon, styles } = usePostProperty(props.post.type)
<template>
<YunCard
m="y-4 auto"
v-animateonscroll="{
// translate
enterClass: 'animate-fade-in animate-duration-200ms',
leaveClass: 'animate-fade-out animate-duration-200ms',
}"
class="w-full"
m="auto"
:class="post.cover ? 'post-card-image' : 'post-card'"
overflow="hidden" :style="styles"
>
@ -83,9 +89,10 @@ const { icon, styles } = usePostProperty(props.post.type)
<style lang="scss">
.post-card {
animation: card-appear 0.6s ease-in-out forwards, card-appear 0.6s ease-in-out forwards reverse;
animation-timeline: view();
animation-range: entry, exit;
// safari not support
// animation: card-appear 0.6s ease-in-out forwards, card-appear 0.6s ease-in-out forwards reverse;
// animation-timeline: view();
// animation-range: entry, exit;
}
@keyframes card-appear {
@ -101,6 +108,6 @@ const { icon, styles } = usePostProperty(props.post.type)
}
.yun-card-actions {
border-top: 1px solid rgba(122, 122, 122, 0.05);
border-top: 1px solid rgb(122 122 122 / 0.05);
}
</style>

View File

@ -19,7 +19,7 @@ defineProps<{
>
<div m="x-1" inline-flex i-ri-folder-2-line />
<span>
{{ Array.isArray(categories) ? categories.join(' > ') : categories }}
{{ Array.isArray(categories) ? categories.join(' / ') : categories }}
</span>
</RouterLink>
</template>

View File

@ -44,7 +44,7 @@ const sortedYears = computed(() => {
</script>
<template>
<div class="post-collapse px-10 lt-sm:px-5" relative>
<div class="post-collapse px-10 lt-sm:px-5 max-w-3xl" relative>
<div w="full" text="center" class="yun-text-light" p="2">
{{ t('counter.archives', posts.length) }}
</div>
@ -65,15 +65,18 @@ const sortedYears = computed(() => {
<article
v-for="post, j in sortByDate(postListByYear[year], isDesc)" :key="j"
class="post-item" relative
class="post-item relative"
>
<header class="post-header" flex items-center relative>
<header
class="post-header cursor-pointer" flex items-center relative
hover="bg-black/1"
>
<div class="post-meta">
<time v-if="post.date" class="post-time" font="mono" opacity="80">{{
formatDate(post.date, 'MM-dd') }}
</time>
</div>
<h2 class="post-title" inline-flex items-center font="serif black">
<h2 class="post-title w-full" inline-flex items-center font="serif black">
<RouterLink :to="post.path || ''" class="post-title-link">
{{ post.title }}
</RouterLink>

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import type { Post } from 'valaxy'
import { formatDate } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { formatTimestamp } from '../utils'
defineProps<{
// FrontMatter
frontmatter: Post
}>()
const { t } = useI18n()
</script>
<template>
<div v-if="frontmatter.date" class="post-time flex items-center gap-4">
<span class="posted-time inline-flex-center gap-1" :title="t('post.posted') + formatTimestamp(frontmatter.date)">
<div class="inline-block" i-ri-calendar-line />
<time class="op-80">{{ formatDate(frontmatter.date) }}</time>
</span>
<span
v-if="frontmatter.updated && frontmatter.updated !== frontmatter.date"
class="edited-time inline-flex-center gap-1" :title="t('post.edited') + formatTimestamp(frontmatter.updated)"
>
<div i-ri-calendar-2-line />
<time class="op-80">{{ formatDate(frontmatter.updated) }}</time>
</span>
</div>
</template>

View File

@ -28,17 +28,18 @@ const displayedPosts = computed(() =>
</script>
<template>
<div class="yun-post-list" w="full" p="x-4 lt-sm:0">
<div flex="~ col" class="yun-post-list gap-4" w="full" p="x-4 lt-sm:0">
<template v-if="!displayedPosts.length">
<div py2 op50 text-center>
博主还什么都没写哦
</div>
</template>
<TransitionGroup name="fade">
<YunPostCard v-for="route, i in displayedPosts" :key="i" :post="route" />
</TransitionGroup>
<YunPostCard v-for="route, i in displayedPosts" :key="i" :post="route" />
</div>
<YunPagination :total="posts.length" :page-size="pageSize" />
<YunPagination
class="mt-5"
:total="posts.length" :page-size="pageSize"
/>
</template>

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
import type { Post } from 'valaxy'
import { formatDate, useSiteConfig } from 'valaxy'
import { useAppStore, useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { formatTimestamp } from '../utils'
defineProps<{
// FrontMatter
frontmatter: Post
}>()
const app = useAppStore()
const { t } = useI18n()
const siteConfig = useSiteConfig()
@ -27,50 +27,42 @@ const siteConfig = useSiteConfig()
</div>
<div
v-if="frontmatter" class="post-meta"
flex="~ col" justify="center" items="center" text="sm" py="1"
v-if="frontmatter"
class="post-meta gap-4"
flex="~ center"
text="sm"
:class="{
'flex-col gap-2!': app.isMobile || frontmatter.updated,
}"
>
<div v-if="frontmatter.date" class="post-time flex items-center">
<span class="posted-time inline-flex-center" :title="t('post.posted') + formatTimestamp(frontmatter.date)">
<div class="inline-block" i-ri-calendar-line />
<time m="l-1">{{ formatDate(frontmatter.date) }}</time>
</span>
<YunPostDateMeta :frontmatter="frontmatter" />
<span
v-if="frontmatter.updated && frontmatter.updated !== frontmatter.date"
class="edited-time inline-flex-center" :title="t('post.edited') + formatTimestamp(frontmatter.updated)"
<div class="inline-flex-center gap-4">
<div
v-if="siteConfig.statistics.enable"
class="inline-flex-center post-counter gap-4"
>
<span m="x-2">-</span>
<div i-ri-calendar-2-line />
<time m="l-1">{{ formatDate(frontmatter.updated) }}</time>
</span>
</div>
<span
v-if="frontmatter.wordCount"
class="word-count inline-flex-center gap-1" :title="t('statistics.word')"
>
<div class="inline-block" i-ri-file-word-line />
<span class="op-80">{{ frontmatter.wordCount }}</span>
</span>
<div
v-if="siteConfig.statistics.enable"
class="post-counter flex items-center" mt="2"
>
<span
v-if="frontmatter.wordCount"
class="word-count inline-flex-center" :title="t('statistics.word')"
>
<div class="inline-block" i-ri-file-word-line />
<span m="l-1">{{ frontmatter.wordCount }}</span>
</span>
<span
v-if="frontmatter.readingTime"
class="reading-time inline-flex-center gap-1"
:title="t('statistics.time')"
>
<div i-ri-timer-line />
<time class="op-80">{{ frontmatter.readingTime }}m</time>
</span>
</div>
<span
v-if="frontmatter.readingTime"
class="reading-time inline-flex-center"
:title="t('statistics.time')"
>
<span m="x-2">-</span>
<div i-ri-timer-line />
<time m="l-1">{{ frontmatter.readingTime }}m</time>
</span>
<YunWalineMeta />
</div>
</div>
<slot />
</template>
<style>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { useCategories, useSiteStore, useTags } from 'valaxy'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const site = useSiteStore()
const categories = useCategories()
const tags = useTags()
</script>
<template>
<nav class="site-nav" text-xl mt-6>
<YunPostClassifyNavItem
to="/archives/" icon="i-ri-archive-line" :title="t('menu.archives')"
:total="site.postList.length"
/>
<YunPostClassifyNavItem
to="/categories/" icon="i-ri-folder-2-line" :title="t('menu.categories')"
:total="Array.from(categories.children).length"
/>
<YunPostClassifyNavItem
to="/tags/" icon="i-ri-price-tag-3-line" :title="t('menu.tags')"
:total="Array.from(tags).length"
/>
</nav>
</template>
<style lang="scss">
@use "valaxy/client/styles/mixins/index.scss" as *;
.site-nav {
display: flex;
justify-content: center;
overflow: hidden;
line-height: 1.5;
white-space: nowrap;
text-align: center;
margin-top: 1rem;
}
</style>

View File

@ -21,9 +21,9 @@ function onClick() {
<template>
<button
class="yun-search-btn popup-trigger h-full inline-flex justify-center items-center aspect-1 bg-$va-c-bg"
class="yun-search-btn popup-trigger size-12 inline-flex justify-center items-center"
text="xl $va-c-text"
hover="bg-$va-c-bg-soft"
hover="bg-white/80 dark:bg-black/80"
:title="t('menu.search')"
@click="onClick"
>

View File

@ -55,14 +55,4 @@ function toggleOptionVisible(e: MouseEvent) {
.select-options {
margin: 0;
}
.v-enter-active,
.v-leave-active {
transition: opacity .2s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
// new sidebar card
</script>
<template>
<YunCard class="sticky top-68px p-4 min-h-sm w-80">
<YunSiteInfo class="text-center" />
<YunPostsInfo />
</YunCard>
</template>

View File

@ -22,6 +22,27 @@ const siteConfig = useSiteConfig()
</template>
<style lang="scss">
.site-author-avatar {
display: inline-block;
line-height: 0;
position: relative;
img {
height: 96px;
width: 96px;
max-width: 100%;
margin: 0;
padding: 4px;
background-color: white;
box-shadow: 0 0 10px rgba(black, 0.2);
transition: 0.4s;
&:hover {
box-shadow: 0 0 30px rgba(var(--va-c-primary-rgb), 0.2);
}
}
}
.site-info {
&.fix-top {
margin-top: -1.5rem;

View File

@ -49,7 +49,7 @@ const sponsorBtnTitle = computed(() => {
@use "valaxy/client/styles/mixins/index.scss" as *;
.sponsor-button {
background-color: rgba(255, 255, 255, 0.1);
background-color: rgb(255 255 255 / 0.1);
div {
transform: scale(1.1);
@ -57,7 +57,7 @@ const sponsorBtnTitle = computed(() => {
}
&:hover {
background-color: rgba(255, 255, 255, 0.9);
background-color: rgb(255 255 255 / 0.9);
div {
transform: scale(1.2);
@ -74,10 +74,12 @@ const sponsorBtnTitle = computed(() => {
.qrcode-container {
overflow: hidden;
height: 0;
transition: height var(--va-transition-duration) map.get($cubic-bezier, 'ease-in-out');
opacity: 0;
transition: all var(--va-transition-duration) map.get($cubic-bezier, 'ease-in-out');
&.show {
height: 260px;
opacity: 1;
}
}

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{
icon: string
}>()
</script>
<template>
<div
class="size-12 inline-flex-center cursor-pointer"
text="$va-c-text"
hover="bg-white/80 dark:bg-black/80 op-100"
op-80
>
<div class="size-6" :class="icon" />
</div>
</template>

View File

@ -1,14 +1,32 @@
<script setup lang="ts">
import { useFrontmatter, useSiteConfig } from 'valaxy'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useYunAppStore } from '../../stores'
const yunApp = useYunAppStore()
const fm = useFrontmatter()
const siteConfig = useSiteConfig()
const showPostTitle = ref(false)
watch(() => yunApp.scrollY, () => {
showPostTitle.value = yunApp.scrollY > 200
})
const router = useRouter()
function goToLink() {
if (!showPostTitle.value)
router.push('/')
}
</script>
<template>
<div
v-motion
flex="~ col h-full"
:class="{
'cursor-pointer': !showPostTitle,
}"
:initial="{
opacity: 0,
y: 10,
@ -21,12 +39,16 @@ const siteConfig = useSiteConfig()
delay: 1200,
},
}"
@click="goToLink"
>
<span
v-if="fm.title"
v-if="fm.title && showPostTitle"
class="nav-menu-post-title text-sm font-bold flex items-center gap-1 truncate"
>
<div class="size-4" i-ri-article-line />
<div
class="size-4"
:class="fm.icon || 'i-ri-article-line'"
/>
<span> {{ fm.title }}</span>
</span>
<span v-else class="font-light truncate">
@ -37,9 +59,11 @@ const siteConfig = useSiteConfig()
<style lang="scss">
.nav-menu-post-title {
// safari not support
animation: nav-menu-title-appear 0.3s linear forwards;
animation-timeline: scroll();
animation-range: 0 calc(30vh);
// animation-timeline: scroll();
// animation-range: 0 calc(30vh);
}
@keyframes nav-menu-title-appear {

View File

@ -0,0 +1,29 @@
<script lang="ts" setup>
defineProps<{
icon: string
title: string
/**
* Total number
*/
total: number
to: string
}>()
</script>
<template>
<RouterLink
flex="~ col center"
class="gap-1 w-20"
:to="to" :title="title"
>
<div flex="~ col" class="text-$va-c-text inline-flex-center gap-1">
<div class="text-xl" :class="icon" />
<span class="text-sm">
{{ title }}
</span>
</div>
<span
class="count text-base"
>{{ total }}</span>
</RouterLink>
</template>

View File

@ -11,15 +11,15 @@ const yunApp = useYunAppStore()
<template>
<AppLink
class="link-item w-full h-12 items-center justify-center gap-2 transition rounded"
class="link-item w-full items-center justify-center gap-2 transition rounded text-xl p-3"
inline-flex
:to="page.url" :title="page.name"
:style="`color:${page.color}`"
hover="bg-gray-100/50 dark:bg-gray-800/50"
@click="yunApp.fullscreenMenu.isOpen = false"
>
<div :class="page.icon" class="icon size-6" />
<span class="text-lg">
<div :class="page.icon" class="icon" />
<span>
{{ page.name }}
</span>
</AppLink>

View File

@ -14,14 +14,26 @@ const { t } = useI18n()
</script>
<template>
<div v-if="addonWaline && addonWaline.options" flex="~" text="sm" my="1" h="5">
<div v-if="addonWaline.options.pageview" inline-flex justify="center" items="center" mx="2" :title="t('post.pageview_count')">
<div v-if="addonWaline && addonWaline.options" class="inline-flex gap-4" text="sm" h="5">
<div
v-if="addonWaline.options.pageview" inline-flex justify="center" items="center"
:title="t('post.pageview_count')"
>
<div inline-flex i-ri-eye-line />
<span ml-1 inline-flex class="waline-pageview-count" :data-path="route.path" />
<span
ml-1 inline-flex class="waline-pageview-count op-80"
:data-path="route.path"
/>
</div>
<div v-if="addonWaline.options.comment" inline-flex justify="center" items="center" mx="2" :title="t('post.comment_count')">
<div
v-if="addonWaline.options.comment" inline-flex justify="center" items="center"
:title="t('post.comment_count')"
>
<div inline-flex i-ri-chat-4-line />
<span ml-1 inline-flex class="waline-comment-count" :data-path="route.path" />
<span
ml-1 inline-flex class="waline-comment-count op-80"
:data-path="route.path"
/>
</div>
</div>
</template>

View File

@ -1,10 +1,15 @@
<script lang="ts" setup>
import { defineWebPage, useSchemaOrg } from '@unhead/schema-org'
import { useFrontmatter, usePostTitle, useSiteStore } from 'valaxy'
import { useAppStore, useFrontmatter, usePostTitle, useSiteStore } from 'valaxy'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useYunAppStore } from '../stores'
const { t } = useI18n()
const app = useAppStore()
const yun = useYunAppStore()
const frontmatter = useFrontmatter()
const title = usePostTitle(frontmatter)
@ -15,28 +20,43 @@ useSchemaOrg([
'@type': 'CollectionPage',
}),
])
const pageIcon = computed(() => {
if (!frontmatter.value.icon)
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
frontmatter.value.icon = 'i-ri-archive-line'
return frontmatter.value.icon
})
</script>
<template>
<YunSidebar v-if="$slots['sidebar-child']">
<slot name="sidebar-child" />
</YunSidebar>
<YunSidebar v-else />
<div
flex="~"
class="mt-24 md:mt-36 w-full max-w-screen-2xl m-auto justify-center items-start gap-4"
:class="{
'flex-col': app.isMobile,
}"
>
<YunSidebarCard v-if="yun.size.isLg" />
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
:title="title || t('menu.archives')"
:icon="frontmatter.icon || 'i-ri-archive-line'"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
</template>
<template #main-content>
<RouterView />
<YunPostCollapse :posts="site.postList" />
</template>
</component>
</RouterView>
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
class="mt-8"
:title="title || t('menu.archives')"
:icon="pageIcon"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
</template>
<template #main-content>
<RouterView />
<YunPostCollapse :posts="site.postList" />
</template>
</component>
</RouterView>
</div>
<YunFooter />
</template>

View File

@ -1,12 +1,15 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useCategories, useFrontmatter, usePostTitle, useSiteStore } from 'valaxy'
import { useAppStore, useCategories, useFrontmatter, usePostTitle, useSiteStore } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { defineWebPage, useSchemaOrg } from '@unhead/schema-org'
import { useYunAppStore } from '../stores'
const { t } = useI18n()
const app = useAppStore()
const yun = useYunAppStore()
const site = useSiteStore()
const frontmatter = useFrontmatter()
@ -14,6 +17,13 @@ const route = useRoute()
const curCategory = computed(() => (route.query.category as string || ''))
const categories = useCategories()
const pageIcon = computed(() => {
if (!frontmatter.value.icon)
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
frontmatter.value.icon = 'i-ri-folder-2-line'
return frontmatter.value.icon
})
const posts = computed(() => {
const list = site.postList.filter((post) => {
if (post.categories && curCategory.value !== 'Uncategorized') {
@ -39,38 +49,47 @@ useSchemaOrg([
</script>
<template>
<YunSidebar v-if="$slots['sidebar-child']">
<slot name="sidebar-child" />
</YunSidebar>
<YunSidebar v-else />
<div
flex="~"
class="mt-24 md:mt-36 w-full max-w-screen-2xl m-auto justify-center items-start gap-4"
:class="{
'flex-col': app.isMobile,
}"
>
<YunSidebarCard v-if="yun.size.isLg" />
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
:title="title || t('menu.categories')"
:icon="frontmatter.icon || 'i-ri-folder-2-line'"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
</template>
<template #main-content>
<div text="center" class="yun-text-light" p="2">
{{ t('counter.categories', Array.from(categories.children).length) }}
</div>
<YunCategories :categories="categories.children" />
<RouterView />
</template>
<template #main-nav-before>
<YunCard v-if="curCategory" class="post-collapse-container" m="t-4" w="full">
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
:title="curCategory === 'Uncategorized' ? t('category.uncategorized') : curCategory.split('/').join(' / ')"
icon="i-ri-folder-open-line"
class="mt-8"
:title="title || t('menu.categories')"
:icon="pageIcon"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
<YunPostCollapse w="full" m="b-4" p="x-20 lt-sm:x-5" :posts="posts" />
</YunCard>
</template>
</component>
</RouterView>
</template>
<template #main-content>
<div text="center" class="yun-text-light" p="2">
{{ t('counter.categories', Array.from(categories.children).length) }}
</div>
<YunCategories :categories="categories.children" />
<RouterView />
</template>
<template #main-nav-before>
<YunCard v-if="curCategory" class="post-collapse-container" m="t-4" w="full">
<YunPageHeader
m="t-10"
:title="curCategory === 'Uncategorized' ? t('category.uncategorized') : curCategory.split('/').join(' / ')"
icon="i-ri-folder-open-line"
/>
<YunPostCollapse w="full" m="b-4" p="x-20 lt-sm:x-5" :posts="posts" />
</YunCard>
</template>
</component>
</RouterView>
</div>
<YunFooter />
</template>

View File

@ -1,10 +1,19 @@
<script setup lang="ts">
import { useYunAppStore } from '../stores'
const yun = useYunAppStore()
</script>
<template>
<YunSidebar v-if="$slots['sidebar-child']">
<slot name="sidebar-child" />
</YunSidebar>
<YunSidebar v-else />
<RouterView />
<div
flex="~"
class="mt-24 md:mt-36 w-full max-w-screen-2xl m-auto justify-center items-start gap-4"
:class="{
'flex-col': yun.size.isXs,
}"
>
<YunSidebarCard v-if="yun.size.isLg" />
<RouterView />
</div>
<YunFooter />
</template>

View File

@ -0,0 +1,3 @@
<template>
<RouterView />
</template>

View File

@ -19,7 +19,11 @@ const showNotice = computed(() => {
<template>
<main
class="yun-main flex-center"
:class="!yunStore.leftSidebar.isOpen ? 'pl-0' : 'md:pl-$va-sidebar-width'" flex="~ col" w="full"
:class="{
'pl-0': !yunStore.leftSidebar.isOpen,
'md:pl-$va-sidebar-width': yunStore.leftSidebar.isOpen,
'pt-36': isPage,
}" flex="~ col" w="full"
>
<YunSidebar :show-hamburger="true" />
@ -31,7 +35,11 @@ const showNotice = computed(() => {
<YunNotice
v-if="showNotice"
:content="themeConfig.notice.content" mt="4"
class="mb-10"
:class="{
'mt-4': !isPage,
}"
:content="themeConfig.notice.content"
/>
<slot name="board" />

View File

@ -1,11 +1,14 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useFrontmatter, useFullUrl, useSiteConfig } from 'valaxy'
import { useAppStore, useFrontmatter, useFullUrl, useSiteConfig } from 'valaxy'
import type { Article } from '@unhead/schema-org'
import { defineArticle, useSchemaOrg } from '@unhead/schema-org'
import { toDate } from 'date-fns'
import { useYunAppStore } from '../stores'
const app = useAppStore()
const yun = useYunAppStore()
const siteConfig = useSiteConfig()
const frontmatter = useFrontmatter()
const url = useFullUrl()
@ -41,18 +44,26 @@ useSchemaOrg(
</script>
<template>
<div class="mt-40">
<YunSidebar v-if="$slots['sidebar-child']">
<div
flex="~"
class="mt-24 md:mt-36 w-full max-w-screen-2xl m-auto justify-center items-start gap-4"
:class="{
'flex-col': app.isMobile,
}"
>
<!-- <YunSidebar v-if="$slots['sidebar-child']">
<slot name="sidebar-child" />
</YunSidebar>
<YunSidebar v-else />
<YunSidebar v-else /> -->
<RouterView v-slot="{ Component }" class="mt-30">
<YunSidebarCard v-if="yun.size.isLg" />
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header-after>
<YunPostMeta :frontmatter="frontmatter" />
<YunWalineMeta />
<YunPostCategoriesAndTags :frontmatter="frontmatter" />
<YunPostCategoriesAndTags class="mt-2" :frontmatter="frontmatter" />
</template>
<template #main-content-after>
@ -65,5 +76,9 @@ useSchemaOrg(
</template>
</component>
</RouterView>
<YunAside v-if="!app.isMobile" />
</div>
<YunFooter />
</template>

View File

@ -1,10 +1,11 @@
<script lang="ts" setup>
import { useFrontmatter, useInvisibleElement, usePostTitle, useSiteStore } from 'valaxy'
import { useAppStore, useFrontmatter, useInvisibleElement, usePostTitle, useSiteStore } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { defineWebPage, useSchemaOrg } from '@unhead/schema-org'
import { useThemeConfig, useYunTags } from '../composables'
import { useYunAppStore } from '../stores'
useSchemaOrg([
defineWebPage({
@ -12,6 +13,9 @@ useSchemaOrg([
}),
])
const app = useAppStore()
const yun = useYunAppStore()
const route = useRoute()
const router = useRouter()
@ -58,46 +62,54 @@ const title = usePostTitle(frontmatter)
</script>
<template>
<YunSidebar v-if="$slots['sidebar-child']">
<slot name="sidebar-child" />
</YunSidebar>
<YunSidebar v-else />
<div
flex="~"
class="mt-24 md:mt-36 w-full max-w-screen-2xl m-auto justify-center items-start gap-4"
:class="{
'flex-col': app.isMobile,
}"
>
<YunSidebarCard v-if="yun.size.isLg" />
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
:title="title || t('menu.tags')"
:icon="frontmatter.icon || 'i-ri-tag-line'"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
</template>
<template #main-content>
<div class="yun-text-light" text="center" p="2">
{{ t('counter.tags', Array.from(tags).length) }}
</div>
<div class="justify-center items-end" flex="~ wrap" gap="1">
<YunLayoutPostTag
v-for="[key, tag] in Array.from(tags).sort()"
:key="key"
:title="key"
:count="tag.count"
:style="getTagStyle(tag.count)"
@click="displayTag(key.toString())"
<RouterView v-slot="{ Component }">
<component :is="Component">
<template #main-header>
<YunPageHeader
class="mt-8"
:title="title || t('menu.tags')"
:icon="frontmatter.icon || 'i-ri-tag-line'"
:color="frontmatter.color"
:page-title-class="frontmatter.pageTitleClass"
/>
</div>
</template>
<template #main-content>
<div class="yun-text-light" text="center" p="2">
{{ t('counter.tags', Array.from(tags).length) }}
</div>
<RouterView />
</template>
<div class="justify-center items-end" flex="~ wrap" gap="1">
<YunLayoutPostTag
v-for="[key, tag] in Array.from(tags).sort()"
:key="key"
:title="key"
:count="tag.count"
:style="getTagStyle(tag.count)"
@click="displayTag(key.toString())"
/>
</div>
<template #main-nav-before>
<YunCard v-if="curTag" ref="collapse" m="t-4" w="full">
<YunPageHeader :title="curTag" icon="i-ri-hashtag" />
<YunPostCollapse w="full" m="b-4" p="x-20 lt-sm:x-5" :posts="posts" />
</YunCard>
</template>
</component>
</RouterView>
<RouterView />
</template>
<template #main-nav-before>
<YunCard v-if="curTag" ref="collapse" m="t-4" w="full">
<YunPageHeader :title="curTag" icon="i-ri-hashtag" />
<YunPostCollapse w="full" m="b-4" p="x-20 lt-sm:x-5" :posts="posts" />
</YunCard>
</template>
</component>
</RouterView>
</div>
<YunFooter />
</template>

View File

@ -25,7 +25,9 @@
"@iconify-json/simple-icons": "^1.2.5",
"@vueuse/motion": "^2.2.5",
"animejs": "^3.2.2",
"gsap": "^3.12.5"
"gsap": "^3.12.5",
"primevue": "^4.0.7",
"radix-vue": "^1.9.6"
},
"devDependencies": {
"@types/animejs": "^3.1.12",

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
// dynamic page
// layout is home
</script>
<template>

View File

@ -2,12 +2,23 @@ import { defineAppSetup } from 'valaxy'
import '../styles/global.scss'
import { MotionPlugin } from '@vueuse/motion'
import PrimeVue from 'primevue/config'
import AnimateOnScroll from 'primevue/animateonscroll'
import StyleClass from 'primevue/styleclass'
export default defineAppSetup((ctx) => {
// can do for setup
const { router, app } = ctx
// https://motion.vueuse.org/
app.use(MotionPlugin)
// primevue
app.use(PrimeVue, {
unstyled: true,
})
app.directive('animateonscroll', AnimateOnScroll)
app.directive('styleclass', StyleClass)
router.afterEach(() => {
/**
* router import order

View File

@ -1,21 +1,34 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useToggle } from '@vueuse/core'
import { useScroll, useToggle } from '@vueuse/core'
import { useScreenSize } from 'valaxy'
export const useYunAppStore = defineStore('yun-app', () => {
const { y } = useScroll(window)
// 左侧边栏
const [isLeftSidebarOpen, toggleLeftSidebar] = useToggle()
// 右侧边栏
const [isRightSidebarOpen, toggleRightSidebar] = useToggle()
// 全屏菜单
const [isFullscreenMenuOpen, toggleFullscreenMenu] = useToggle()
// init once
const size = useScreenSize()
return {
size,
leftSidebar: {
isOpen: isLeftSidebarOpen,
toggle: toggleLeftSidebar,
},
rightSidebar: {
isOpen: isRightSidebarOpen,
toggle: toggleRightSidebar,
},
fullscreenMenu: {
isOpen: isFullscreenMenuOpen,
toggle: toggleFullscreenMenu,
},
scrollY: y,
}
})

View File

@ -1,6 +1,19 @@
// cubic-bezier(0.18, 0.66, 0.05, 0.96)
// cubic-bezier(0.77, 0, 0.17, 1.02)
@use 'sass:map';
@use '../vars.scss' as *;
.v-enter-active,
.v-leave-active {
transition: opacity .2s map.get($cubic-bezier, 'ease-in');
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
opacity: 1;

View File

@ -224,3 +224,26 @@ export interface ThemeConfig extends DefaultTheme.Config {
}
export type UserThemeConfig = Partial<ThemeConfig>
/**
* For user links
*/
export interface LinkType {
avatar: string
name: string
url: string
color: string
blog: string
desc: string
}
/**
* girl
*/
export interface GirlType {
name: string
url: string
avatar: string
from?: string
reason?: string
}

View File

@ -3,7 +3,7 @@ import { defineConfig } from 'unocss'
export default defineConfig({
shortcuts: [
['yun-main', 'lt-md:pl-0'],
['yun-card', 'm-auto transition yun-transition shadow hover:shadow-lg'],
['yun-card', 'transition yun-transition shadow hover:shadow-lg'],
],
rules: [
[

View File

@ -0,0 +1,2 @@
export * from './useInvisibleElement'
export * from './useScreenSize'

View File

@ -0,0 +1,22 @@
import { useMediaQuery } from '@vueuse/core'
/**
* breakpoints ref https://tailwindcss.com/docs/screens
*/
export function useScreenSize() {
const isXs = useMediaQuery('(max-width: 639px)')
const isSm = useMediaQuery('(min-width: 640px)')
const isMd = useMediaQuery('(min-width: 768px)')
const isLg = useMediaQuery('(min-width: 1024px)')
const isXl = useMediaQuery('(min-width: 1280px)')
const is2xl = useMediaQuery('(min-width: 1536px)')
return {
isXs,
isSm,
isMd,
isLg,
isXl,
is2xl,
}
}

View File

@ -1,5 +1,4 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useToggle } from '@vueuse/core'
import { ref } from 'vue'
import { useMobile, useThemeConfig, useValaxyDark } from 'valaxy'
@ -18,9 +17,6 @@ export const useAppStore = defineStore('app', () => {
const isMobile = useMobile()
const showLoading = ref(true)
// right sidebar with toc
const [isRightSidebarOpen, toggleRightSidebar] = useToggle(false)
return {
isMobile,
// for dark
@ -30,9 +26,6 @@ export const useAppStore = defineStore('app', () => {
toggleDarkWithTransition,
showLoading,
isRightSidebarOpen,
toggleRightSidebar,
}
})

View File

@ -66,7 +66,7 @@ const EXCLUDE = [
]
export function createConfigPlugin(options: ResolvedValaxyOptions): Plugin {
const themeDeps = Object.keys((options.config.themeConfig.pkg.dependencies || {}))
// const themeDeps = Object.keys((options.config.themeConfig.pkg.dependencies || {}))
const addonDeps = options.addons.map(i => Object.keys(i.pkg.dependencies || {})).flat()
return {
@ -94,8 +94,8 @@ export function createConfigPlugin(options: ResolvedValaxyOptions): Plugin {
// must need it
include: uniq([
...clientDeps,
// theme deps
...themeDeps,
// remove theme deps, for primevue parse entry
// ...themeDeps,
// addon deps
...addonDeps,
]).filter(i => !EXCLUDE.includes(i)),

View File

@ -705,6 +705,12 @@ importers:
gsap:
specifier: ^3.12.5
version: 3.12.5
primevue:
specifier: ^4.0.7
version: 4.0.7(vue@3.5.10(typescript@5.6.2))
radix-vue:
specifier: ^1.9.6
version: 1.9.6(vue@3.5.10(typescript@5.6.2))
devDependencies:
'@types/animejs':
specifier: ^3.1.12
@ -1877,6 +1883,24 @@ packages:
'@polka/url@1.0.0-next.28':
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
'@primeuix/styled@0.0.5':
resolution: {integrity: sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==}
engines: {node: '>=12.11.0'}
'@primeuix/utils@0.0.5':
resolution: {integrity: sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==}
engines: {node: '>=12.11.0'}
'@primevue/core@4.0.7':
resolution: {integrity: sha512-SvWiNBEeR6hm4wjnze+rITUjHMFLwIzpRFlq+GqmJyZmjJy4h8UUksi0EoyqAWCAwKgmwlxY6XNqGJmMVyOguQ==}
engines: {node: '>=12.11.0'}
peerDependencies:
vue: ^3.0.0
'@primevue/icons@4.0.7':
resolution: {integrity: sha512-tj4dfRdV5iN6O0mbkpjhMsGlT3wZTqOPL779ndY5gKuCwN5zcFmKmABWVQmr/ClRivnMkw6Yr1x6gRTV/N0ydg==}
engines: {node: '>=12.11.0'}
'@radix-ui/colors@3.0.0':
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
@ -6602,6 +6626,10 @@ packages:
resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==}
engines: {node: '>=18'}
primevue@4.0.7:
resolution: {integrity: sha512-88qazHqldkqsCxvhjnjO65XMBfJyHQoFW3BQvrJYO6RqPheHB4f7cY61eqtBpJAjnM5x+YKTZiWx/gBuUzqT7Q==}
engines: {node: '>=12.11.0'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@ -9786,6 +9814,25 @@ snapshots:
'@polka/url@1.0.0-next.28': {}
'@primeuix/styled@0.0.5':
dependencies:
'@primeuix/utils': 0.0.5
'@primeuix/utils@0.0.5': {}
'@primevue/core@4.0.7(vue@3.5.10(typescript@5.6.2))':
dependencies:
'@primeuix/styled': 0.0.5
'@primeuix/utils': 0.0.5
vue: 3.5.10(typescript@5.6.2)
'@primevue/icons@4.0.7(vue@3.5.10(typescript@5.6.2))':
dependencies:
'@primeuix/utils': 0.0.5
'@primevue/core': 4.0.7(vue@3.5.10(typescript@5.6.2))
transitivePeerDependencies:
- vue
'@radix-ui/colors@3.0.0': {}
'@react-dnd/asap@4.0.1': {}
@ -15265,6 +15312,15 @@ snapshots:
dependencies:
parse-ms: 4.0.0
primevue@4.0.7(vue@3.5.10(typescript@5.6.2)):
dependencies:
'@primeuix/styled': 0.0.5
'@primeuix/utils': 0.0.5
'@primevue/core': 4.0.7(vue@3.5.10(typescript@5.6.2))
'@primevue/icons': 4.0.7(vue@3.5.10(typescript@5.6.2))
transitivePeerDependencies:
- vue
process-nextick-args@2.0.1: {}
prompts@2.4.2: