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.
@@ -1,262 +1,274 @@
1
- <script lang="ts" setup>
2
- import { useAddonAlgolia } from 'valaxy-addon-algolia'
3
- import { onMounted, onUnmounted } from 'vue'
4
- import { useI18n } from 'vue-i18n'
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
- const { t } = useI18n()
85
+ async function update(options: AlgoliaSearchOptions) {
86
+ await nextTick()
7
87
 
8
- const { loaded, load, dispatchEvent } = useAddonAlgolia()
88
+ // Normalize askAi: string -> { assistantId }
89
+ const askAi = typeof options.askAi === 'string'
90
+ ? { assistantId: options.askAi }
91
+ : options.askAi || undefined
9
92
 
10
- defineExpose({
11
- loaded,
12
- load,
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
- function isEditingContent(event: KeyboardEvent): boolean {
17
- const element = event.target as HTMLElement
18
- const tagName = element.tagName
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
- return (
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
- onMounted(() => {
29
- const handleSearchHotKey = (event: KeyboardEvent) => {
30
- if (
31
- (event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey))
32
- || (!isEditingContent(event) && event.key === '/')
33
- ) {
34
- event.preventDefault()
35
- load()
36
- // eslint-disable-next-line ts/no-use-before-define
37
- remove()
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 remove = () => {
42
- window.removeEventListener('keydown', handleSearchHotKey)
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
- window.addEventListener('keydown', handleSearchHotKey)
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
- .DocSearch-Button:focus {
106
- outline: 1px dotted;
107
- outline: 5px auto -webkit-focus-ring-color;
108
- }
109
-
110
- .DocSearch-Button-Key--pressed {
111
- transform: none;
112
- box-shadow: none;
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
- .DocSearch-Button:focus:not(:focus-visible) {
116
- outline: none !important;
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
- @media (width >= 768px) {
120
- .DocSearch-Button {
121
- justify-content: flex-start;
122
- border: 1px solid transparent;
123
- border-radius: 8px;
124
- padding: 0 10px 0 12px;
125
- width: 100%;
126
- height: 40px;
127
- background-color: var(--va-c-bg-alt);
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
- .DocSearch-Button:hover {
131
- border-color: var(--va-c-brand);
132
- background: var(--va-c-bg-alt);
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
- .DocSearch-Button .DocSearch-Button-Container {
137
- display: flex;
138
- align-items: center;
139
- }
140
-
141
- .DocSearch-Button .DocSearch-Search-Icon {
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
- .DocSearch-Button .DocSearch-Button-Placeholder {
165
- display: none;
166
- margin-top: 2px;
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
- .DocSearch-Button .DocSearch-Button-Keys {
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
- .DocSearch-Button .DocSearch-Button-Key {
199
- display: block;
200
- margin: 2px 0 0;
201
- border: 1px solid var(--va-c-divider);
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
- .DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
219
- /* rtl:begin:ignore */
220
- border-right: 1px solid var(--va-c-divider);
221
- border-left: none;
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
- .DocSearch-Button .DocSearch-Button-Key:first-child {
230
- font-size: 1px;
231
- letter-spacing: -12px;
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
- .dark .DocSearch-Form {
260
- background-color: var(--va-c-bg-soft-mute);
261
- }
262
- </style>
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.href === route.path)
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 space-y-1 text-center">
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.href">
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.href" class="py-8">
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.href">
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 Home
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 text-gray-500"
25
- v-html="post.excerpt"
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>