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
|
@@ -1,262 +1,274 @@
|
|
|
1
|
-
<script lang="ts"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
|
|
3
|
+
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
|
|
4
|
+
import type { AlgoliaSearchOptions } from 'valaxy-addon-algolia'
|
|
5
|
+
import { useAddonAlgoliaConfig } from 'valaxy-addon-algolia'
|
|
6
|
+
import { nextTick, onUnmounted, watch } from 'vue'
|
|
7
|
+
import { useRouter } from 'vue-router'
|
|
8
|
+
|
|
9
|
+
import '../styles/docsearch.css'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
openRequest?: {
|
|
13
|
+
target: 'search' | 'askAi' | 'toggleAskAi'
|
|
14
|
+
nonce: number
|
|
15
|
+
} | null
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const algoliaConfig = useAddonAlgoliaConfig()
|
|
20
|
+
|
|
21
|
+
let cleanup = () => {}
|
|
22
|
+
let docsearchInstance: DocSearchInstance | undefined
|
|
23
|
+
let sidepanelInstance: SidepanelInstance | undefined
|
|
24
|
+
let openOnReady: 'search' | 'askAi' | null = null
|
|
25
|
+
let initializeCount = 0
|
|
26
|
+
let docsearchLoader: Promise<typeof import('@docsearch/js')> | undefined
|
|
27
|
+
let sidepanelLoader: Promise<typeof import('@docsearch/sidepanel-js')> | undefined
|
|
28
|
+
let lastFocusedElement: HTMLElement | null = null
|
|
29
|
+
let skipEventDocsearch = false
|
|
30
|
+
let skipEventSidepanel = false
|
|
31
|
+
|
|
32
|
+
watch(
|
|
33
|
+
() => algoliaConfig.value?.options,
|
|
34
|
+
(options) => {
|
|
35
|
+
if (options)
|
|
36
|
+
update(options)
|
|
37
|
+
},
|
|
38
|
+
{ immediate: true },
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
onUnmounted(cleanup)
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
() => props.openRequest?.nonce,
|
|
45
|
+
() => {
|
|
46
|
+
const req = props.openRequest
|
|
47
|
+
if (!req)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if (req.target === 'search') {
|
|
51
|
+
if (docsearchInstance?.isReady) {
|
|
52
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.open())
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
openOnReady = 'search'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (req.target === 'toggleAskAi') {
|
|
59
|
+
if (sidepanelInstance?.isOpen) {
|
|
60
|
+
sidepanelInstance.close()
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// askAi - open sidepanel or fallback to docsearch modal
|
|
68
|
+
if (sidepanelInstance?.isReady) {
|
|
69
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
70
|
+
}
|
|
71
|
+
else if (sidepanelInstance) {
|
|
72
|
+
openOnReady = 'askAi'
|
|
73
|
+
}
|
|
74
|
+
else if (docsearchInstance?.isReady) {
|
|
75
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
openOnReady = 'askAi'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ immediate: true },
|
|
83
|
+
)
|
|
5
84
|
|
|
6
|
-
|
|
85
|
+
async function update(options: AlgoliaSearchOptions) {
|
|
86
|
+
await nextTick()
|
|
7
87
|
|
|
8
|
-
|
|
88
|
+
// Normalize askAi: string -> { assistantId }
|
|
89
|
+
const askAi = typeof options.askAi === 'string'
|
|
90
|
+
? { assistantId: options.askAi }
|
|
91
|
+
: options.askAi || undefined
|
|
9
92
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
dispatchEvent,
|
|
14
|
-
})
|
|
93
|
+
const appId = options.appId ?? askAi?.appId
|
|
94
|
+
const apiKey = options.apiKey ?? askAi?.apiKey
|
|
95
|
+
const indexName = options.indexName ?? askAi?.indexName
|
|
15
96
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
97
|
+
if (!appId || !apiKey || !indexName) {
|
|
98
|
+
console.warn('[valaxy-theme-press] Algolia search cannot be initialized: missing appId/apiKey/indexName.')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
19
101
|
|
|
20
|
-
|
|
21
|
-
element.isContentEditable
|
|
22
|
-
|| tagName === 'INPUT'
|
|
23
|
-
|| tagName === 'SELECT'
|
|
24
|
-
|| tagName === 'TEXTAREA'
|
|
25
|
-
)
|
|
102
|
+
await initialize({ ...options, appId, apiKey, indexName })
|
|
26
103
|
}
|
|
27
104
|
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
105
|
+
async function initialize(userOptions: AlgoliaSearchOptions) {
|
|
106
|
+
const currentInitialize = ++initializeCount
|
|
107
|
+
|
|
108
|
+
// Always tear down previous instances first
|
|
109
|
+
cleanup()
|
|
110
|
+
|
|
111
|
+
const { askAi: _askAi, locales: _locales, mode: _mode, ...docSearchUserOptions } = userOptions
|
|
112
|
+
// Normalize askAi: string -> { assistantId }
|
|
113
|
+
const askAi = typeof _askAi === 'string'
|
|
114
|
+
? { assistantId: _askAi }
|
|
115
|
+
: _askAi || undefined
|
|
116
|
+
|
|
117
|
+
const { default: docsearch } = await loadDocsearch()
|
|
118
|
+
if (currentInitialize !== initializeCount)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
// Initialize sidepanel if askAi.sidePanel is configured
|
|
122
|
+
if (askAi?.sidePanel) {
|
|
123
|
+
const { default: sidepanel } = await loadSidepanel()
|
|
124
|
+
if (currentInitialize !== initializeCount)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
const sidePanelConfig = askAi.sidePanel === true ? {} : askAi.sidePanel
|
|
128
|
+
|
|
129
|
+
sidepanelInstance = sidepanel({
|
|
130
|
+
...sidePanelConfig,
|
|
131
|
+
container: '#press-docsearch-sidepanel',
|
|
132
|
+
indexName: askAi.indexName ?? docSearchUserOptions.indexName,
|
|
133
|
+
appId: askAi.appId ?? docSearchUserOptions.appId,
|
|
134
|
+
apiKey: askAi.apiKey ?? docSearchUserOptions.apiKey,
|
|
135
|
+
assistantId: askAi.assistantId,
|
|
136
|
+
onOpen: focusInput,
|
|
137
|
+
onClose: onClose.bind(null, 'sidepanel'),
|
|
138
|
+
onReady: () => {
|
|
139
|
+
if (openOnReady === 'askAi') {
|
|
140
|
+
openOnReady = null
|
|
141
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
keyboardShortcuts: {
|
|
145
|
+
'Ctrl/Cmd+I': false,
|
|
146
|
+
},
|
|
147
|
+
} as SidepanelProps)
|
|
39
148
|
}
|
|
40
149
|
|
|
41
|
-
const
|
|
42
|
-
|
|
150
|
+
const options: DocSearchProps = {
|
|
151
|
+
...docSearchUserOptions as DocSearchProps,
|
|
152
|
+
container: '#press-docsearch',
|
|
153
|
+
navigator: {
|
|
154
|
+
navigate(item) {
|
|
155
|
+
const { pathname, hash } = new URL(item.itemUrl, location.origin)
|
|
156
|
+
router.push(pathname + hash)
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
transformItems: items =>
|
|
160
|
+
items.map(item => ({
|
|
161
|
+
...item,
|
|
162
|
+
url: getRelativePath(item.url),
|
|
163
|
+
})),
|
|
164
|
+
// When sidepanel is enabled, intercept Ask AI events to open it instead
|
|
165
|
+
...(sidepanelInstance && {
|
|
166
|
+
interceptAskAiEvent: (initialMessage: any) => {
|
|
167
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open(initialMessage))
|
|
168
|
+
return true
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
onOpen: focusInput,
|
|
172
|
+
onClose: onClose.bind(null, 'docsearch'),
|
|
173
|
+
onReady: () => {
|
|
174
|
+
if (openOnReady === 'search') {
|
|
175
|
+
openOnReady = null
|
|
176
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.open())
|
|
177
|
+
}
|
|
178
|
+
else if (openOnReady === 'askAi' && !sidepanelInstance) {
|
|
179
|
+
openOnReady = null
|
|
180
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
keyboardShortcuts: {
|
|
184
|
+
'/': false,
|
|
185
|
+
'Ctrl/Cmd+K': false,
|
|
186
|
+
},
|
|
43
187
|
}
|
|
44
188
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
onUnmounted(remove)
|
|
48
|
-
})
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<template>
|
|
52
|
-
<div>
|
|
53
|
-
<AlgoliaSearchBox v-if="loaded" />
|
|
54
|
-
|
|
55
|
-
<div v-else id="docsearch" @click="load">
|
|
56
|
-
<button
|
|
57
|
-
class="DocSearch DocSearch-Button"
|
|
58
|
-
aria-label="Search"
|
|
59
|
-
>
|
|
60
|
-
<span class="DocSearch-Button-Container">
|
|
61
|
-
<svg
|
|
62
|
-
class="DocSearch-Search-Icon"
|
|
63
|
-
width="20"
|
|
64
|
-
height="20"
|
|
65
|
-
viewBox="0 0 20 20"
|
|
66
|
-
>
|
|
67
|
-
<path
|
|
68
|
-
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"
|
|
69
|
-
stroke="currentColor"
|
|
70
|
-
fill="none"
|
|
71
|
-
fill-rule="evenodd"
|
|
72
|
-
stroke-linecap="round"
|
|
73
|
-
stroke-linejoin="round"
|
|
74
|
-
/>
|
|
75
|
-
</svg>
|
|
76
|
-
<span class="DocSearch-Button-Placeholder">{{ t('search.placeholder') }}</span>
|
|
77
|
-
</span>
|
|
78
|
-
<span class="DocSearch-Button-Keys">
|
|
79
|
-
<kbd class="DocSearch-Button-Key" />
|
|
80
|
-
<kbd class="DocSearch-Button-Key">K</kbd>
|
|
81
|
-
</span>
|
|
82
|
-
</button>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</template>
|
|
86
|
-
|
|
87
|
-
<style>
|
|
88
|
-
/* stylelint-disable selector-class-pattern */
|
|
89
|
-
.DocSearch-Button {
|
|
90
|
-
display: flex;
|
|
91
|
-
justify-content: center;
|
|
92
|
-
align-items: center;
|
|
93
|
-
margin: 0;
|
|
94
|
-
padding: 0;
|
|
95
|
-
width: 32px;
|
|
96
|
-
height: 55px;
|
|
97
|
-
background: transparent;
|
|
98
|
-
transition: border-color 0.25s;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.DocSearch-Button:hover {
|
|
102
|
-
background: transparent;
|
|
103
|
-
}
|
|
189
|
+
docsearchInstance = docsearch(options)
|
|
104
190
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
191
|
+
cleanup = () => {
|
|
192
|
+
docsearchInstance?.destroy()
|
|
193
|
+
sidepanelInstance?.destroy()
|
|
194
|
+
docsearchInstance = undefined
|
|
195
|
+
sidepanelInstance = undefined
|
|
196
|
+
openOnReady = null
|
|
197
|
+
lastFocusedElement = null
|
|
198
|
+
}
|
|
113
199
|
}
|
|
114
200
|
|
|
115
|
-
|
|
116
|
-
|
|
201
|
+
function focusInput() {
|
|
202
|
+
requestAnimationFrame(() => {
|
|
203
|
+
const input
|
|
204
|
+
= document.querySelector<HTMLInputElement>('#docsearch-input')
|
|
205
|
+
|| document.querySelector<HTMLInputElement>('#docsearch-sidepanel textarea')
|
|
206
|
+
input?.focus()
|
|
207
|
+
})
|
|
117
208
|
}
|
|
118
209
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
210
|
+
function onBeforeOpen(target: 'docsearch' | 'sidepanel', cb: () => void) {
|
|
211
|
+
if (target === 'docsearch') {
|
|
212
|
+
if (sidepanelInstance?.isOpen) {
|
|
213
|
+
skipEventSidepanel = true
|
|
214
|
+
sidepanelInstance.close()
|
|
215
|
+
}
|
|
216
|
+
else if (!docsearchInstance?.isOpen) {
|
|
217
|
+
if (document.activeElement instanceof HTMLElement)
|
|
218
|
+
lastFocusedElement = document.activeElement
|
|
219
|
+
}
|
|
128
220
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
221
|
+
else if (target === 'sidepanel') {
|
|
222
|
+
if (docsearchInstance?.isOpen) {
|
|
223
|
+
skipEventDocsearch = true
|
|
224
|
+
docsearchInstance.close()
|
|
225
|
+
}
|
|
226
|
+
else if (!sidepanelInstance?.isOpen) {
|
|
227
|
+
if (document.activeElement instanceof HTMLElement)
|
|
228
|
+
lastFocusedElement = document.activeElement
|
|
229
|
+
}
|
|
133
230
|
}
|
|
231
|
+
setTimeout(cb, 0)
|
|
134
232
|
}
|
|
135
233
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
position: relative;
|
|
143
|
-
width: 16px;
|
|
144
|
-
height: 16px;
|
|
145
|
-
color: var(--va-c-text-1);
|
|
146
|
-
fill: currentcolor;
|
|
147
|
-
transition: color 0.5s;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.DocSearch-Button:hover .DocSearch-Search-Icon {
|
|
151
|
-
color: var(--va-c-text-1);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@media (width >= 768px) {
|
|
155
|
-
.DocSearch-Button .DocSearch-Search-Icon {
|
|
156
|
-
top: 1px;
|
|
157
|
-
margin-right: 8px;
|
|
158
|
-
width: 14px;
|
|
159
|
-
height: 14px;
|
|
160
|
-
color: var(--va-c-text-2);
|
|
234
|
+
function onClose(target: 'docsearch' | 'sidepanel') {
|
|
235
|
+
if (target === 'docsearch') {
|
|
236
|
+
if (skipEventDocsearch) {
|
|
237
|
+
skipEventDocsearch = false
|
|
238
|
+
return
|
|
239
|
+
}
|
|
161
240
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
padding: 0 16px 0 0;
|
|
168
|
-
font-size: 13px;
|
|
169
|
-
font-weight: 500;
|
|
170
|
-
color: var(--va-c-text-2);
|
|
171
|
-
transition: color 0.5s;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
|
|
175
|
-
color: var(--va-c-text-1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
@media (width >= 768px) {
|
|
179
|
-
.DocSearch-Button .DocSearch-Button-Placeholder {
|
|
180
|
-
display: inline-block;
|
|
241
|
+
else if (target === 'sidepanel') {
|
|
242
|
+
if (skipEventSidepanel) {
|
|
243
|
+
skipEventSidepanel = false
|
|
244
|
+
return
|
|
245
|
+
}
|
|
181
246
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/* rtl:ignore */
|
|
186
|
-
direction: ltr;
|
|
187
|
-
display: none;
|
|
188
|
-
min-width: auto;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
@media (width >= 768px) {
|
|
192
|
-
.DocSearch-Button .DocSearch-Button-Keys {
|
|
193
|
-
display: flex;
|
|
194
|
-
align-items: center;
|
|
247
|
+
if (lastFocusedElement) {
|
|
248
|
+
lastFocusedElement.focus()
|
|
249
|
+
lastFocusedElement = null
|
|
195
250
|
}
|
|
196
251
|
}
|
|
197
252
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/* rtl:begin:ignore */
|
|
204
|
-
border-right: none;
|
|
205
|
-
border-radius: 4px 0 0 4px;
|
|
206
|
-
padding-left: 6px;
|
|
207
|
-
|
|
208
|
-
/* rtl:end:ignore */
|
|
209
|
-
min-width: 0;
|
|
210
|
-
width: auto;
|
|
211
|
-
height: 22px;
|
|
212
|
-
font-family: var(--va-font-sans);
|
|
213
|
-
font-size: 12px;
|
|
214
|
-
font-weight: 500;
|
|
215
|
-
transition: color 0.5s, border-color 0.5s;
|
|
253
|
+
function loadDocsearch() {
|
|
254
|
+
if (!docsearchLoader)
|
|
255
|
+
docsearchLoader = import('@docsearch/js')
|
|
256
|
+
return docsearchLoader
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
border-radius: 0 4px 4px 0;
|
|
223
|
-
padding-left: 2px;
|
|
224
|
-
padding-right: 6px;
|
|
225
|
-
|
|
226
|
-
/* rtl:end:ignore */
|
|
259
|
+
function loadSidepanel() {
|
|
260
|
+
if (!sidepanelLoader)
|
|
261
|
+
sidepanelLoader = import('@docsearch/sidepanel-js')
|
|
262
|
+
return sidepanelLoader
|
|
227
263
|
}
|
|
228
264
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
color: transparent;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.DocSearch-Button .DocSearch-Button-Key:first-child::after {
|
|
236
|
-
content: 'Ctrl';
|
|
237
|
-
font-size: 12px;
|
|
238
|
-
letter-spacing: normal;
|
|
239
|
-
color: var(--docsearch-muted-color);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.mac .DocSearch-Button .DocSearch-Button-Key:first-child::after {
|
|
243
|
-
content: '\2318';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
|
|
247
|
-
display: none;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.dark .DocSearch-Footer {
|
|
251
|
-
border-top: 1px solid var(--va-c-divider);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.DocSearch-Form {
|
|
255
|
-
border: 1px solid var(--va-c-brand);
|
|
256
|
-
background-color: var(--va-c-white);
|
|
265
|
+
function getRelativePath(url: string) {
|
|
266
|
+
const { pathname, hash } = new URL(url, location.origin)
|
|
267
|
+
return pathname.replace(/\.html$/, '') + hash
|
|
257
268
|
}
|
|
269
|
+
</script>
|
|
258
270
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
</
|
|
271
|
+
<template>
|
|
272
|
+
<div id="press-docsearch" />
|
|
273
|
+
<div id="press-docsearch-sidepanel" />
|
|
274
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useFrontmatter, useSiteStore, useValaxyI18n } from 'valaxy'
|
|
2
|
+
import { formatDate, useFrontmatter, useSiteStore, useValaxyI18n } from 'valaxy'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
import { useRoute } from 'vue-router'
|
|
@@ -10,20 +10,23 @@ const route = useRoute()
|
|
|
10
10
|
const site = useSiteStore()
|
|
11
11
|
|
|
12
12
|
function findCurrentIndex() {
|
|
13
|
-
return site.postList.findIndex(p => p.
|
|
13
|
+
return site.postList.findIndex(p => p.path === route.path)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const nextPost = computed(() => site.postList[findCurrentIndex() - 1])
|
|
17
17
|
const prevPost = computed(() => site.postList[findCurrentIndex() + 1])
|
|
18
|
+
const posts = computed(() => site.postList.filter(p => p.path && !p.path.endsWith('/')))
|
|
18
19
|
|
|
19
20
|
const { $tO } = useValaxyI18n()
|
|
20
21
|
const $title = computed(() => $tO(frontmatter.value.title))
|
|
22
|
+
|
|
23
|
+
const createdDate = computed(() => frontmatter.value.date ? formatDate(frontmatter.value.date) : '')
|
|
24
|
+
const updatedDate = computed(() => frontmatter.value.updated ? formatDate(frontmatter.value.updated) : '')
|
|
21
25
|
</script>
|
|
22
26
|
|
|
23
27
|
<template>
|
|
24
28
|
<article class="xl:divide-y xl:divide-gray-200 max-w-7xl m-auto" p="x-6" w="full">
|
|
25
|
-
<header class="pt-20 xl:pb-10
|
|
26
|
-
<PressDate :date="frontmatter.date" />
|
|
29
|
+
<header class="pt-20 xl:pb-10 text-center">
|
|
27
30
|
<h1
|
|
28
31
|
class="
|
|
29
32
|
text-3xl
|
|
@@ -32,10 +35,30 @@ const $title = computed(() => $tO(frontmatter.value.title))
|
|
|
32
35
|
tracking-tight
|
|
33
36
|
sm:text-4xl sm:leading-10
|
|
34
37
|
md:text-5xl md:leading-14
|
|
38
|
+
mb-4
|
|
35
39
|
"
|
|
36
40
|
>
|
|
37
41
|
{{ $title }}
|
|
38
42
|
</h1>
|
|
43
|
+
<div class="flex items-center justify-center gap-4 text-sm text-gray-500">
|
|
44
|
+
<span v-if="createdDate" class="inline-flex items-center gap-1">
|
|
45
|
+
<span class="i-ri-calendar-line" />
|
|
46
|
+
<time :datetime="createdDate">{{ createdDate }}</time>
|
|
47
|
+
</span>
|
|
48
|
+
<span v-if="updatedDate && updatedDate !== createdDate" class="inline-flex items-center gap-1">
|
|
49
|
+
<span class="i-ri-edit-line" />
|
|
50
|
+
<time :datetime="updatedDate">{{ updatedDate }}</time>
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div v-if="frontmatter.tags?.length || frontmatter.categories" class="mt-3 flex items-center justify-center gap-2 flex-wrap text-xs">
|
|
54
|
+
<RouterLink
|
|
55
|
+
v-for="tag in (Array.isArray(frontmatter.tags) ? frontmatter.tags : [])" :key="tag"
|
|
56
|
+
class="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 no-underline hover:bg-primary/10 hover:text-primary transition-colors"
|
|
57
|
+
:to="{ path: '/tags/', query: { tag } }"
|
|
58
|
+
>
|
|
59
|
+
#{{ tag }}
|
|
60
|
+
</RouterLink>
|
|
61
|
+
</div>
|
|
39
62
|
</header>
|
|
40
63
|
|
|
41
64
|
<div
|
|
@@ -63,29 +86,47 @@ const $title = computed(() => $tO(frontmatter.value.title))
|
|
|
63
86
|
xl:col-start-1 xl:row-start-2 xl:col-span-2
|
|
64
87
|
"
|
|
65
88
|
>
|
|
66
|
-
<div v-if="nextPost" class="py-8">
|
|
89
|
+
<div v-if="nextPost && nextPost.path" class="py-8">
|
|
67
90
|
<h2 class="text-xs tracking-wide uppercase text-gray-500">
|
|
68
91
|
Next Article
|
|
69
92
|
</h2>
|
|
70
93
|
<div class="link">
|
|
71
|
-
<RouterLink :to="nextPost.
|
|
94
|
+
<RouterLink :to="nextPost.path">
|
|
72
95
|
{{ $tO(nextPost.title) }}
|
|
73
96
|
</RouterLink>
|
|
74
97
|
</div>
|
|
75
98
|
</div>
|
|
76
|
-
<div v-if="prevPost && prevPost.
|
|
99
|
+
<div v-if="prevPost && prevPost.path" class="py-8">
|
|
77
100
|
<h2 class="text-xs tracking-wide uppercase text-gray-500">
|
|
78
101
|
Previous Article
|
|
79
102
|
</h2>
|
|
80
103
|
<div class="link">
|
|
81
|
-
<RouterLink :to="prevPost.
|
|
104
|
+
<RouterLink :to="prevPost.path">
|
|
82
105
|
{{ $tO(prevPost.title) }}
|
|
83
106
|
</RouterLink>
|
|
84
107
|
</div>
|
|
85
108
|
</div>
|
|
109
|
+
|
|
110
|
+
<div v-if="posts.length" class="py-8">
|
|
111
|
+
<h2 class="text-xs tracking-wide uppercase text-gray-500 mb-4">
|
|
112
|
+
All Posts
|
|
113
|
+
</h2>
|
|
114
|
+
<ul class="space-y-2">
|
|
115
|
+
<li v-for="post in posts" :key="post.path">
|
|
116
|
+
<RouterLink
|
|
117
|
+
class="link block truncate" :class="post.path === route.path ? 'font-bold text-primary' : 'op-50 hover:op-80'"
|
|
118
|
+
:to="post.path || ''"
|
|
119
|
+
:title="$tO(post.title)"
|
|
120
|
+
>
|
|
121
|
+
{{ $tO(post.title) }}
|
|
122
|
+
</RouterLink>
|
|
123
|
+
</li>
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
86
127
|
<div class="pt-8">
|
|
87
|
-
<RouterLink class="link" to="/">
|
|
88
|
-
← Back to
|
|
128
|
+
<RouterLink class="link" to="/posts/">
|
|
129
|
+
← Back to Posts
|
|
89
130
|
</RouterLink>
|
|
90
131
|
</div>
|
|
91
132
|
</footer>
|
|
@@ -10,26 +10,38 @@ const { $tO } = useValaxyI18n()
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
|
-
<article class="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
|
|
13
|
+
<article class="press-article-card space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
|
|
14
14
|
<PressDate :date="post.date" />
|
|
15
15
|
<div class="space-y-5 xl:col-span-3">
|
|
16
16
|
<div class="space-y-6">
|
|
17
17
|
<h2 class="text-2xl leading-8 font-bold tracking-tight">
|
|
18
|
-
<RouterLink class="text-gray-900" :to="post.path || ''">
|
|
18
|
+
<RouterLink class="press-article-card-link text-gray-900 dark:text-gray-100 hover:text-primary transition-colors" :to="post.path || ''">
|
|
19
19
|
{{ $tO(post.title) }}
|
|
20
20
|
</RouterLink>
|
|
21
21
|
</h2>
|
|
22
22
|
<div
|
|
23
23
|
v-if="post.excerpt"
|
|
24
|
-
class="prose dark:prose-invert max-w-none
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
class="press-article-card-excerpt prose dark:prose-invert max-w-none"
|
|
25
|
+
>
|
|
26
|
+
<ValaxyDynamicComponent :template-str="post.excerpt" />
|
|
27
|
+
</div>
|
|
27
28
|
</div>
|
|
28
29
|
<div class="text-base leading-6 font-medium">
|
|
29
|
-
<RouterLink class="link" aria-label="read more" :to="post.path || ''">
|
|
30
|
+
<RouterLink class="press-article-card-link text-primary hover:underline" aria-label="read more" :to="post.path || ''">
|
|
30
31
|
Read more →
|
|
31
32
|
</RouterLink>
|
|
32
33
|
</div>
|
|
33
34
|
</div>
|
|
34
35
|
</article>
|
|
35
36
|
</template>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
.press-article-card-link {
|
|
40
|
+
text-decoration: none !important;
|
|
41
|
+
border-bottom: none !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.press-article-card-excerpt :deep(a) {
|
|
45
|
+
border-bottom: none;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -7,7 +7,7 @@ const editLink = useEditLink()
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<template>
|
|
10
|
-
<div flex justify="between" text="sm">
|
|
10
|
+
<div flex justify="between" items="center" text="sm">
|
|
11
11
|
<a flex items="center" class="decoration-none!" :href="editLink.url" target="_blank">
|
|
12
12
|
<div i-ri-external-link-line />
|
|
13
13
|
<span ml-1>{{ editLink.text || t('tooltip.edit_this_page') }}</span>
|