valaxy-theme-press 0.26.13 → 0.28.0-beta.1

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.
@@ -0,0 +1,42 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{
3
+ ariaLabel?: string
4
+ ariaKeyshortcuts?: string
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <button type="button" class="PressNavBarAskAiButton" :aria-label="ariaLabel || 'Ask AI'" :aria-keyshortcuts="ariaKeyshortcuts">
10
+ <span i-ri-sparkling-line aria-hidden="true" />
11
+ </button>
12
+ </template>
13
+
14
+ <style>
15
+ /* stylelint-disable selector-class-pattern */
16
+ .PressNavBarAskAiButton {
17
+ display: flex;
18
+ align-items: center;
19
+ height: var(--pr-nav-height);
20
+ padding: 8px 14px;
21
+ font-size: 20px;
22
+ color: var(--va-c-text-2);
23
+ cursor: pointer;
24
+ background: transparent;
25
+ border: none;
26
+ }
27
+
28
+ @media (width >= 768px) {
29
+ .PressNavBarAskAiButton {
30
+ height: auto;
31
+ padding: 11.5px;
32
+ transition: color 0.3s ease;
33
+ background-color: var(--va-c-bg-alt);
34
+ border-radius: 8px;
35
+ font-size: 15px;
36
+ }
37
+
38
+ .PressNavBarAskAiButton:hover {
39
+ color: var(--va-c-primary);
40
+ }
41
+ }
42
+ </style>
@@ -1,25 +1,146 @@
1
1
  <script lang="ts" setup>
2
+ import { onKeyStroke } from '@vueuse/core'
2
3
  import { useSiteConfig } from 'valaxy'
3
- import { computed, defineAsyncComponent } from 'vue'
4
+ import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
5
+ import PressNavBarAskAiButton from './PressNavBarAskAiButton.vue'
6
+ import PressNavBarSearchButton from './PressNavBarSearchButton.vue'
4
7
 
5
- // ref vitepress search box
6
8
  const siteConfig = useSiteConfig()
9
+
7
10
  const isAlgolia = computed(() => siteConfig.value.search.provider === 'algolia')
8
11
  const isFuse = computed(() => siteConfig.value.search.provider === 'fuse')
9
12
 
10
- const PressAlgoliaSearch = isAlgolia.value && defineAsyncComponent({
11
- loader: () => import('./PressAlgoliaSearch.vue'),
12
- errorComponent: import('./PressFuseSearchModal.vue'),
13
+ // Whether to show the Ask AI button (requires askAi config in addon-algolia)
14
+ const showAskAi = ref(false)
15
+
16
+ if (isAlgolia.value) {
17
+ import('valaxy-addon-algolia').then(({ useAddonAlgoliaConfig }) => {
18
+ const algoliaConfig = useAddonAlgoliaConfig()
19
+ const askAi = algoliaConfig.value?.options?.askAi
20
+ showAskAi.value = !!askAi
21
+ }).catch(() => {})
22
+ }
23
+
24
+ const PressAlgoliaSearch = isAlgolia.value
25
+ ? defineAsyncComponent(() => import('./PressAlgoliaSearch.vue'))
26
+ : () => null
27
+
28
+ const PressFuseSearch = isFuse.value
29
+ ? defineAsyncComponent(() => import('./PressFuseSearch.vue'))
30
+ : () => null
31
+
32
+ // #region Algolia lazy loading
33
+
34
+ type OpenTarget = 'search' | 'askAi' | 'toggleAskAi'
35
+ interface OpenRequest { target: OpenTarget, nonce: number }
36
+ const openRequest = ref<OpenRequest | null>(null)
37
+ let openNonce = 0
38
+
39
+ const loaded = ref(false)
40
+ const actuallyLoaded = ref(false)
41
+
42
+ // Preconnect to Algolia DSN on idle
43
+ onMounted(async () => {
44
+ if (!isAlgolia.value)
45
+ return
46
+
47
+ const id = 'PressAlgoliaPreconnect'
48
+ if (document.getElementById(id))
49
+ return
50
+
51
+ // Dynamically import addon config to get appId for preconnect
52
+ try {
53
+ const { useAddonAlgoliaConfig } = await import('valaxy-addon-algolia')
54
+ const algoliaConfig = useAddonAlgoliaConfig()
55
+ const appId = algoliaConfig.value?.options?.appId
56
+
57
+ if (!appId)
58
+ return
59
+
60
+ const rIC = window.requestIdleCallback || setTimeout
61
+ rIC(() => {
62
+ const preconnect = document.createElement('link')
63
+ preconnect.id = id
64
+ preconnect.rel = 'preconnect'
65
+ preconnect.href = `https://${appId}-dsn.algolia.net`
66
+ preconnect.crossOrigin = ''
67
+ document.head.appendChild(preconnect)
68
+ })
69
+ }
70
+ catch {
71
+ // valaxy-addon-algolia not installed, skip preconnect
72
+ }
13
73
  })
74
+
75
+ // Keyboard shortcuts for Algolia
76
+ if (isAlgolia.value) {
77
+ onKeyStroke('k', (event) => {
78
+ if (event.ctrlKey || event.metaKey) {
79
+ event.preventDefault()
80
+ loadAndOpen('search')
81
+ }
82
+ })
83
+
84
+ onKeyStroke('i', (event) => {
85
+ if ((event.ctrlKey || event.metaKey) && showAskAi.value) {
86
+ event.preventDefault()
87
+ loadAndOpen('askAi')
88
+ }
89
+ })
90
+
91
+ onKeyStroke('/', (event) => {
92
+ if (!isEditingContent(event)) {
93
+ event.preventDefault()
94
+ loadAndOpen('search')
95
+ }
96
+ })
97
+ }
98
+
99
+ function loadAndOpen(target: OpenTarget) {
100
+ if (!loaded.value)
101
+ loaded.value = true
102
+
103
+ openRequest.value = { target, nonce: ++openNonce }
104
+ }
105
+
106
+ // #endregion
107
+
108
+ function isEditingContent(event: KeyboardEvent): boolean {
109
+ const element = event.target as HTMLElement
110
+ const tagName = element.tagName
111
+
112
+ return (
113
+ element.isContentEditable
114
+ || tagName === 'INPUT'
115
+ || tagName === 'SELECT'
116
+ || tagName === 'TEXTAREA'
117
+ )
118
+ }
14
119
  </script>
15
120
 
16
121
  <template>
17
122
  <div v-if="siteConfig.search.enable" class="VPNavBarSearch">
18
- <ClientOnly>
19
- <PressAlgoliaSearch v-if="isAlgolia" />
20
- </ClientOnly>
21
-
22
- <PressFuseSearch v-if="isFuse" />
123
+ <template v-if="isAlgolia">
124
+ <PressNavBarSearchButton
125
+ aria-keyshortcuts="/ control+k meta+k"
126
+ @click="loadAndOpen('search')"
127
+ />
128
+ <PressNavBarAskAiButton
129
+ v-if="showAskAi"
130
+ aria-keyshortcuts="control+i meta+i"
131
+ @click="actuallyLoaded ? loadAndOpen('toggleAskAi') : loadAndOpen('askAi')"
132
+ />
133
+ <ClientOnly>
134
+ <PressAlgoliaSearch
135
+ v-if="loaded"
136
+ :open-request="openRequest"
137
+ @vue:before-mount="actuallyLoaded = true"
138
+ />
139
+ </ClientOnly>
140
+ </template>
141
+ <template v-else-if="isFuse">
142
+ <PressFuseSearch />
143
+ </template>
23
144
  </div>
24
145
  </template>
25
146
 
@@ -32,6 +153,7 @@ const PressAlgoliaSearch = isAlgolia.value && defineAsyncComponent({
32
153
 
33
154
  @media (width >= 768px) {
34
155
  .VPNavBarSearch {
156
+ gap: 8px;
35
157
  flex-grow: 1;
36
158
  padding-left: 24px;
37
159
  }
@@ -0,0 +1,158 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{
3
+ text?: string
4
+ ariaLabel?: string
5
+ ariaKeyshortcuts?: string
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <button
11
+ type="button"
12
+ class="PressSearchButton"
13
+ :aria-label="ariaLabel || 'Search'"
14
+ :aria-keyshortcuts="ariaKeyshortcuts"
15
+ >
16
+ <span class="PressSearchButton-icon">
17
+ <svg width="20" height="20" viewBox="0 0 20 20">
18
+ <path
19
+ d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
20
+ stroke="currentColor"
21
+ fill="none"
22
+ fill-rule="evenodd"
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ />
26
+ </svg>
27
+ <span class="PressSearchButton-text">{{ text || 'Search' }}</span>
28
+ </span>
29
+ <span class="PressSearchButton-keys" aria-hidden="true">
30
+ <kbd class="key-cmd">&#x2318;</kbd>
31
+ <kbd class="key-ctrl">Ctrl</kbd>
32
+ <kbd>K</kbd>
33
+ </span>
34
+ </button>
35
+ </template>
36
+
37
+ <style>
38
+ .PressSearchButton {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 8px;
42
+ margin: 0;
43
+ padding: 8px 14px;
44
+ height: var(--va-nav-height, 55px);
45
+ border: none;
46
+ background: transparent;
47
+ font-size: 20px;
48
+ cursor: pointer;
49
+ transition: border-color 0.25s;
50
+ }
51
+
52
+ .PressSearchButton:hover {
53
+ background: transparent;
54
+ }
55
+
56
+ .PressSearchButton:focus {
57
+ outline: 1px dotted;
58
+ outline: 5px auto -webkit-focus-ring-color;
59
+ }
60
+
61
+ .PressSearchButton:focus:not(:focus-visible) {
62
+ outline: none !important;
63
+ }
64
+
65
+ @media (width >= 768px) {
66
+ .PressSearchButton {
67
+ height: auto;
68
+ padding: 8px 12px;
69
+ border: 1px solid transparent;
70
+ border-radius: 8px;
71
+ background-color: var(--va-c-bg-alt);
72
+ font-size: 14px;
73
+ line-height: 1;
74
+ color: var(--va-c-text-2);
75
+ }
76
+
77
+ .PressSearchButton:hover {
78
+ border-color: var(--va-c-primary);
79
+ background: var(--va-c-bg-alt);
80
+ }
81
+ }
82
+
83
+ .PressSearchButton-icon {
84
+ display: flex;
85
+ align-items: center;
86
+ }
87
+
88
+ .PressSearchButton-icon svg {
89
+ position: relative;
90
+ width: 16px;
91
+ height: 16px;
92
+ color: var(--va-c-text-1);
93
+ fill: currentcolor;
94
+ transition: color 0.5s;
95
+ }
96
+
97
+ .PressSearchButton:hover .PressSearchButton-icon svg {
98
+ color: var(--va-c-text-1);
99
+ }
100
+
101
+ @media (width >= 768px) {
102
+ .PressSearchButton-icon svg {
103
+ top: 1px;
104
+ margin-right: 8px;
105
+ width: 14px;
106
+ height: 14px;
107
+ color: var(--va-c-text-2);
108
+ }
109
+ }
110
+
111
+ .PressSearchButton-text {
112
+ display: none;
113
+ font-size: 13px;
114
+ font-weight: 500;
115
+ color: var(--va-c-text-2);
116
+ transition: color 0.5s;
117
+ }
118
+
119
+ .PressSearchButton:hover .PressSearchButton-text {
120
+ color: var(--va-c-text-1);
121
+ }
122
+
123
+ @media (width >= 768px) {
124
+ .PressSearchButton-text {
125
+ display: inline;
126
+ }
127
+ }
128
+
129
+ .PressSearchButton-keys {
130
+ direction: ltr;
131
+ display: none;
132
+ min-width: auto;
133
+ }
134
+
135
+ :root.mac .key-ctrl,
136
+ :root:not(.mac) .key-cmd {
137
+ display: none;
138
+ }
139
+
140
+ @media (width >= 768px) {
141
+ .PressSearchButton-keys {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 4px;
145
+ padding: 4px 6px;
146
+ border: 1px solid var(--va-c-divider);
147
+ border-radius: 4px;
148
+ }
149
+ }
150
+
151
+ .PressSearchButton-keys kbd {
152
+ font-family: var(--va-font-sans);
153
+ font-size: 12px;
154
+ font-weight: 500;
155
+ line-height: 1;
156
+ color: var(--docsearch-muted-color, var(--va-c-text-2));
157
+ }
158
+ </style>
@@ -0,0 +1,274 @@
1
+ <script lang="ts" setup>
2
+ import {
3
+ DropdownMenuContent,
4
+ DropdownMenuItem,
5
+ DropdownMenuPortal,
6
+ DropdownMenuRoot,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from 'reka-ui'
10
+ import { useCopyMarkdown, useFrontmatter, useFullUrl, useValaxyI18n } from 'valaxy'
11
+ import { computed, ref } from 'vue'
12
+ import { useI18n } from 'vue-i18n'
13
+ import { useEditLink } from '../composables'
14
+
15
+ const { copy, copied, loading, available, mdUrl } = useCopyMarkdown()
16
+ const { t } = useI18n()
17
+ const editLink = useEditLink()
18
+ const frontmatter = useFrontmatter()
19
+ const { $tO } = useValaxyI18n()
20
+ const fullUrl = useFullUrl()
21
+
22
+ const linkCopied = ref(false)
23
+
24
+ interface MenuItem {
25
+ key: string
26
+ label: string
27
+ icon?: string
28
+ disabled?: boolean
29
+ external?: boolean
30
+ }
31
+
32
+ const menuItems = computed<MenuItem[]>(() => {
33
+ const items: MenuItem[] = [
34
+ {
35
+ key: 'copy-markdown',
36
+ label: copied.value ? t('post.copied_markdown', 'Copied!') : t('post.copy_markdown', 'Copy Markdown'),
37
+ icon: copied.value ? 'i-ri-check-line text-green-500' : 'i-ri-file-copy-line',
38
+ disabled: loading.value,
39
+ },
40
+ {
41
+ key: 'copy-link',
42
+ label: linkCopied.value ? t('post.copied_markdown', 'Copied!') : t('post.copy_markdown_link', 'Copy Markdown Link'),
43
+ icon: linkCopied.value ? 'i-ri-check-line text-green-500' : 'i-ri-link',
44
+ },
45
+ {
46
+ key: 'view-markdown',
47
+ label: t('post.view_as_markdown', 'View as Markdown'),
48
+ icon: 'i-ri-file-text-line',
49
+ external: true,
50
+ },
51
+ { key: 'separator', label: '' },
52
+ {
53
+ key: 'open-chatgpt',
54
+ label: t('post.open_in_chatgpt', 'Open in ChatGPT'),
55
+ icon: 'i-simple-icons-openai',
56
+ external: true,
57
+ },
58
+ {
59
+ key: 'open-claude',
60
+ label: t('post.open_in_claude', 'Open in Claude'),
61
+ icon: 'i-simple-icons-claude',
62
+ external: true,
63
+ },
64
+ ]
65
+
66
+ if (editLink.value.url) {
67
+ items.push(
68
+ { key: 'separator-2', label: '' },
69
+ {
70
+ key: 'edit-link',
71
+ label: editLink.value.text || t('post.open_in_github', 'Open in GitHub'),
72
+ icon: 'i-ri-github-line',
73
+ external: true,
74
+ },
75
+ )
76
+ }
77
+
78
+ return items
79
+ })
80
+
81
+ async function copyMarkdownLink() {
82
+ try {
83
+ const title = $tO(frontmatter.value.title) || 'Untitled'
84
+ const link = `[${title}](${fullUrl.value})`
85
+ await navigator.clipboard.writeText(link)
86
+ linkCopied.value = true
87
+ setTimeout(() => {
88
+ linkCopied.value = false
89
+ }, 2000)
90
+ }
91
+ catch (err) {
92
+ console.error('[valaxy] Failed to copy markdown link:', err)
93
+ }
94
+ }
95
+
96
+ function buildAIUrl(provider: 'chatgpt' | 'claude') {
97
+ const rawMdUrl = new URL(mdUrl.value, fullUrl.value).href
98
+ const prompt = `Read ${rawMdUrl} so I can ask questions about it.`
99
+ if (provider === 'chatgpt')
100
+ return `https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`
101
+ return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
102
+ }
103
+
104
+ function onSelect(key: string) {
105
+ switch (key) {
106
+ case 'copy-markdown':
107
+ copy()
108
+ break
109
+ case 'copy-link':
110
+ copyMarkdownLink()
111
+ break
112
+ case 'view-markdown':
113
+ window.open(mdUrl.value, '_blank')
114
+ break
115
+ case 'open-chatgpt':
116
+ window.open(buildAIUrl('chatgpt'), '_blank')
117
+ break
118
+ case 'open-claude':
119
+ window.open(buildAIUrl('claude'), '_blank')
120
+ break
121
+ case 'edit-link':
122
+ if (editLink.value.url)
123
+ window.open(editLink.value.url, '_blank')
124
+ break
125
+ }
126
+ }
127
+ </script>
128
+
129
+ <template>
130
+ <div v-if="available" class="press-post-actions">
131
+ <button
132
+ class="press-post-actions-main"
133
+ :disabled="loading"
134
+ :aria-label="t('post.copy_markdown', 'Copy Markdown')"
135
+ @click="copy"
136
+ >
137
+ <div v-if="copied" i-ri-check-line class="text-green-500" />
138
+ <div v-else i-ri-file-copy-line />
139
+ <span>{{ copied ? t('post.copied_markdown', 'Copied!') : t('post.copy_page', 'Copy page') }}</span>
140
+ </button>
141
+ <DropdownMenuRoot>
142
+ <DropdownMenuTrigger as-child>
143
+ <button class="press-post-actions-trigger" :aria-label="t('menu.title', 'More actions')">
144
+ <div i-ri-arrow-down-s-line />
145
+ </button>
146
+ </DropdownMenuTrigger>
147
+ <DropdownMenuPortal>
148
+ <DropdownMenuContent class="press-dropdown-menu-content" :side-offset="4" align="end">
149
+ <template v-for="(item, index) in menuItems" :key="`${item.key}-${index}`">
150
+ <DropdownMenuSeparator v-if="item.key.startsWith('separator')" class="press-dropdown-menu-separator" />
151
+ <DropdownMenuItem v-else class="press-dropdown-menu-item" :disabled="item.disabled" @select="onSelect(item.key)">
152
+ <div v-if="item.icon" :class="item.icon" />
153
+ <span flex-1>{{ item.label }}</span>
154
+ <div v-if="item.external" i-ri-external-link-line class="press-dropdown-menu-external" />
155
+ </DropdownMenuItem>
156
+ </template>
157
+ </DropdownMenuContent>
158
+ </DropdownMenuPortal>
159
+ </DropdownMenuRoot>
160
+ </div>
161
+ </template>
162
+
163
+ <style>
164
+ .press-post-actions {
165
+ display: inline-flex;
166
+ align-items: stretch;
167
+ border: 1px solid var(--va-c-divider);
168
+ border-radius: 6px;
169
+ overflow: hidden;
170
+ flex-shrink: 0;
171
+ }
172
+
173
+ .press-post-actions-main {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 4px;
177
+ cursor: pointer;
178
+ border: none;
179
+ background: var(--va-c-bg-alt);
180
+ color: var(--va-c-text-2);
181
+ padding: 4px 10px;
182
+ font-size: 12px;
183
+ line-height: 1;
184
+ transition: color 0.2s, background 0.2s;
185
+ white-space: nowrap;
186
+ }
187
+
188
+ .press-post-actions-main:hover {
189
+ color: var(--va-c-primary);
190
+ background: var(--va-c-bg-soft);
191
+ }
192
+
193
+ .press-post-actions-main:disabled {
194
+ opacity: 0.5;
195
+ cursor: not-allowed;
196
+ }
197
+
198
+ .press-post-actions-trigger {
199
+ display: inline-flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ cursor: pointer;
203
+ border: none;
204
+ border-left: 1px solid var(--va-c-divider);
205
+ background: var(--va-c-bg-alt);
206
+ color: var(--va-c-text-3);
207
+ padding: 4px 6px;
208
+ font-size: 14px;
209
+ transition: color 0.2s, background 0.2s;
210
+ }
211
+
212
+ .press-post-actions-trigger:hover {
213
+ color: var(--va-c-primary);
214
+ background: var(--va-c-bg-soft);
215
+ }
216
+
217
+ .press-dropdown-menu-content {
218
+ min-width: 200px;
219
+ background: var(--va-c-bg);
220
+ border: 1px solid var(--va-c-divider);
221
+ border-radius: 8px;
222
+ padding: 4px;
223
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
224
+ z-index: 100;
225
+ animation: press-dropdown-fade-in 0.15s ease;
226
+ }
227
+
228
+ @keyframes press-dropdown-fade-in {
229
+ from {
230
+ opacity: 0;
231
+ transform: translateY(-4px);
232
+ }
233
+ to {
234
+ opacity: 1;
235
+ transform: translateY(0);
236
+ }
237
+ }
238
+
239
+ .press-dropdown-menu-item {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 8px;
243
+ padding: 6px 10px;
244
+ border-radius: 4px;
245
+ cursor: pointer;
246
+ font-size: 13px;
247
+ color: var(--va-c-text-2);
248
+ transition: background 0.15s, color 0.15s;
249
+ outline: none;
250
+ }
251
+
252
+ .press-dropdown-menu-item:hover,
253
+ .press-dropdown-menu-item[data-highlighted] {
254
+ background: var(--va-c-bg-soft);
255
+ color: var(--va-c-text);
256
+ }
257
+
258
+ .press-dropdown-menu-item[data-disabled] {
259
+ opacity: 0.5;
260
+ cursor: not-allowed;
261
+ }
262
+
263
+ .press-dropdown-menu-separator {
264
+ height: 1px;
265
+ background: var(--va-c-divider);
266
+ margin: 4px 8px;
267
+ }
268
+
269
+ .press-dropdown-menu-external {
270
+ font-size: 11px;
271
+ color: var(--va-c-text-3);
272
+ opacity: 0.6;
273
+ }
274
+ </style>
@@ -12,13 +12,16 @@ const props = withDefaults(defineProps<{
12
12
  })
13
13
 
14
14
  const site = useSiteStore()
15
- const posts = computed(() => props.posts || site.postList)
15
+ const posts = computed(() => {
16
+ const list = props.posts || site.postList
17
+ return list.filter(p => p.path && !p.path.endsWith('/'))
18
+ })
16
19
  </script>
17
20
 
18
21
  <template>
19
22
  <ul class="divide-y divide-gray-200">
20
23
  <TransitionGroup name="fade">
21
- <li v-for="post, i in posts" :key="i" class="py-12">
24
+ <li v-for="post, i in posts" :key="i" class="py-8">
22
25
  <PressArticleCard :post="post" />
23
26
  </li>
24
27
  </TransitionGroup>
@@ -62,10 +62,13 @@ onContentUpdated(() => {
62
62
  <slot name="main-content-before" />
63
63
 
64
64
  <ValaxyMd class="mx-auto w-full max-w-4xl" :frontmatter="frontmatter">
65
- <h1 v-if="hasSidebar && !isHome && $title" :id="$title" tabindex="-1">
66
- {{ $title }}
67
- <a class="header-anchor" :href="`#${$title}`" aria-hidden="true" />
68
- </h1>
65
+ <div v-if="hasSidebar && !isHome && $title" flex items-center justify-between gap-2>
66
+ <h1 :id="$title" tabindex="-1" class="!mt-0">
67
+ {{ $title }}
68
+ <a class="header-anchor" :href="`#${$title}`" aria-hidden="true" />
69
+ </h1>
70
+ <PressPostActions />
71
+ </div>
69
72
  <slot name="main-content-md" />
70
73
  <slot />
71
74
  </ValaxyMd>
@@ -0,0 +1,23 @@
1
+ <script lang="ts" setup>
2
+ import { useFrontmatter, useValaxyI18n } from 'valaxy'
3
+ import { computed } from 'vue'
4
+
5
+ const frontmatter = useFrontmatter()
6
+ const { $tO } = useValaxyI18n()
7
+ const title = computed(() => $tO(frontmatter.value.title))
8
+ </script>
9
+
10
+ <template>
11
+ <Layout>
12
+ <template #main-content>
13
+ <div class="max-w-6xl mx-auto w-full pb-12 px-4 sm:px-6 lg:px-8">
14
+ <div class="pt-6 pb-8 space-y-2 md:space-y-5">
15
+ <h1 class="text-3xl leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-5xl md:leading-13">
16
+ {{ title }}
17
+ </h1>
18
+ </div>
19
+ <PressPostList />
20
+ </div>
21
+ </template>
22
+ </Layout>
23
+ </template>