@stackonward/cli 1.5.2 → 1.6.0

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 (78) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +112 -56
  4. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/android.svg +10 -0
  5. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/apple.svg +3 -0
  6. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/whatsapp.svg +5 -0
  7. package/dist/resources/components/nuxt/site-source/app/components/app/Sidebar.vue +1 -1
  8. package/dist/resources/components/nuxt/site-source/app/components/checkout/Status.vue +3 -3
  9. package/dist/resources/components/nuxt/site-source/app/components/consent/Banner.vue +23 -8
  10. package/dist/resources/components/nuxt/site-source/app/components/content/CallToAction.vue +59 -0
  11. package/dist/resources/components/nuxt/site-source/app/components/content/Card.vue +35 -0
  12. package/dist/resources/components/nuxt/site-source/app/components/content/Compatibility.vue +10 -0
  13. package/dist/resources/components/nuxt/site-source/app/components/content/Faq.vue +56 -0
  14. package/dist/resources/components/nuxt/site-source/app/components/content/Features.vue +8 -0
  15. package/dist/resources/components/nuxt/site-source/app/components/content/List.vue +1 -8
  16. package/dist/resources/components/nuxt/site-source/app/components/content/Reviews.vue +93 -0
  17. package/dist/resources/components/nuxt/site-source/app/components/countdown/Timer.vue +112 -0
  18. package/dist/resources/components/nuxt/site-source/app/components/locale/Menu.vue +3 -3
  19. package/dist/resources/components/nuxt/site-source/app/components/site/BackToTop.vue +30 -23
  20. package/dist/resources/components/nuxt/site-source/app/components/site/Header.vue +2 -2
  21. package/dist/resources/components/nuxt/site-source/app/components/steps/Progress.vue +36 -0
  22. package/dist/resources/components/nuxt/site-source/app/components/steps/Showcase.vue +340 -0
  23. package/dist/resources/components/nuxt/site-source/app/components/theme/Menu.vue +4 -4
  24. package/dist/resources/components/nuxt/site-source/app/components/ui/Button.vue +28 -2
  25. package/dist/resources/components/nuxt/site-source/app/components/ui/GradientBorder.vue +5 -5
  26. package/dist/resources/components/nuxt/site-source/app/components/ui/Icon.vue +14 -0
  27. package/dist/resources/components/nuxt/site-source/app/components/ui/Image.vue +126 -0
  28. package/dist/resources/components/nuxt/site-source/app/components/ui/Marquee.vue +99 -0
  29. package/dist/resources/components/nuxt/site-source/app/components/ui/Modal.vue +26 -88
  30. package/dist/resources/components/nuxt/site-source/app/components/ui/Popover.vue +87 -93
  31. package/dist/resources/components/nuxt/site-source/app/components/ui/Text.vue +84 -12
  32. package/dist/resources/components/nuxt/site-source/app/components/ui/Video.vue +105 -0
  33. package/dist/resources/components/nuxt/site-source/app/components/whatsapp/Chat.vue +126 -0
  34. package/dist/resources/components/nuxt/site-source/app/components/youtube/Embed.vue +286 -73
  35. package/dist/resources/components/nuxt/site-source/app/composables/useFloatingBottomStack.ts +44 -0
  36. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +6 -8
  37. package/dist/resources/components/nuxt/site-source/app/consent/types.ts +1 -4
  38. package/dist/resources/components/nuxt/site-source/app/content/types.ts +3 -0
  39. package/dist/resources/components/nuxt/site-source/app/theme/accordion.ts +5 -4
  40. package/dist/resources/components/nuxt/site-source/app/theme/breadcrumb.ts +14 -4
  41. package/dist/resources/components/nuxt/site-source/app/theme/button.ts +138 -28
  42. package/dist/resources/components/nuxt/site-source/app/theme/carousel.ts +11 -7
  43. package/dist/resources/components/nuxt/site-source/app/theme/checkbox.ts +9 -3
  44. package/dist/resources/components/nuxt/site-source/app/theme/drawer.ts +6 -0
  45. package/dist/resources/components/nuxt/site-source/app/theme/form-field.ts +8 -0
  46. package/dist/resources/components/nuxt/site-source/app/theme/icon.ts +6 -0
  47. package/dist/resources/components/nuxt/site-source/app/theme/index.ts +8 -1
  48. package/dist/resources/components/nuxt/site-source/app/theme/input.ts +34 -18
  49. package/dist/resources/components/nuxt/site-source/app/theme/modal.ts +9 -5
  50. package/dist/resources/components/nuxt/site-source/app/theme/pagination.ts +9 -2
  51. package/dist/resources/components/nuxt/site-source/app/theme/radio-group.ts +7 -0
  52. package/dist/resources/components/nuxt/site-source/app/theme/select.ts +19 -7
  53. package/dist/resources/components/nuxt/site-source/app/theme/stepper.ts +15 -6
  54. package/dist/resources/components/nuxt/site-source/app/theme/textarea.ts +6 -0
  55. package/dist/resources/components/nuxt/site-source/app/theme/toast.ts +1 -4
  56. package/dist/resources/components/nuxt/site-source/app/theme/tooltip.ts +5 -1
  57. package/dist/resources/components/registry.json +133 -45
  58. package/dist/resources/components/vue/ui/BottomSheet.vue +5 -1
  59. package/dist/resources/components/vue/ui/ScrollArea.vue +2 -1
  60. package/dist/resources/registry.json +4 -4
  61. package/dist/resources/templates/base/.claude/docs/architecture-template.md +2 -1
  62. package/dist/resources/templates/base/AGENTS.md +7 -0
  63. package/dist/resources/templates/base/CLAUDE.md +7 -0
  64. package/dist/resources/templates/nuxt/.claude/docs/stack.md +14 -1
  65. package/dist/resources/templates/nuxt/.vscode/extensions.json +9 -0
  66. package/dist/resources/templates/nuxt/.vscode/settings.json +21 -0
  67. package/dist/resources/templates/nuxt/.zed/settings.json +42 -0
  68. package/dist/resources/templates/nuxt/DESIGN.md +146 -0
  69. package/dist/resources/templates/nuxt/README.md +10 -0
  70. package/dist/resources/templates/nuxt/app/app.config.ts +2 -0
  71. package/dist/resources/templates/nuxt/app/pages/app/index.vue +9 -7
  72. package/dist/resources/templates/nuxt/app/pages/index.vue +12 -27
  73. package/dist/resources/templates/nuxt/app/pages/ui/index.vue +219 -3
  74. package/dist/resources/templates/nuxt/nuxt.config.ts +2 -4
  75. package/dist/resources/templates/nuxt/package.json +4 -3
  76. package/dist/resources/templates/nuxt/stylelint.config.mjs +12 -0
  77. package/package.json +2 -2
  78. package/dist/resources/components/nuxt/site-source/app/assets/icons/common/.gitkeep +0 -0
