@fy-/fws-vue-core 3.0.4 → 3.0.6

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 (121) hide show
  1. package/package.json +6 -8
  2. package/src/components/fws/CmsArticleBoxed.vue +247 -0
  3. package/src/components/fws/CmsArticleSingle.vue +201 -0
  4. package/src/components/fws/DataTable.vue +659 -0
  5. package/src/components/fws/FilterData.vue +423 -0
  6. package/src/components/fws/UserData.vue +220 -0
  7. package/src/components/fws/UserFlow.vue +955 -0
  8. package/src/components/fws/UserOAuth2.vue +521 -0
  9. package/src/components/fws/UserProfile.vue +615 -0
  10. package/src/components/fws/UserProfileStrict.vue +233 -0
  11. package/src/components/ssr/ClientOnly.ts +10 -0
  12. package/src/components/ui/DefaultBreadcrumb.vue +99 -0
  13. package/src/components/ui/DefaultConfirm.vue +178 -0
  14. package/src/components/ui/DefaultConfirmWithInput.vue +217 -0
  15. package/src/components/ui/DefaultDropdown.vue +104 -0
  16. package/src/components/ui/DefaultDropdownLink.vue +94 -0
  17. package/src/components/ui/DefaultGallery.vue +1056 -0
  18. package/src/components/ui/DefaultInput.vue +768 -0
  19. package/src/components/ui/DefaultLoader.vue +125 -0
  20. package/src/components/ui/DefaultModal.vue +350 -0
  21. package/src/components/ui/DefaultNotif.vue +332 -0
  22. package/src/components/ui/DefaultPaging.vue +395 -0
  23. package/src/components/ui/DefaultSidebar.vue +267 -0
  24. package/src/components/ui/DefaultTagInput.vue +415 -0
  25. package/src/components/ui/transitions/CollapseTransition.vue +19 -0
  26. package/src/components/ui/transitions/ExpandTransition.vue +19 -0
  27. package/src/components/ui/transitions/FadeTransition.vue +17 -0
  28. package/src/components/ui/transitions/ScaleTransition.vue +21 -0
  29. package/src/components/ui/transitions/SlideTransition.vue +32 -0
  30. package/src/composables/event-bus.ts +15 -0
  31. package/src/composables/rest.ts +165 -0
  32. package/src/composables/seo.ts +142 -0
  33. package/src/composables/ssr.ts +103 -0
  34. package/src/composables/templating.ts +133 -0
  35. package/src/composables/translations.ts +45 -0
  36. package/src/env.d.ts +10 -0
  37. package/{dist/src/index.d.ts → src/index.ts} +71 -45
  38. package/src/plugin.ts +42 -0
  39. package/src/safelist.html +11 -0
  40. package/src/stores/serverRouter.ts +62 -0
  41. package/src/stores/user.ts +118 -0
  42. package/src/types.ts +58 -0
  43. package/dist/index.css +0 -2
  44. package/dist/index.js +0 -5767
  45. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts +0 -32
  46. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts.map +0 -1
  47. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts +0 -29
  48. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts.map +0 -1
  49. package/dist/src/components/fws/DataTable.vue.d.ts +0 -52
  50. package/dist/src/components/fws/DataTable.vue.d.ts.map +0 -1
  51. package/dist/src/components/fws/FilterData.vue.d.ts +0 -15
  52. package/dist/src/components/fws/FilterData.vue.d.ts.map +0 -1
  53. package/dist/src/components/fws/UserData.vue.d.ts +0 -8
  54. package/dist/src/components/fws/UserData.vue.d.ts.map +0 -1
  55. package/dist/src/components/fws/UserFlow.vue.d.ts +0 -116
  56. package/dist/src/components/fws/UserFlow.vue.d.ts.map +0 -1
  57. package/dist/src/components/fws/UserOAuth2.vue.d.ts +0 -17
  58. package/dist/src/components/fws/UserOAuth2.vue.d.ts.map +0 -1
  59. package/dist/src/components/fws/UserProfile.vue.d.ts +0 -40
  60. package/dist/src/components/fws/UserProfile.vue.d.ts.map +0 -1
  61. package/dist/src/components/fws/UserProfileStrict.vue.d.ts +0 -12
  62. package/dist/src/components/fws/UserProfileStrict.vue.d.ts.map +0 -1
  63. package/dist/src/components/ssr/ClientOnly.d.ts +0 -4
  64. package/dist/src/components/ssr/ClientOnly.d.ts.map +0 -1
  65. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts +0 -11
  66. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts.map +0 -1
  67. package/dist/src/components/ui/DefaultConfirm.vue.d.ts +0 -81
  68. package/dist/src/components/ui/DefaultConfirm.vue.d.ts.map +0 -1
  69. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts +0 -81
  70. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts.map +0 -1
  71. package/dist/src/components/ui/DefaultDropdown.vue.d.ts +0 -35
  72. package/dist/src/components/ui/DefaultDropdown.vue.d.ts.map +0 -1
  73. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts +0 -23
  74. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts.map +0 -1
  75. package/dist/src/components/ui/DefaultGallery.vue.d.ts +0 -114
  76. package/dist/src/components/ui/DefaultGallery.vue.d.ts.map +0 -1
  77. package/dist/src/components/ui/DefaultInput.vue.d.ts +0 -61
  78. package/dist/src/components/ui/DefaultInput.vue.d.ts.map +0 -1
  79. package/dist/src/components/ui/DefaultLoader.vue.d.ts +0 -12
  80. package/dist/src/components/ui/DefaultLoader.vue.d.ts.map +0 -1
  81. package/dist/src/components/ui/DefaultModal.vue.d.ts +0 -36
  82. package/dist/src/components/ui/DefaultModal.vue.d.ts.map +0 -1
  83. package/dist/src/components/ui/DefaultNotif.vue.d.ts +0 -3
  84. package/dist/src/components/ui/DefaultNotif.vue.d.ts.map +0 -1
  85. package/dist/src/components/ui/DefaultPaging.vue.d.ts +0 -13
  86. package/dist/src/components/ui/DefaultPaging.vue.d.ts.map +0 -1
  87. package/dist/src/components/ui/DefaultSidebar.vue.d.ts +0 -29
  88. package/dist/src/components/ui/DefaultSidebar.vue.d.ts.map +0 -1
  89. package/dist/src/components/ui/DefaultTagInput.vue.d.ts +0 -34
  90. package/dist/src/components/ui/DefaultTagInput.vue.d.ts.map +0 -1
  91. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts +0 -18
  92. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts.map +0 -1
  93. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts +0 -18
  94. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts.map +0 -1
  95. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts +0 -18
  96. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts.map +0 -1
  97. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts +0 -18
  98. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts.map +0 -1
  99. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts +0 -21
  100. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts.map +0 -1
  101. package/dist/src/composables/event-bus.d.ts +0 -8
  102. package/dist/src/composables/event-bus.d.ts.map +0 -1
  103. package/dist/src/composables/rest.d.ts +0 -24
  104. package/dist/src/composables/rest.d.ts.map +0 -1
  105. package/dist/src/composables/seo.d.ts +0 -26
  106. package/dist/src/composables/seo.d.ts.map +0 -1
  107. package/dist/src/composables/ssr.d.ts +0 -24
  108. package/dist/src/composables/ssr.d.ts.map +0 -1
  109. package/dist/src/composables/templating.d.ts +0 -7
  110. package/dist/src/composables/templating.d.ts.map +0 -1
  111. package/dist/src/composables/translations.d.ts +0 -8
  112. package/dist/src/composables/translations.d.ts.map +0 -1
  113. package/dist/src/index.d.ts.map +0 -1
  114. package/dist/src/plugin.d.ts +0 -3
  115. package/dist/src/plugin.d.ts.map +0 -1
  116. package/dist/src/stores/serverRouter.d.ts +0 -34
  117. package/dist/src/stores/serverRouter.d.ts.map +0 -1
  118. package/dist/src/stores/user.d.ts +0 -139
  119. package/dist/src/stores/user.d.ts.map +0 -1
  120. package/dist/src/types.d.ts +0 -48
  121. package/dist/src/types.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,20 +1,18 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue-core",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "type": "module",
