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.
- package/components/PressAlgoliaSearch.vue +243 -231
- package/components/PressArticle.vue +51 -10
- package/components/PressArticleCard.vue +18 -6
- package/components/PressDocFooter.vue +1 -1
- package/components/PressNavBarAskAiButton.vue +42 -0
- package/components/PressNavBarSearch.vue +132 -10
- package/components/PressNavBarSearchButton.vue +158 -0
- package/components/PressPostActions.vue +274 -0
- package/components/PressPostList.vue +5 -2
- package/components/ValaxyMain.vue +7 -4
- package/layouts/posts.vue +23 -0
- package/layouts/tags.vue +85 -0
- package/package.json +6 -4
- package/setup/main.ts +7 -1
- package/styles/docsearch.css +106 -0
- package/valaxy.config.ts +1 -1
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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">⌘</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(() =>
|
|
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-
|
|
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
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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>
|