@@ -7,83 +7,170 @@ const props = withDefaults(
7
7
  privacyEnhanced?: boolean;
8
8
  controls?: boolean;
9
9
  autoplay?: boolean;
10
+ mute?: boolean;
11
+ loop?: boolean;
10
12
  start?: number;
11
13
  playlist?: string;
14
+ lazy?: boolean;
15
+ thumbnailQuality?: string;
16
+ thumbnailSrc?: string;
17
+ ccLangPref?: string;
18
+ language?: string;
19
+ rel?: boolean;
12
20
  }>(),
13
21
  {
14
22
  title: "Embedded video",
15
- aspectRatio: "16 / 9",
23
+ aspectRatio: "16:9",
16
24
  privacyEnhanced: undefined,
17
25
  controls: true,
18
- autoplay: false,
19
- start: undefined,
20
- playlist: "",
26
+ autoplay: true,
27
+ mute: true,
28
+ loop: false,
29
+ start: 0,
30
+ lazy: true,
31
+ thumbnailQuality: "hqdefault",
32
+ thumbnailSrc: "",
33
+ ccLangPref: "",
34
+ rel: false,
21
35
  },
22
36
  );
23
-
24
37
  const config = useRuntimeConfig();
25
-
38
+ const { locale, locales } = useI18n();
26
39
  function extractYoutubeId(source: string): string {
27
- if (/^[a-zA-Z0-9_-]{11}$/.test(source)) {
28
- return source;
29
- }
30
-
40
+ if (/^[a-zA-Z0-9_-]{11}$/.test(source)) return source;
31
41
  try {
32
- const url = new URL(source, "https://www.youtube.com");
33
- if (url.hostname === "youtu.be") {
34
- return url.pathname.replace("/", "").slice(0, 11);
35
- }
36
- if (url.pathname.startsWith("/embed/") || url.pathname.startsWith("/shorts/")) {
37
- return url.pathname.split("/")[2]?.slice(0, 11) || "";
42
+ const url = new URL(source);
43
+ let id = "";
44
+ if (url.hostname === "youtu.be") id = url.pathname.slice(1);
45
+ else if (
46
+ ["youtube.com", "www.youtube.com", "m.youtube.com", "www.youtube-nocookie.com"].includes(
47
+ url.hostname,
48
+ )
49
+ ) {
50
+ id =
51
+ url.pathname.startsWith("/embed/") || url.pathname.startsWith("/shorts/")
52
+ ? (url.pathname.split("/")[2] ?? "")
53
+ : (url.searchParams.get("v") ?? "");
38
54
  }
39
- return url.searchParams.get("v")?.slice(0, 11) || "";
55
+ return /^[a-zA-Z0-9_-]{11}$/.test(id) ? id : "";
40
56
  } catch {
41
57
  return "";
42
58
  }
43
59
  }
44
-
45
60
  const videoId = computed(() => extractYoutubeId(props.source));
46
- const usePrivacyHost = computed(() => {
47
- if (typeof props.privacyEnhanced === "boolean") {
48
- return props.privacyEnhanced;
49
- }
50
- return config.public.media?.youtube?.privacyEnhanced !== false;
61
+ const isActivated = ref(!props.lazy);
62
+ const isIframeLoaded = ref(false);
63
+ const hasPreconnected = ref(false);
64
+ const fallbackTitle = computed(() => props.title || "YouTube video player");
65
+ const ariaLabel = computed(() => "Play " + fallbackTitle.value);
66
+ const thumbnailUrl = computed(() => {
67
+ if (props.thumbnailSrc) return props.thumbnailSrc;
68
+ const webp = props.thumbnailQuality.includes("webp");
69
+ return (
70
+ "https://i.ytimg.com/" +
71
+ (webp ? "vi_webp/" : "vi/") +
72
+ videoId.value +
73
+ "/" +
74
+ props.thumbnailQuality.replace("_webp", "") +
75
+ (webp ? ".webp" : ".jpg")
76
+ );
51
77
  });
52
-
53
- const embedSrc = computed(() => {
54
- if (!videoId.value) {
55
- return "";
56
- }
57
-
58
- const host = usePrivacyHost.value ? "www.youtube-nocookie.com" : "www.youtube.com";
59
- const url = new URL(`https://${host}/embed/${videoId.value}`);
60
- url.searchParams.set("playsinline", "1");
61
- url.searchParams.set("rel", "0");
62
- url.searchParams.set("controls", props.controls ? "1" : "0");
63
-
64
- if (props.autoplay) {
65
- url.searchParams.set("autoplay", "1");
66
- url.searchParams.set("mute", "1");
67
- }
68
- if (Number.isInteger(props.start) && Number(props.start) > 0) {
69
- url.searchParams.set("start", String(props.start));
70
- }
71
- if (props.playlist) {
72
- url.searchParams.set("playlist", props.playlist);
73
- }
74
-
78
+ const calculateAspectRatio = computed(() => {
79
+ const [width, height] = props.aspectRatio.split(/[:/]/).map(Number);
80
+ return width && height ? (height / width) * 100 : 56.25;
81
+ });
82
+ const youtubeLocale = computed(
83
+ () =>
84
+ props.language ??
85
+ locales.value.find((item) => item.code === locale.value)?.language ??
86
+ locale.value,
87
+ );
88
+ const youtubeEmbedUrl = computed(() => {
89
+ const host =
90
+ (props.privacyEnhanced ?? config.public.media?.youtube?.privacyEnhanced !== false)
91
+ ? "www.youtube-nocookie.com"
92
+ : "www.youtube.com";
93
+ const url = new URL("https://" + host + "/embed/" + videoId.value);
94
+ const params = {
95
+ playsinline: "1",
96
+ autoplay: props.autoplay ? "1" : "0",
97
+ controls: props.controls ? "1" : "0",
98
+ mute: props.mute ? "1" : "0",
99
+ loop: props.loop ? "1" : "0",
100
+ rel: props.rel ? "1" : "0",
101
+ hl: youtubeLocale.value,
102
+ cc_lang_pref: props.ccLangPref || youtubeLocale.value,
103
+ };
104
+ for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
105
+ if (props.start > 0) url.searchParams.set("start", String(props.start));
106
+ if (props.playlist || props.loop)
107
+ url.searchParams.set("playlist", props.playlist || videoId.value);
75
108
  return url.toString();
76
109
  });
110
+ useHead(() => ({
111
+ link: hasPreconnected.value
112
+ ? [
113
+ "https://www.youtube-nocookie.com",
114
+ "https://www.youtube.com",
115
+ "https://www.gstatic.com",
116
+ ].map((href) => ({ rel: "preconnect", href }))
117
+ : [],
118
+ }));
119
+ function warmConnections() {
120
+ hasPreconnected.value = true;
121
+ }
122
+ const { start: startLoadTimer, stop: stopLoadTimer } = useTimeoutFn(
123
+ () => {
124
+ if (!isIframeLoaded.value) isActivated.value = false;
125
+ },
126
+ 8000,
127
+ { immediate: false },
128
+ );
129
+ function activatePlayer() {
130
+ if (isActivated.value || !videoId.value) return;
131
+ warmConnections();
132
+ isActivated.value = true;
133
+ isIframeLoaded.value = false;
134
+ startLoadTimer();
135
+ }
136
+ function handleIframeLoad() {
137
+ stopLoadTimer();
138
+ isIframeLoaded.value = true;
139
+ }
140
+ watch([() => props.source, () => props.lazy], () => {
141
+ stopLoadTimer();
142
+ isActivated.value = false;
143
+ isIframeLoaded.value = false;
144
+ if (!props.lazy) activatePlayer();
145
+ });
146
+ onMounted(() => {
147
+ if (!props.lazy) startLoadTimer();
148
+ });
77
149
  </script>
78
150
 
79
151
  <template>
80
- <figure class="youtube-embed" :style="{ '--youtube-embed-ratio': aspectRatio }">
152
+ <div
153
+ class="youtube-video-container"
154
+ :class="{
155
+ 'fixed-aspect-ratio': aspectRatio !== 'auto',
156
+ 'is-activated': isActivated,
157
+ }"
158
+ :style="aspectRatio !== 'auto' ? { paddingBottom: `${calculateAspectRatio}%` } : {}"
159
+ >
160
+ <div
161
+ v-if="!videoId"
162
+ class="absolute inset-0 grid place-items-center bg-surface-muted p-4"
163
+ role="alert"
164
+ >
165
+ <UiText tone="danger" size="sm">Invalid YouTube source.</UiText>
166
+ </div>
81
167
  <iframe
82
- v-if="embedSrc"
83
- class="youtube-embed__frame"
84
- :src="embedSrc"
85
- :title="title"
86
- loading="lazy"
168
+ v-if="isActivated && videoId"
169
+ key="youtube-iframe"
170
+ class="youtube-iframe"
171
+ :src="youtubeEmbedUrl"
172
+ :title="title || fallbackTitle"
173
+ :loading="lazy ? 'lazy' : 'eager'"
87
174
  allow="
88
175
  accelerometer;
89
176
  autoplay;
@@ -95,40 +182,166 @@ const embedSrc = computed(() => {
95
182
  "
96
183
  referrerpolicy="strict-origin-when-cross-origin"
97
184
  allowfullscreen
98
- />
99
- <div v-else class="youtube-embed__fallback">
100
- <UiText tone="danger" size="sm">Invalid YouTube source.</UiText>
101
- </div>
102
- </figure>
185
+ @load="handleIframeLoad"
186
+ ></iframe>
187
+
188
+ <button
189
+ v-if="!isIframeLoaded && videoId"
190
+ key="youtube-facade"
191
+ role="button"
192
+ class="youtube-facade"
193
+ :class="{ 'youtube-facade--loading': isActivated }"
194
+ :aria-label="ariaLabel"
195
+ :disabled="isActivated"
196
+ @click="activatePlayer"
197
+ @pointerover="warmConnections"
198
+ @focus="warmConnections"
199
+ >
200
+ <UiImage
201
+ provider="none"
202
+ :viewport-load="false"
203
+ class="youtube-thumbnail"
204
+ :src="thumbnailUrl"
205
+ :alt="title || fallbackTitle"
206
+ width="1280"
207
+ height="720"
208
+ loading="lazy"
209
+ draggable="false"
210
+ />
211
+ <span class="youtube-facade-overlay" aria-hidden="true"></span>
212
+ <span v-if="!isActivated" class="youtube-facade-play" aria-hidden="true">
213
+ <svg height="100%" version="1.1" viewBox="0 0 68 48" width="100%">
214
+ <path
215
+ class="ytp-large-play-button-bg"
216
+ d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z"
217
+ fill="#f03"
218
+ ></path>
219
+ <path d="M 45,24 27,14 27,34" fill="#fff"></path>
220
+ </svg>
221
+ </span>
222
+ <span v-else class="youtube-loading-spinner" aria-hidden="true"></span>
223
+ <span class="sr-only">{{ ariaLabel }}</span>
224
+ </button>
225
+ </div>
103
226
  </template>
104
227
 
105
228
  <style scoped>
106
- .youtube-embed {
229
+ .youtube-video-container {
230
+ position: relative;
107
231
  width: 100%;
108
- min-width: 200px;
109
- margin: 0;
110
232
  overflow: hidden;
111
- border: 1px solid var(--site-color-border);
112
- border-radius: 12px;
113
- background: var(--site-color-surface-muted);
114
- aspect-ratio: var(--youtube-embed-ratio);
115
233
  }
116
234
 
117
- .youtube-embed__frame,
118
- .youtube-embed__fallback {
235
+ .fixed-aspect-ratio {
236
+ height: 0;
237
+ }
238
+
239
+ .youtube-video-container:not(.fixed-aspect-ratio) {
240
+ height: 400px;
241
+ }
242
+
243
+ .youtube-iframe {
244
+ position: absolute;
245
+ inset: 0;
119
246
  width: 100%;
120
247
  height: 100%;
248
+ border: 0;
121
249
  }
122
250
 
123
- .youtube-embed__frame {
124
- display: block;
251
+ .youtube-facade {
252
+ position: absolute;
253
+ inset: 0;
254
+ width: 100%;
255
+ height: 100%;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
125
259
  border: 0;
260
+ border-radius: 0;
261
+ padding: 0;
262
+ cursor: pointer;
263
+ color: inherit;
264
+ }
265
+
266
+ .youtube-facade:focus-visible {
267
+ outline: 2px solid rgb(255 255 255 / 80%);
268
+ outline-offset: -2px;
269
+ }
270
+
271
+ .youtube-facade--loading {
272
+ cursor: default;
273
+ pointer-events: none;
274
+ }
275
+
276
+ .youtube-thumbnail {
277
+ width: 100%;
278
+ height: 100%;
279
+ object-fit: cover;
280
+ transition: transform 0.3s ease;
281
+ }
282
+
283
+ .youtube-facade:hover .youtube-thumbnail {
284
+ transform: scale(1.03);
285
+ }
286
+
287
+ .youtube-facade-overlay {
288
+ position: absolute;
289
+ inset: 0;
290
+ background: linear-gradient(180deg, rgb(0 0 0 / 25%) 0%, rgb(0 0 0 / 55%) 100%);
291
+ transition: background 0.3s ease;
292
+ display: none;
293
+ }
294
+
295
+ .youtube-facade--loading .youtube-facade-overlay,
296
+ .youtube-facade:hover .youtube-facade-overlay {
297
+ display: block;
298
+ background: linear-gradient(180deg, rgb(0 0 0 / 5%) 0%, rgb(0 0 0 / 45%) 100%);
126
299
  }
127
300
 
128
- .youtube-embed__fallback {
129
- display: grid;
130
- min-height: 200px;
131
- place-items: center;
132
- padding: 16px;
301
+ .youtube-facade-play {
302
+ position: absolute;
303
+ width: 68px;
304
+ height: 48px;
305
+ background: transparent;
306
+ }
307
+
308
+ .youtube-facade--loading .youtube-facade-play {
309
+ display: none;
310
+ }
311
+
312
+ .youtube-loading-spinner {
313
+ position: absolute;
314
+ width: 70px;
315
+ height: 70px;
316
+ border-radius: 50%;
317
+ border: 8px solid rgb(255 255 255 / 30%);
318
+ border-top-color: #fff;
319
+ animation: youtube-spinner 0.9s linear infinite;
320
+ }
321
+
322
+ @media (width <= 768px) {
323
+ .youtube-video-container:not(.fixed-aspect-ratio) {
324
+ height: 240px;
325
+ }
326
+ }
327
+
328
+ @media (width <= 540px) {
329
+ .youtube-facade-play {
330
+ width: 50px;
331
+ height: 30px;
332
+ }
333
+
334
+ .youtube-loading-spinner {
335
+ width: 40px;
336
+ height: 40px;
337
+ border: 4px solid rgb(255 255 255 / 30%);
338
+ border-top-color: #fff;
339
+ }
340
+ }
341
+
342
+ @keyframes youtube-spinner {
343
+ to {
344
+ transform: rotate(360deg);
345
+ }
133
346
  }
134
347
  </style>
@@ -0,0 +1,44 @@
1
+ interface FloatingLayer {
2
+ height: number;
3
+ visible: boolean;
4
+ order: number;
5
+ }
6
+
7
+ export function useFloatingBottomStack() {
8
+ const layers = useState<Record<string, FloatingLayer>>("site-floating-layers", () => ({}));
9
+ const ordered = computed(() =>
10
+ Object.entries(layers.value).sort(
11
+ ([a, first], [b, second]) => first.order - second.order || a.localeCompare(b),
12
+ ),
13
+ );
14
+ function setLayer(key: string, layer: FloatingLayer) {
15
+ const current = layers.value[key];
16
+ const height = Math.max(0, Math.ceil(layer.height));
17
+ if (
18
+ current?.height === height &&
19
+ current.visible === layer.visible &&
20
+ current.order === layer.order
21
+ )
22
+ return;
23
+ layers.value[key] = { ...layer, height };
24
+ }
25
+ function clearLayer(key: string) {
26
+ layers.value = Object.fromEntries(
27
+ Object.entries(layers.value).filter(([name]) => name !== key),
28
+ );
29
+ }
30
+ function getLayerBottom(key: string) {
31
+ return computed(() => {
32
+ let offset = 0;
33
+ for (const [name, layer] of ordered.value) {
34
+ if (name === key) break;
35
+ if (layer.visible) offset += layer.height;
36
+ }
37
+ return offset;
38
+ });
39
+ }
40
+ const dockedHeight = computed(() =>
41
+ ordered.value.reduce((sum, [, layer]) => sum + (layer.visible ? layer.height : 0), 0),
42
+ );
43
+ return { layers: readonly(layers), setLayer, clearLayer, getLayerBottom, dockedHeight };
44
+ }
@@ -20,14 +20,12 @@ export function useNavigation(): Navigation {
20
20
  appGroups: computed(() =>
21
21
  site.value.navigation.app.map((group) => ({
22
22
  ...(group.labelKey === undefined ? {} : { label: t(group.labelKey) }),
23
- items: group.items.map(
24
- (item): AppNavItem => ({
25
- label: t(item.labelKey),
26
- to: item.to,
27
- ...(item.icon === undefined ? {} : { icon: item.icon }),
28
- ...(item.badge === undefined ? {} : { badge: item.badge }),
29
- }),
30
- ),
23
+ items: group.items.map((item): AppNavItem => ({
24
+ label: t(item.labelKey),
25
+ to: item.to,
26
+ ...(item.icon === undefined ? {} : { icon: item.icon }),
27
+ ...(item.badge === undefined ? {} : { badge: item.badge }),
28
+ })),
31
29
  })),
32
30
  ),
33
31
  footerGroups: computed(() =>
@@ -19,10 +19,7 @@ export interface ConsentConfig {
19
19
  export type ConsentValue = "accepted" | "rejected";
20
20
 
21
21
  export type ConsentPersistenceError =
22
- | "read-failed"
23
- | "write-failed"
24
- | "clear-failed"
25
- | "write-and-clear-failed";
22
+ "read-failed" | "write-failed" | "clear-failed" | "write-and-clear-failed";
26
23
 
27
24
  export interface ConsentState {
28
25
  decision: ConsentValue | null;
@@ -5,6 +5,9 @@ export interface ListItem {
5
5
  readonly category?: string;
6
6
  readonly date?: string;
7
7
  readonly author?: string;
8
+ readonly image?: { readonly src: string; readonly alt: string };
9
+ readonly categoryTo?: string;
10
+ readonly dateTime?: string;
8
11
  }
9
12
 
10
13
  export interface Article {
@@ -1,9 +1,10 @@
1
1
  export default {
2
2
  slots: {
3
- root: "divide-y divide-border",
4
- item: "py-1",
3
+ root: "space-y-7.5 cursor-pointer max-xl:space-y-4.5 max-md:!space-y-2.5",
4
+ item: "w-full py-7 pl-6 pr-9 max-md:py-4 max-md:pl-4 max-md:pr-9 bg-surface rounded-xl shadow-accordion overflow-hidden transition-all duration-300 border-none",
5
5
  trigger:
6
- "rounded-md px-2 py-3 text-left text-sm font-semibold text-text hover:bg-surface-muted",
7
- content: "px-2 pb-3 text-sm leading-6 text-muted",
6
+ "p-0 flex items-center justify-between gap-2.5 min-h-12 transition-all duration-300 cursor-pointer",
7
+ content: "transition-all duration-300 overflow-hidden",
8
+ body: "p-0 mt-4 text-muted max-md:mt-2.5",
8
9
  },
9
10
  };
@@ -1,8 +1,18 @@
1
1
  export default {
2
2
  slots: {
3
- root: "text-sm",
4
- list: "gap-1.5",
5
- link: "text-muted hover:text-text",
6
- separator: "text-dimmed",
3
+ root: "relative min-w-0",
4
+ list: "flex items-center gap-1.5 max-md:flex-wrap",
5
+ linkLabel: "text-lg",
6
+ separator: "flex text-lg",
7
+ },
8
+ variants: {
9
+ active: {
10
+ true: {
11
+ link: "text-text font-normal",
12
+ },
13
+ false: {
14
+ link: "text-dimmed font-normal",
15
+ },
16
+ },
7
17
  },
8
18
  };