5
5
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
6
6
  "license": "MIT",
7
7
  "exports": {
8
8
  ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js"
11
- },
12
- "./style.css": "./dist/index.css"
9
+ "import": "./src/index.ts"
10
+ }
13
11
  },
14
- "main": "./dist/index.js",
15
- "types": "./dist/index.d.ts",
12
+ "main": "./src/index.ts",
13
+ "types": "./src/index.ts",
16
14
  "files": [
17
- "dist"
15
+ "src"
18
16
  ],
19
17
  "sideEffects": [
20
18
  "*.css",
@@ -0,0 +1,247 @@
1
+ <script setup lang="ts">
2
+ import { CalendarDaysIcon } from '@heroicons/vue/24/solid'
3
+
4
+ defineProps({
5
+ article: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ type: {
10
+ type: String,
11
+ default: 'blog',
12
+ },
13
+ imageDomain: {
14
+ type: String,
15
+ default: 'https://s.nocachenocry.com',
16
+ },
17
+ })
18
+ </script>
19
+
20
+ <template>
21
+ <article
22
+ itemscope
23
+ itemtype="https://schema.org/Article"
24
+ class="fws-article-boxed"
25
+ >
26
+ <meta itemprop="wordCount" :content="article.WordCount">
27
+ <meta itemprop="datePublished" :content="article.CreatedAt.iso">
28
+ <meta itemprop="dateModified" :content="article.UpdatedAt.iso">
29
+ <meta itemprop="inLanguage" :content="article.Locale">
30
+ <meta itemprop="headline" :content="article.Title">
31
+ <meta
32
+ v-if="article.CoverUUID"
33
+ itemprop="thumbnailUrl"
34
+ :content="`${imageDomain}/${article.CoverUUID}?vars=format=webp:resize=512x512`"
35
+ >
36
+ <RouterLink
37
+ :to="`/${type}/${article.Slug}`"
38
+ :title="article.Title"
39
+ >
40
+ <img
41
+ v-if="article.CoverUUID"
42
+ :src="`${imageDomain}/${article.CoverUUID}?vars=format=webp:scale_crop_center=400x195`"
43
+ loading="lazy"
44
+ :title="article.Title"
45
+ :alt="article.Title"
46
+ class="fws-article-boxed__cover"
47
+ width="400"
48
+ height="195"
49
+ >
50
+ </RouterLink>
51
+ <h2 class="fws-article-boxed__title">
52
+ <RouterLink
53
+ :to="`/${type}/${article.Slug}`"
54
+ :title="article.Title"
55
+ rel="bookmark"
56
+ >
57
+ {{ article.Title }}
58
+ </RouterLink>
59
+ </h2>
60
+ <p class="fws-article-boxed__overview" itemprop="description">
61
+ {{ article.Overview }}
62
+ </p>
63
+ <div class="fws-article-boxed__footer">
64
+ <time
65
+ class="fws-article-boxed__date"
66
+ itemprop="datePublished"
67
+ :content="new Date(parseInt(article.CreatedAt.unixms)).toISOString()"
68
+ :datetime="new Date(parseInt(article.CreatedAt.unixms)).toISOString()"
69
+ >
70
+ <CalendarDaysIcon class="fws-article-boxed__date-icon" />
71
+ {{ $formatDate(article.CreatedAt.unixms) }}
72
+ </time>
73
+ <meta itemprop="dateModified" :content="new Date(parseInt(article.UpdatedAt.unixms)).toISOString()">
74
+ <meta itemprop="inLanguage" :content="article.Language__">
75
+ <RouterLink
76
+ :to="`/${type}/${article.Slug}`"
77
+ :title="article.Title"
78
+ class="fws-article-boxed__read-more"
79
+ >
80
+ {{ $t("read_more_cta") || 'Read more' }}
81
+ <svg class="fws-article-boxed__arrow" fill="currentColor" viewBox="0 0 20 20">
82
+ <path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
83
+ </svg>
84
+ </RouterLink>
85
+ </div>
86
+ </article>
87
+ </template>
88
+
89
+ <style scoped>
90
+ /* Card — Stripe multi-layer blue-tinted shadow, Notion radius */
91
+ .fws-article-boxed {
92
+ display: flex;
93
+ flex-direction: column;
94
+ justify-content: space-between;
95
+ padding: 0;
96
+ border-radius: 12px;
97
+ background: #fff;
98
+ overflow: hidden;
99
+ box-shadow:
100
+ 0 0 0 1px rgba(0, 0, 0, 0.05),
101
+ rgba(50, 50, 93, 0.08) 0 2px 5px,
102
+ rgba(0, 0, 0, 0.04) 0 1px 2px;
103
+ transition: box-shadow 0.25s ease, transform 0.25s ease;
104
+ }
105
+
106
+ .fws-article-boxed:hover {
107
+ transform: translateY(-2px);
108
+ box-shadow:
109
+ 0 0 0 1px rgba(0, 0, 0, 0.06),
110
+ rgba(50, 50, 93, 0.15) 0 8px 24px,
111
+ rgba(0, 0, 0, 0.08) 0 3px 8px;
112
+ }
113
+
114
+ .dark .fws-article-boxed {
115
+ background: rgba(255, 255, 255, 0.02);
116
+ box-shadow:
117
+ 0 0 0 1px rgba(255, 255, 255, 0.06);
118
+ }
119
+
120
+ .dark .fws-article-boxed:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow:
123
+ 0 0 0 1px rgba(255, 255, 255, 0.1),
124
+ rgba(0, 0, 0, 0.4) 0 8px 24px;
125
+ }
126
+
127
+ /* Cover image — contained with zoom on hover */
128
+ .fws-article-boxed__cover {
129
+ width: 100%;
130
+ aspect-ratio: 16 / 9;
131
+ object-fit: cover;
132
+ flex-shrink: 0;
133
+ transition: transform 0.4s ease;
134
+ }
135
+
136
+ .fws-article-boxed:hover .fws-article-boxed__cover {
137
+ transform: scale(1.03);
138
+ }
139
+
140
+ /* Content padding inside card body */
141
+ .fws-article-boxed__title {
142
+ font-size: 17px;
143
+ font-weight: 600;
144
+ margin: 14px 16px 6px;
145
+ flex-shrink: 0;
146
+ line-height: 1.35;
147
+ letter-spacing: -0.01em;
148
+ }
149
+
150
+ .fws-article-boxed__title a {
151
+ color: var(--fv-neutral-900, #0a0a0a);
152
+ text-decoration: none;
153
+ transition: color 0.15s ease;
154
+ }
155
+
156
+ .fws-article-boxed__title a:hover {
157
+ color: var(--fv-primary-600, #7c3aed);
158
+ }
159
+
160
+ .dark .fws-article-boxed__title a {
161
+ color: var(--fv-neutral-100, #f5f5f5);
162
+ }
163
+
164
+ .dark .fws-article-boxed__title a:hover {
165
+ color: var(--fv-primary-400, #a78bfa);
166
+ }
167
+
168
+ /* Overview — Stripe 300 weight, tighter leading */
169
+ .fws-article-boxed__overview {
170
+ font-size: 14px;
171
+ font-weight: 400;
172
+ color: var(--fv-neutral-500, #737373);
173
+ margin: 0 16px 16px;
174
+ flex-grow: 1;
175
+ line-height: 1.55;
176
+ display: -webkit-box;
177
+ -webkit-line-clamp: 3;
178
+ -webkit-box-orient: vertical;
179
+ overflow: hidden;
180
+ }
181
+
182
+ .dark .fws-article-boxed__overview {
183
+ color: var(--fv-neutral-400, #a3a3a3);
184
+ }
185
+
186
+ /* Footer */
187
+ .fws-article-boxed__footer {
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
191
+ flex-shrink: 0;
192
+ padding: 10px 16px;
193
+ border-top: 1px solid rgba(0, 0, 0, 0.04);
194
+ }
195
+
196
+ .dark .fws-article-boxed__footer {
197
+ border-top-color: rgba(255, 255, 255, 0.04);
198
+ }
199
+
200
+ .fws-article-boxed__date {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 4px;
204
+ font-size: 12px;
205
+ font-weight: 400;
206
+ color: var(--fv-neutral-400, #a3a3a3);
207
+ }
208
+
209
+ .dark .fws-article-boxed__date {
210
+ color: var(--fv-neutral-500, #737373);
211
+ }
212
+
213
+ .fws-article-boxed__date-icon {
214
+ width: 13px;
215
+ height: 13px;
216
+ }
217
+
218
+ /* Read more — pill link with arrow slide on hover */
219
+ .fws-article-boxed__read-more {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ font-size: 13px;
223
+ font-weight: 500;
224
+ color: var(--fv-primary-600, #7c3aed);
225
+ text-decoration: none;
226
+ transition: gap 0.2s ease;
227
+ gap: 4px;
228
+ }
229
+
230
+ .fws-article-boxed__read-more:hover {
231
+ gap: 8px;
232
+ }
233
+
234
+ .dark .fws-article-boxed__read-more {
235
+ color: var(--fv-primary-400, #a78bfa);
236
+ }
237
+
238
+ .fws-article-boxed__arrow {
239
+ width: 14px;
240
+ height: 14px;
241
+ transition: transform 0.2s ease;
242
+ }
243
+
244
+ .fws-article-boxed__read-more:hover .fws-article-boxed__arrow {
245
+ transform: translateX(2px);
246
+ }
247
+ </style>
@@ -0,0 +1,201 @@
1
+ <script setup lang="ts">
2
+ import type { Component } from 'vue'
3
+ import type { LazyHead } from '../../composables/seo'
4
+ import type { BreadcrumbLink } from '../../types'
5
+ import { defineWebPage } from '@unhead/schema-org'
6
+ import { useSchemaOrg } from '@unhead/schema-org/vue'
7
+ import { ref, watchEffect } from 'vue'
8
+ import { useRoute } from 'vue-router'
9
+ import { useRest } from '../../composables/rest'
10
+ import { useSeo } from '../../composables/seo'
11
+ import DefaultBreadcrumb from '../ui/DefaultBreadcrumb.vue'
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ baseUrl: string
16
+ cmsAlias: string
17
+ notFound: Component
18
+ baseBreadcrumb?: BreadcrumbLink[]
19
+ showImage?: boolean
20
+ showPreview?: boolean
21
+ showTitle?: boolean
22
+ postValue?: any
23
+ passData?: boolean
24
+ imageDomain?: string
25
+ multLanguage?: boolean
26
+ urlSlug?: string
27
+ }>(),
28
+ {
29
+ baseBreadcrumb: () => [],
30
+ showImage: true,
31
+ showPreview: true,
32
+ showTitle: true,
33
+ postValue: () => undefined,
34
+ passData: false,
35
+ imageDomain: 'https://s.nocachenocry.com',
36
+ multLanguage: true,
37
+ urlSlug: 'blog',
38
+ },
39
+ )
40
+
41
+ const rest = useRest()
42
+ const post = ref<any>([])
43
+ const route = useRoute()
44
+ const seo = ref<LazyHead>({})
45
+ const is404 = ref(false)
46
+
47
+ async function getBlogPost() {
48
+ let data: any
49
+ if (!props.passData) {
50
+ data = await rest(`Cms/${props.cmsAlias}/Post/${route.params.slug}`, 'GET')
51
+ }
52
+ else {
53
+ data = props.postValue
54
+ }
55
+ if (data && data.result === 'success') {
56
+ post.value = data.data
57
+ seo.value.title = post.value.Title
58
+ seo.value.description = post.value.Overview
59
+
60
+ if (post.value.CoverUUID) {
61
+ seo.value.image = `${props.imageDomain}/${post.value.CoverUUID}?vars=format=png:resize=512x512`
62
+ seo.value.imageWidth = '512'
63
+ seo.value.imageHeight = '512'
64
+ seo.value.imageType = 'image/png'
65
+ }
66
+ seo.value.locale = post.value.Locale || 'en-US'
67
+ if (props.multLanguage) {
68
+ seo.value.url = `https://${props.baseUrl}/l/${seo.value.locale}/${props.urlSlug}/${post.value.Slug}`
69
+ }
70
+ else {
71
+ seo.value.url = `https://${props.baseUrl}/${props.urlSlug}/${post.value.Slug}`
72
+ }
73
+ if (post.value.Locales && post.value.Locales.length > 1) {
74
+ seo.value.alternateLocales = post.value.Locales
75
+ }
76
+ }
77
+ else {
78
+ if (!props.passData) {
79
+ is404.value = true
80
+ }
81
+ }
82
+ }
83
+
84
+ await getBlogPost()
85
+ watchEffect(() => {
86
+ getBlogPost()
87
+ })
88
+ useSeo(seo)
89
+ useSchemaOrg([
90
+ defineWebPage({
91
+ datePublished: post.value.CreatedAt?.iso,
92
+ dateModified: post.value.UpdatedAt?.iso,
93
+ }),
94
+ ])
95
+ </script>
96
+
97
+ <template>
98
+ <div>
99
+ <div v-if="!is404 && post">
100
+ <div v-if="baseBreadcrumb.length > 0" class="fws-article-single__breadcrumb">
101
+ <DefaultBreadcrumb :links="[...baseBreadcrumb, { name: post.Title }]" />
102
+ </div>
103
+ <article itemscope itemtype="https://schema.org/Article" class="fws-article-single">
104
+ <meta itemprop="wordCount" :content="post.WordCount">
105
+ <meta itemprop="datePublished" :content="post.CreatedAt?.iso">
106
+ <meta itemprop="dateModified" :content="post.UpdatedAt?.iso">
107
+ <meta itemprop="inLanguage" :content="post.Locale">
108
+ <meta itemprop="headline" :content="post.Title">
109
+ <meta
110
+ v-if="post.CoverUUID"
111
+ itemprop="thumbnailUrl"
112
+ :content="`${imageDomain}/${post.CoverUUID}?vars=format=webp:resize=512x512`"
113
+ >
114
+ <div v-if="showTitle" class="fws-article-single__header">
115
+ <h1 class="fws-article-single__title">
116
+ {{ post.Title }}
117
+ </h1>
118
+ <p v-if="showPreview" class="fws-article-single__overview">
119
+ {{ post.Overview }}
120
+ </p>
121
+ </div>
122
+ <img
123
+ v-if="post.CoverUUID && showImage"
124
+ :src="`${imageDomain}/${post.CoverUUID}?vars=format=webp:resize=768x768`"
125
+ :alt="post.Title"
126
+ class="fws-article-single__cover"
127
+ >
128
+ <section
129
+ itemprop="articleBody"
130
+ class="fws-article-single__body prose dark:prose-invert"
131
+ v-html="post.Body"
132
+ />
133
+ </article>
134
+ </div>
135
+ <component :is="notFound" v-if="is404" />
136
+ </div>
137
+ </template>
138
+
139
+ <style scoped>
140
+ .fws-article-single__breadcrumb {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ margin-top: 12px;
145
+ }
146
+
147
+ @media (max-width: 768px) {
148
+ .fws-article-single__breadcrumb {
149
+ display: none;
150
+ }
151
+ }
152
+
153
+ .fws-article-single__header {
154
+ padding: 16px 16px 0;
155
+ max-width: 100%;
156
+ margin: 0 auto;
157
+ width: 100%;
158
+ }
159
+
160
+ .fws-article-single__title {
161
+ margin: 0 0 16px;
162
+ font-size: 32px;
163
+ font-weight: 800;
164
+ line-height: 1.2;
165
+ letter-spacing: -0.02em;
166
+ text-align: center;
167
+ color: var(--fv-neutral-900, #0a0a0a);
168
+ }
169
+
170
+ .dark .fws-article-single__title {
171
+ color: #fff;
172
+ }
173
+
174
+ .fws-article-single__overview {
175
+ font-size: 18px;
176
+ font-weight: 300;
177
+ text-align: center;
178
+ color: var(--fv-neutral-500, #737373);
179
+ margin: 0;
180
+ }
181
+
182
+ .dark .fws-article-single__overview {
183
+ color: var(--fv-neutral-400, #a3a3a3);
184
+ }
185
+
186
+ .fws-article-single__cover {
187
+ display: block;
188
+ max-width: 768px;
189
+ max-height: 280px;
190
+ height: auto;
191
+ margin: 24px auto;
192
+ border-radius: 12px;
193
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
194
+ }
195
+
196
+ .fws-article-single__body {
197
+ max-width: 100%;
198
+ margin: 0 auto 24px;
199
+ width: 100%;
200
+ }
201
+ </style>