valaxy-theme-yun 1.0.0-rc.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.npmignore +8 -0
  2. package/App.vue +9 -6
  3. package/components/ValaxyMain.vue +6 -0
  4. package/components/YunAside.vue +97 -7
  5. package/components/YunBackToTop.vue +86 -39
  6. package/components/YunBanner.vue +6 -2
  7. package/components/YunCard.vue +7 -1
  8. package/components/YunComment.vue +1 -1
  9. package/components/YunDocsSidebar.vue +53 -13
  10. package/components/YunDropdownMenu.vue +1 -1
  11. package/components/YunFullscreenMenu.vue +0 -3
  12. package/components/YunFuseSearch.vue +403 -68
  13. package/components/YunLocalOutlineNav.vue +133 -0
  14. package/components/YunOutline.vue +10 -16
  15. package/components/YunPostCard.vue +22 -9
  16. package/components/YunPrologue.vue +115 -10
  17. package/components/YunSay.vue +3 -3
  18. package/components/YunSearchBtn.vue +3 -4
  19. package/components/YunSelect.vue +160 -47
  20. package/components/YunTooltip.vue +2 -2
  21. package/components/animation/LineBurstEffects.vue +44 -46
  22. package/components/animation/YunAnimLineDraw.vue +1 -1
  23. package/components/collection/YunCollectionMobile.vue +118 -0
  24. package/components/collection/YunCollectionSidebar.vue +97 -74
  25. package/components/config/YunToggleDark.vue +67 -30
  26. package/components/layout/YunLayoutLeft.vue +7 -2
  27. package/components/layout/YunLayoutRight.vue +22 -3
  28. package/components/layout/YunLayoutWrapper.vue +53 -5
  29. package/components/prologue/YunPrologueNavigation.vue +221 -0
  30. package/components/prologue/YunPrologueSquare.vue +72 -37
  31. package/components/site/YunSiteLinkItem.vue +6 -4
  32. package/components/theme/nimbo/YunNimboNavMenu.vue +1 -6
  33. package/components/theme/strato/YunStratoNavMenu.vue +4 -8
  34. package/components/ui/YunExcerptBottomGradient.vue +1 -1
  35. package/composables/collection.ts +16 -0
  36. package/composables/localMenu.ts +21 -0
  37. package/layouts/archives.vue +2 -2
  38. package/layouts/categories.vue +2 -2
  39. package/layouts/collection.vue +19 -23
  40. package/layouts/default.vue +2 -2
  41. package/layouts/docs.vue +27 -3
  42. package/layouts/home.vue +49 -25
  43. package/layouts/moments.vue +6 -1
  44. package/layouts/post.vue +23 -6
  45. package/layouts/tags.vue +2 -2
  46. package/locales/en.yml +8 -0
  47. package/locales/zh-CN.yml +8 -0
  48. package/node/config.ts +13 -0
  49. package/package.json +7 -8
  50. package/styles/common/button.scss +21 -0
  51. package/styles/common/markdown.scss +1 -0
  52. package/styles/common/nav.scss +54 -0
  53. package/styles/css-vars.scss +10 -0
  54. package/styles/global.scss +0 -2
  55. package/styles/index.ts +1 -0
  56. package/styles/layout/post.scss +30 -2
  57. package/types/index.d.ts +39 -2
  58. package/utils/sidebar.ts +9 -0
package/.npmignore ADDED
@@ -0,0 +1,8 @@
1
+ # dev / tooling files — themes publish their source, so only exclude non-runtime junk
2
+ .codebuddy
3
+ bump.config.ts
4
+ docs
5
+ tsconfig.json
6
+ *.tsbuildinfo
7
+ *.log
8
+ .DS_Store
package/App.vue CHANGED
@@ -2,13 +2,11 @@
2
2
  import { useHead } from '@unhead/vue'
3
3
  import { TooltipProvider } from 'reka-ui'
4
4
  import { useAppStore } from 'valaxy'
5
- import { computed, onMounted, watch } from 'vue'
5
+ import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
6
6
  import { useRoute } from 'vue-router'
7
7
  import { useThemeConfig } from './composables'
8
8
  import { useYunAppStore } from './stores'
9
9
 
10
- const isDev = import.meta.env.DEV
11
-
12
10
  const appStore = useAppStore()
13
11
 
14
12
  // Use a safe default for SSR; real themeColor is applied after mount
@@ -52,17 +50,22 @@ watch(
52
50
  { immediate: true },
53
51
  )
54
52
 
55
- onMounted(() => {
56
- // for mobile vh
53
+ function updateViewportHeight() {
57
54
  document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`)
55
+ }
56
+
57
+ onMounted(() => {
58
+ // The viewport can be zero when an iframe mounts inside a hidden panel.
59
+ updateViewportHeight()
60
+ window.addEventListener('resize', updateViewportHeight)
58
61
  app.showLoading = false
59
62
  })
63
+ onBeforeUnmount(() => window.removeEventListener('resize', updateViewportHeight))
60
64
  </script>
61
65
 
62
66
  <template>
63
67
  <TooltipProvider>
64
68
  <YunStratoApp v-if="yun.isStrato" />
65
- <ValaxyDebug v-if="isDev" />
66
69
 
67
70
  <YunPageHeaderGradient />
68
71
  <YunNavMenu />
@@ -119,4 +119,10 @@ onContentUpdated(() => {
119
119
  }
120
120
  }
121
121
  }
122
+
123
+ @media (width < 768px) {
124
+ .yun-main > .content > .yun-card:first-child {
125
+ border-radius: 0;
126
+ }
127
+ }
122
128
  </style>
@@ -1,9 +1,13 @@
1
1
  <script lang="ts" setup>
2
2
  import { useFrontmatter } from 'valaxy'
3
- import { computed } from 'vue'
3
+ import { computed, nextTick, useTemplateRef, watch } from 'vue'
4
4
  import { useI18n } from 'vue-i18n'
5
5
  import { useYunAppStore } from '../stores'
6
6
 
7
+ defineProps<{
8
+ mobileTitle?: string
9
+ }>()
10
+
7
11
  const fm = useFrontmatter()
8
12
  const { t } = useI18n()
9
13
  const yun = useYunAppStore()
@@ -13,17 +17,59 @@ const showToc = computed(() => {
13
17
  })
14
18
 
15
19
  const asideEnabled = computed(() => fm.value.aside !== false)
20
+ const closeButton = useTemplateRef<HTMLButtonElement>('closeButton')
21
+ let keepHeadingFocus = false
22
+
23
+ function closeAside(focusHeading = false) {
24
+ if (!yun.rightSidebar.isOpen)
25
+ return
26
+ keepHeadingFocus = focusHeading
27
+ yun.rightSidebar.toggle()
28
+ }
29
+
30
+ watch(() => yun.rightSidebar.isOpen, (open) => {
31
+ if (open) {
32
+ nextTick(() => closeButton.value?.focus({ preventScroll: true }))
33
+ }
34
+ else if (keepHeadingFocus) {
35
+ keepHeadingFocus = false
36
+ }
37
+ else {
38
+ nextTick(() => document.querySelector<HTMLElement>('[aria-controls="yun-page-outline"][aria-expanded="false"]')?.focus({ preventScroll: true }))
39
+ }
40
+ })
41
+
42
+ function onKeydown(event: KeyboardEvent) {
43
+ if (event.key === 'Escape' && yun.rightSidebar.isOpen) {
44
+ event.preventDefault()
45
+ closeAside()
46
+ }
47
+ }
16
48
  </script>
17
49
 
18
50
  <template>
19
51
  <aside
20
52
  v-if="asideEnabled"
53
+ id="yun-page-outline"
21
54
  flex="~ col"
22
55
  class="va-card yun-aside min-h-sm rounded-2"
23
56
  :class="{ open: yun.rightSidebar.isOpen }"
24
57
  text="center"
25
58
  overflow="auto"
59
+ @keydown="onKeydown"
26
60
  >
61
+ <div class="yun-aside-mobile-header">
62
+ <span>{{ mobileTitle || t('sidebar.toc') }}</span>
63
+ <button
64
+ ref="closeButton"
65
+ type="button"
66
+ class="yun-aside-close yun-icon-btn"
67
+ :aria-label="t('theme.closeOutline')"
68
+ @click="closeAside()"
69
+ >
70
+ <span i-ri-close-line aria-hidden="true" />
71
+ </button>
72
+ </div>
27
73
  <div class="w-full" flex="~ col" pb-2>
28
74
  <template v-if="showToc">
29
75
  <h2
@@ -32,7 +78,7 @@ const asideEnabled = computed(() => fm.value.aside !== false)
32
78
  >
33
79
  {{ t('sidebar.toc') }}
34
80
  </h2>
35
- <YunOutline />
81
+ <YunOutline @select="closeAside(true)" />
36
82
  </template>
37
83
 
38
84
  <div class="flex-grow" />
@@ -56,10 +102,14 @@ const asideEnabled = computed(() => fm.value.aside !== false)
56
102
  right: 0;
57
103
  bottom: 0;
58
104
  z-index: var(--yun-z-aside);
59
- width: 0;
105
+ width: min(360px, calc(100vw - 24px));
106
+ height: 100dvh;
107
+ max-height: 100dvh;
108
+ visibility: hidden;
60
109
  transform: translateX(100%);
61
- transition: all var(--va-transition-duration-fast) map.get($cubic-bezier, 'ease-in-out');
62
- max-height: calc(100vh - var(--yun-margin-top));
110
+ transition: transform var(--va-transition-duration-fast) map.get($cubic-bezier, 'ease-in-out'), visibility 0s linear var(--va-transition-duration-fast);
111
+ border-radius: 16px 0 0 16px;
112
+ box-shadow: -12px 0 40px rgb(0 0 0 / 0.12);
63
113
 
64
114
  // float panel
65
115
  &.float {
@@ -72,13 +122,39 @@ const asideEnabled = computed(() => fm.value.aside !== false)
72
122
  }
73
123
 
74
124
  &.show {
75
- width: 320px;
125
+ visibility: visible;
126
+ transform: translateX(0);
76
127
  }
77
128
 
78
129
  // Mobile/tablet: toggle open via JS
79
130
  &.open {
80
- width: 320px;
131
+ visibility: visible;
81
132
  transform: translateX(0);
133
+ transition: transform var(--va-transition-duration-fast) map.get($cubic-bezier, 'ease-in-out'), visibility 0s;
134
+ }
135
+ }
136
+
137
+ .yun-aside-mobile-header {
138
+ display: flex;
139
+ flex: none;
140
+ align-items: center;
141
+ justify-content: space-between;
142
+ min-height: 56px;
143
+ padding: 0 12px 0 20px;
144
+ border-bottom: 1px solid var(--yun-surface-line);
145
+ font-size: 16px;
146
+ font-weight: 600;
147
+ text-align: left;
148
+ }
149
+
150
+ .yun-aside-close {
151
+ color: var(--va-c-text);
152
+ }
153
+
154
+ @media (width < 1280px) {
155
+ .yun-aside > div > h2,
156
+ .yun-aside .outline-title {
157
+ display: none;
82
158
  }
83
159
  }
84
160
 
@@ -89,18 +165,32 @@ const asideEnabled = computed(() => fm.value.aside !== false)
89
165
  top: var(--yun-margin-top);
90
166
  z-index: auto;
91
167
  width: 320px;
168
+ height: auto;
92
169
  max-height: calc(100vh - var(--yun-margin-top));
170
+ visibility: visible;
93
171
  transform: translateX(0);
172
+ border-radius: 8px;
173
+ box-shadow: none;
94
174
 
95
175
  // On xl, sidebar toggle should not affect layout
96
176
  &.open {
97
177
  width: 320px;
98
178
  }
99
179
  }
180
+
181
+ .yun-aside-mobile-header {
182
+ display: none;
183
+ }
100
184
  }
101
185
 
102
186
  .toc-btn {
103
187
  color: var(--va-c-primary);
104
188
  z-index: var(--yun-z-toc-btn);
105
189
  }
190
+
191
+ @media (prefers-reduced-motion: reduce) {
192
+ .yun-aside {
193
+ transition: none;
194
+ }
195
+ }
106
196
  </style>
@@ -1,73 +1,120 @@
1
1
  <script lang="ts" setup>
2
2
  import { useBackToTop } from 'valaxy'
3
3
  import { computed } from 'vue'
4
+ import { useI18n } from 'vue-i18n'
4
5
 
5
6
  const { show, percentage, backToTop } = useBackToTop({ offset: 100 })
6
7
 
7
- const radius = 48
8
- const circumference = 2 * radius * Math.PI
8
+ const { t } = useI18n()
9
9
 
10
- const strokeOffset = computed(() => {
11
- // 周长
12
- const val = (1 - percentage.value) * circumference
13
- return val < 0 ? 0 : val
10
+ const progressAngle = computed(() => {
11
+ const progress = Number.isFinite(percentage.value) ? percentage.value : 0
12
+ return `${Math.min(1, Math.max(0, progress)) * 360}deg`
14
13
  })
15
14
  </script>
16
15
 
17
16
  <template>
18
- <a
19
- href="#"
20
- class="back-to-top yun-icon-btn bg-$va-c-bg-soft shadow-md"
17
+ <button
18
+ type="button"
19
+ class="back-to-top yun-icon-btn"
20
+ :aria-label="t('theme.backToTop')"
21
+ :title="t('theme.backToTop')"
22
+ :tabindex="show ? 0 : -1"
23
+ :aria-hidden="!show"
21
24
  :class="show && 'show'"
25
+ :style="{ '--yun-back-top-progress': progressAngle }"
22
26
  @click="backToTop"
23
27
  >
24
- <div class="size-8" i-ri-arrow-up-s-line />
25
- <svg class="progress-circle-container" viewBox="0 0 100 100">
26
- <circle
27
- :stroke-dasharray="`${circumference} ${circumference}`"
28
- :stroke-dashoffset="strokeOffset"
29
- stroke="currentColor" stroke-width="2" stroke-linecap="round"
30
- class="progress-circle" cx="50" cy="50" :r="radius" fill="none"
31
- />
32
- </svg>
33
- </a>
28
+ <div class="back-to-top-arrow" i-ri-arrow-up-s-line aria-hidden="true" />
29
+ </button>
34
30
  </template>
35
31
 
36
32
  <style lang="scss">
37
- @use "sass:map";
33
+ .back-to-top.yun-icon-btn {
34
+ --yun-back-top-bg: var(--va-c-bg-soft);
35
+ --yun-back-top-progress-opacity: 0.06;
38
36
 
39
- .back-to-top {
40
37
  position: fixed;
41
- right: -1.5rem;
42
- bottom: 1rem;
38
+ right: max(1rem, env(safe-area-inset-right));
39
+ bottom: max(1rem, env(safe-area-inset-bottom));
43
40
  z-index: var(--yun-z-go-up-btn);
44
- opacity: 0;
45
- pointer-events: none;
41
+ width: 3rem;
42
+ height: 3rem;
43
+ padding: 0;
44
+ box-sizing: border-box;
45
+ isolation: isolate;
46
+ overflow: visible;
47
+ border: 0;
48
+ border-radius: 50%;
46
49
  color: var(--va-c-primary);
47
- transform: translateX(0) rotate(270deg);
50
+ background: transparent;
48
51
 
49
- // override yun-icon-btn transition
50
- transition: all var(--va-transition-duration), opacity var(--va-transition-duration-fast) !important;
52
+ // Keep the progress tint inside the face, underneath the arrow.
53
+ &::before {
54
+ content: '';
55
+ position: absolute;
56
+ inset: 0;
57
+ z-index: 0;
58
+ border: 1px solid rgb(var(--va-c-primary-rgb), 0.08);
59
+ border-radius: inherit;
60
+ background: var(--yun-back-top-bg);
61
+ box-shadow: 0 2px 8px rgb(0 0 0 / 0.08);
62
+ transition: background-color 180ms ease;
63
+ pointer-events: none;
64
+ }
65
+
66
+ opacity: 0;
67
+ visibility: hidden;
68
+ pointer-events: none;
69
+ transition: opacity 180ms ease, visibility 180ms ease;
51
70
 
52
71
  &.show {
53
- transform: translateX(-40px) rotate(360deg);
54
72
  opacity: 1;
55
- pointer-events: fill;
73
+ visibility: visible;
74
+ pointer-events: auto;
56
75
  }
57
76
 
58
- .icon {
59
- width: 2.5rem;
60
- height: 2.5rem;
77
+ @media (hover: hover) {
78
+ &.show:hover {
79
+ --yun-back-top-bg: color-mix(in srgb, var(--va-c-bg-soft) 94%, var(--va-c-primary));
80
+ }
61
81
  }
62
- }
63
82
 
64
- .progress-circle {
65
- transition: var(--va-transition-duration) stroke-dashoffset;
66
- transform: rotate(-90deg);
67
- transform-origin: 50% 50%;
83
+ &.show:active {
84
+ --yun-back-top-bg: color-mix(in srgb, var(--va-c-bg-soft) 88%, var(--va-c-primary));
85
+ }
68
86
 
69
- &-container {
87
+ .back-to-top-arrow {
88
+ position: relative;
89
+ z-index: 1;
90
+ width: 24px;
91
+ height: 24px;
92
+ }
93
+
94
+ &::after {
95
+ content: '';
70
96
  position: absolute;
97
+ inset: 1px;
98
+ z-index: 0;
99
+ border-radius: inherit;
100
+ background: conic-gradient(
101
+ from 0deg,
102
+ rgb(var(--va-c-primary-rgb), var(--yun-back-top-progress-opacity)) 0deg var(--yun-back-top-progress, 0deg),
103
+ transparent var(--yun-back-top-progress, 0deg) 360deg
104
+ );
105
+ pointer-events: none;
106
+ }
107
+
108
+ }
109
+
110
+ html.dark .back-to-top.yun-icon-btn {
111
+ --yun-back-top-progress-opacity: 0.1;
112
+ }
113
+
114
+ @media (prefers-reduced-motion: reduce) {
115
+ .back-to-top.yun-icon-btn,
116
+ .back-to-top.yun-icon-btn::before {
117
+ transition: none;
71
118
  }
72
119
  }
73
120
  </style>
@@ -26,7 +26,7 @@ const bannerStyles = computed<CSSProperties>(() => {
26
26
  'justify-content': 'space-between',
27
27
  }
28
28
  if (yun.isStrato)
29
- styles.borderBottom = `1px solid var(--banner-line-color)`
29
+ styles.borderBottom = `1px solid var(--yun-banner-divider-color, var(--banner-line-color))`
30
30
 
31
31
  return styles
32
32
  })
@@ -58,7 +58,7 @@ onMounted(async () => {
58
58
  </script>
59
59
 
60
60
  <template>
61
- <div id="yun-banner" border="b-1px b-solid b-$banner-line-color" :style="bannerStyles">
61
+ <div id="yun-banner" border="b-1px b-solid" :style="bannerStyles">
62
62
  <div class="banner-line-container">
63
63
  <div
64
64
  class="banner-line vertical-line-top"
@@ -94,6 +94,10 @@ onMounted(async () => {
94
94
  @use "../styles/widgets/banner.scss" as *;
95
95
  @use "../styles/modules/prologue.scss" as *;
96
96
 
97
+ #yun-banner {
98
+ border-bottom-color: var(--yun-banner-divider-color, var(--banner-line-color));
99
+ }
100
+
97
101
  :root {
98
102
  // banner
99
103
  --banner-line-color: black;
@@ -4,7 +4,7 @@ defineProps<{ cover?: string }>()
4
4
 
5
5
  <template>
6
6
  <div
7
- class="yun-card flex-center rounded-2 overflow-hidden" flex="col"
7
+ class="yun-card flex-center overflow-hidden" flex="col"
8
8
  min-h="100px"
9
9
  bg="$va-c-bg-light"
10
10
  >
@@ -23,3 +23,9 @@ defineProps<{ cover?: string }>()
23
23
  </div>
24
24
  </div>
25
25
  </template>
26
+
27
+ <style scoped>
28
+ .yun-card {
29
+ border-radius: var(--va-card-border-radius, 0.5rem);
30
+ }
31
+ </style>
@@ -16,7 +16,7 @@ const activeComment = ref(commentSystems.value[0])
16
16
  <YunCard w="full" p="4" class="comment yun-comment sm:p-6 lg:px-12 xl:px-16">
17
17
  <ClientOnly>
18
18
  <div v-if="commentSystems.length > 1" class="flex justify-end w-full mb-2">
19
- <YunSelect v-model="activeComment" :options="commentSystems" />
19
+ <YunSelect v-model="activeComment" :options="commentSystems" compact />
20
20
  </div>
21
21
  <YunWaline v-if="activeComment === 'waline'" />
22
22
  <YunTwikoo v-if="activeComment === 'twikoo'" />
@@ -1,55 +1,71 @@
1
1
  <script setup lang="ts">
2
- import { useEventListener } from '@vueuse/core'
3
- import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
2
+ import { useEventListener, useMediaQuery } from '@vueuse/core'
3
+ import { computed, nextTick, useTemplateRef, watch } from 'vue'
4
4
  import { useI18n } from 'vue-i18n'
5
5
  import { useRoute } from 'vue-router'
6
6
  import { useThemeConfig } from '../composables'
7
- import { resolveYunDocsSidebar } from '../utils/sidebar'
7
+ import { hasYunDocsSidebarNavigation, resolveYunDocsSidebar } from '../utils/sidebar'
8
+
9
+ withDefaults(defineProps<{
10
+ floatingTrigger?: boolean
11
+ }>(), {
12
+ floatingTrigger: true,
13
+ })
14
+
15
+ const isOpen = defineModel<boolean>('open', { default: false })
8
16
 
9
17
  const route = useRoute()
10
18
  const themeConfig = useThemeConfig()
11
19
  const { t } = useI18n()
12
20
 
13
21
  const sidebarItems = computed(() => resolveYunDocsSidebar(themeConfig.value.sidebar, route.path))
14
- const isOpen = shallowRef(false)
22
+ const hasNavigation = computed(() => hasYunDocsSidebarNavigation(sidebarItems.value, route.path))
23
+ const isDesktop = useMediaQuery('(min-width: 1024px)', { ssrWidth: 1280 })
24
+ const closeButton = useTemplateRef<HTMLButtonElement>('closeButton')
15
25
  const triggerRef = useTemplateRef<HTMLButtonElement>('trigger')
16
26
 
27
+ watch(isOpen, (open) => {
28
+ if (open)
29
+ nextTick(() => closeButton.value?.focus({ preventScroll: true }))
30
+ })
31
+
17
32
  watch(
18
33
  () => route.fullPath,
19
34
  () => isOpen.value = false,
20
35
  )
36
+ watch(isDesktop, (desktop) => {
37
+ if (desktop)
38
+ isOpen.value = false
39
+ })
21
40
 
22
41
  useEventListener('keydown', (event: KeyboardEvent) => {
23
42
  if (event.key === 'Escape' && isOpen.value)
24
43
  close(true)
25
44
  })
26
45
 
27
- function toggle() {
28
- isOpen.value = !isOpen.value
29
- }
30
-
31
46
  function close(restoreFocus = false) {
32
47
  isOpen.value = false
33
48
  if (restoreFocus)
34
- nextTick(() => triggerRef.value?.focus())
49
+ nextTick(() => (triggerRef.value || document.querySelector<HTMLElement>('[aria-controls="yun-docs-sidebar"][aria-expanded="false"]'))?.focus({ preventScroll: true }))
35
50
  }
36
51
  </script>
37
52
 
38
53
  <template>
39
- <template v-if="sidebarItems.length">
54
+ <template v-if="hasNavigation">
40
55
  <button
56
+ v-if="floatingTrigger"
41
57
  ref="trigger"
42
58
  type="button"
43
59
  class="yun-docs-sidebar-trigger yun-icon-btn lg:hidden"
44
60
  :aria-label="t('theme.docsSidebar')"
45
61
  :aria-expanded="isOpen"
46
62
  aria-controls="yun-docs-sidebar"
47
- @click="toggle"
63
+ @click="isOpen = !isOpen"
48
64
  >
49
- <span i-ri-menu-2-line aria-hidden="true" />
65
+ <span i-ri-side-bar-line aria-hidden="true" />
50
66
  </button>
51
67
 
52
- <YunOverlay :show="isOpen" @click="close()" />
68
+ <YunOverlay :show="isOpen" @click="close(true)" />
53
69
 
54
70
  <aside
55
71
  id="yun-docs-sidebar-desktop"
@@ -65,6 +81,18 @@ function close(restoreFocus = false) {
65
81
  id="yun-docs-sidebar"
66
82
  class="va-card yun-docs-sidebar yun-docs-sidebar-mobile"
67
83
  >
84
+ <div class="yun-docs-sidebar-mobile-header">
85
+ <span>{{ t('theme.docsSidebar') }}</span>
86
+ <button
87
+ ref="closeButton"
88
+ type="button"
89
+ class="yun-icon-btn"
90
+ :aria-label="t('theme.closeMenu')"
91
+ @click="close(true)"
92
+ >
93
+ <span i-ri-close-line aria-hidden="true" />
94
+ </button>
95
+ </div>
68
96
  <nav :aria-label="t('theme.docsSidebar')">
69
97
  <YunDocsSidebarNav :items="sidebarItems" @navigate="close()" />
70
98
  </nav>
@@ -92,6 +120,18 @@ function close(restoreFocus = false) {
92
120
  border-radius: 0 0.5rem 0.5rem 0;
93
121
  }
94
122
 
123
+ .yun-docs-sidebar-mobile-header {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: space-between;
127
+ min-height: 56px;
128
+ margin: -1rem -1rem 1rem;
129
+ padding: 0 12px 0 20px;
130
+ border-bottom: 1px solid var(--yun-surface-line);
131
+ font-size: 16px;
132
+ font-weight: 600;
133
+ }
134
+
95
135
  .yun-docs-sidebar-trigger {
96
136
  position: fixed;
97
137
  bottom: 4.75rem;
@@ -44,7 +44,7 @@ defineSlots<{
44
44
  <DropdownMenuTrigger as-child>
45
45
  <slot name="trigger" />
46
46
  </DropdownMenuTrigger>
47
- <DropdownMenuPortal>
47
+ <DropdownMenuPortal to="#valaxy-teleports">
48
48
  <DropdownMenuContent class="yun-dropdown-menu-content" :side-offset="sideOffset" :align="align">
49
49
  <template v-for="(item, index) in items" :key="`${item.key}-${index}`">
50
50
  <DropdownMenuSeparator v-if="item.key === 'separator'" class="yun-dropdown-menu-separator" />
@@ -1,16 +1,13 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
3
2
  import { useYunAppStore } from '../stores'
4
3
 
5
4
  const yunApp = useYunAppStore()
6
- const fullscreenMenuRef = ref<HTMLElement>()
7
5
  </script>
8
6
 
9
7
  <template>
10
8
  <Transition name="slide-left">
11
9
  <div
12
10
  v-if="yunApp.fullscreenMenu.isOpen"
13
- ref="fullscreenMenuRef"
14
11
  p="t-12 md:t-20"
15
12
  class="yun-fullscreen-menu fixed left-0 right-0 bottom-0 top-0 bg-$va-c-bg-soft z-$yun-z-fullscreen-menu overflow-auto max-w-md shadow-2xl"
16
13
  >