@stackonward/cli 1.2.1 → 1.3.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.
Files changed (36) hide show
  1. package/dist/index.js +17 -10
  2. package/dist/index.js.map +1 -1
  3. package/dist/resources/components/nuxt/site-source/app/app.vue +9 -6
  4. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +268 -33
  5. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationHeader.vue +7 -4
  6. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationShell.vue +1 -1
  7. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationSidebar.vue +4 -3
  8. package/dist/resources/components/nuxt/site-source/app/components/site/BackToTop.vue +4 -12
  9. package/dist/resources/components/nuxt/site-source/app/components/site/ConsentSettingsButton.vue +24 -0
  10. package/dist/resources/components/nuxt/site-source/app/components/site/ContentArticlePage.vue +18 -6
  11. package/dist/resources/components/nuxt/site-source/app/components/site/ContentListPage.vue +2 -2
  12. package/dist/resources/components/nuxt/site-source/app/components/site/CookieConsent.vue +44 -4
  13. package/dist/resources/components/nuxt/site-source/app/components/site/LocaleSwitcher.vue +57 -0
  14. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingFooter.vue +11 -9
  15. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingHeader.vue +31 -19
  16. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +9 -9
  17. package/dist/resources/components/nuxt/site-source/app/components/site/SiteBrand.vue +3 -2
  18. package/dist/resources/components/nuxt/site-source/app/components/site/StaticPage.vue +2 -1
  19. package/dist/resources/components/nuxt/site-source/app/components/site/ThemeSwitcher.vue +63 -0
  20. package/dist/resources/components/nuxt/site-source/app/components/site/ToastViewport.vue +3 -2
  21. package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +299 -51
  22. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +49 -6
  23. package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +4 -2
  24. package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +29 -11
  25. package/dist/resources/components/nuxt/site-source/app/plugins/consent.ts +43 -0
  26. package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +146 -59
  27. package/dist/resources/components/registry.json +30 -0
  28. package/dist/resources/registry.json +3 -3
  29. package/dist/resources/templates/nuxt/app/app.config.ts +83 -38
  30. package/dist/resources/templates/nuxt/app/i18n/locales/en.json +124 -0
  31. package/dist/resources/templates/nuxt/app/pages/legal/cookies.vue +68 -0
  32. package/dist/resources/templates/nuxt/nuxt.config.ts +16 -0
  33. package/dist/resources/templates/nuxt/package.json +2 -2
  34. package/dist/resources/templates/nuxt/test/unit/consent.test.ts +289 -0
  35. package/dist/resources/templates/nuxt/vitest.config.ts +6 -0
  36. package/package.json +3 -3
@@ -1,16 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  const site = useSiteShell();
3
+ const { localeProperties, t } = useI18n();
4
+ const localeHead = useLocaleHead({ seo: true });
3
5
 
4
6
  useHead(() => ({
5
- htmlAttrs: {
6
- lang: site.value.defaultLocale,
7
- },
7
+ htmlAttrs: localeHead.value.htmlAttrs,
8
8
  titleTemplate: (title?: string) => (title ? `${title} | ${site.value.name}` : site.value.name),
9
9
  meta: [
10
- { name: "description", content: site.value.description },
10
+ ...(localeHead.value.meta || []),
11
+ { name: "description", content: t(site.value.descriptionKey) },
11
12
  { property: "og:site_name", content: site.value.name },
12
13
  { property: "og:type", content: "website" },
13
14
  ],
15
+ link: localeHead.value.link || [],
14
16
  script: site.value.url
15
17
  ? [
16
18
  {
@@ -20,8 +22,8 @@ useHead(() => ({
20
22
  "@type": "WebSite",
21
23
  name: site.value.name,
22
24
  url: site.value.url,
23
- description: site.value.description,
24
- inLanguage: site.value.defaultLocale,
25
+ description: t(site.value.descriptionKey),
26
+ inLanguage: localeProperties.value.language || site.value.defaultLocale,
25
27
  }),
26
28
  },
27
29
  ]
@@ -32,6 +34,7 @@ useHead(() => ({
32
34
  <template>
33
35
  <UApp>
34
36
  <div class="site-root">
37
+ <a class="skip-link" href="#site-content">{{ t("site.skipToContent") }}</a>
35
38
  <NuxtLoadingIndicator color="var(--site-color-primary)" />
36
39
  <NuxtLayout>
37
40
  <NuxtPage />
@@ -90,6 +90,24 @@
90
90
  color-scheme: light;
91
91
  }
92
92
 
93
+ .dark {
94
+ --site-color-bg: #0b0f14;
95
+ --site-color-bg-soft: #101720;
96
+ --site-color-surface: #121922;
97
+ --site-color-surface-muted: #18212c;
98
+ --site-color-surface-elevated: #202c38;
99
+ --site-color-text: #f5f7fa;
100
+ --site-color-muted: #aab6c3;
101
+ --site-color-dimmed: #7f8c99;
102
+ --site-color-border: #2b3947;
103
+ --site-color-border-strong: #405163;
104
+ --site-shadow-soft: 0 18px 44px rgb(0 0 0 / 34%);
105
+ --site-shadow-strong: 0 30px 70px rgb(0 0 0 / 46%);
106
+ --ui-border-muted: #202c38;
107
+
108
+ color-scheme: dark;
109
+ }
110
+
93
111
  * {
94
112
  box-sizing: border-box;
95
113
  }
@@ -137,6 +155,24 @@ summary {
137
155
  min-height: 100dvh;
138
156
  }
139
157
 
158
+ .skip-link {
159
+ position: fixed;
160
+ top: 12px;
161
+ left: 12px;
162
+ z-index: 100;
163
+ min-height: 44px;
164
+ border-radius: var(--site-radius-md);
165
+ background: var(--site-color-surface);
166
+ padding: 11px 16px;
167
+ color: var(--site-color-text);
168
+ box-shadow: var(--site-shadow-soft);
169
+ transform: translateY(calc(-100% - 20px));
170
+ }
171
+
172
+ .skip-link:focus {
173
+ transform: translateY(0);
174
+ }
175
+
140
176
  .base-button,
141
177
  .icon-button {
142
178
  display: inline-flex;
@@ -178,7 +214,7 @@ summary {
178
214
 
179
215
  .base-button--secondary {
180
216
  border-color: rgb(168 101 80 / 34%);
181
- background: rgb(255 255 255 / 82%);
217
+ background: color-mix(in srgb, var(--site-color-surface) 82%, transparent);
182
218
  color: var(--site-color-primary);
183
219
  }
184
220
 
@@ -193,7 +229,7 @@ summary {
193
229
  height: 44px;
194
230
  border-color: rgb(168 101 80 / 22%);
195
231
  border-radius: var(--site-radius-md);
196
- background: rgb(255 255 255 / 78%);
232
+ background: color-mix(in srgb, var(--site-color-surface) 78%, transparent);
197
233
  color: var(--site-color-primary);
198
234
  }
199
235
 
@@ -251,7 +287,7 @@ summary {
251
287
  top: 0;
252
288
  z-index: 40;
253
289
  border-bottom: 1px solid rgb(219 227 232 / 82%);
254
- background: rgb(255 255 255 / 88%);
290
+ background: color-mix(in srgb, var(--site-color-surface) 88%, transparent);
255
291
  backdrop-filter: blur(18px);
256
292
  }
257
293
 
@@ -316,7 +352,7 @@ summary {
316
352
  gap: 6px;
317
353
  border: 1px solid var(--site-color-border);
318
354
  border-radius: var(--site-radius-lg);
319
- background: rgb(255 255 255 / 98%);
355
+ background: color-mix(in srgb, var(--site-color-surface) 98%, transparent);
320
356
  padding: 8px;
321
357
  box-shadow: var(--site-shadow-soft);
322
358
  transform: translateX(-50%);
@@ -345,7 +381,91 @@ summary {
345
381
  }
346
382
 
347
383
  .marketing-header__actions > .base-button--secondary {
348
- background: rgb(255 255 255 / 78%);
384
+ background: color-mix(in srgb, var(--site-color-surface) 78%, transparent);
385
+ }
386
+
387
+ .marketing-header__preferences,
388
+ .mobile-nav__preferences {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 8px;
392
+ }
393
+
394
+ .site-preference {
395
+ position: relative;
396
+ }
397
+
398
+ .site-preference > summary {
399
+ display: inline-flex;
400
+ min-height: 44px;
401
+ align-items: center;
402
+ gap: 7px;
403
+ border: 1px solid var(--site-color-border);
404
+ border-radius: var(--site-radius-md);
405
+ background: color-mix(in srgb, var(--site-color-surface) 84%, transparent);
406
+ padding: 0 10px;
407
+ color: var(--site-color-text);
408
+ cursor: pointer;
409
+ font-size: 14px;
410
+ font-weight: 600;
411
+ list-style: none;
412
+ }
413
+
414
+ .site-preference > summary::-webkit-details-marker {
415
+ display: none;
416
+ }
417
+
418
+ .site-preference[open] > summary,
419
+ .site-preference > summary:hover {
420
+ border-color: var(--site-color-border-strong);
421
+ background: var(--site-color-surface-muted);
422
+ }
423
+
424
+ .site-preference__chevron {
425
+ transition: transform var(--site-transition-fast);
426
+ }
427
+
428
+ .site-preference[open] .site-preference__chevron {
429
+ transform: rotate(180deg);
430
+ }
431
+
432
+ .site-preference__menu {
433
+ position: absolute;
434
+ top: calc(100% + 8px);
435
+ right: 0;
436
+ z-index: 60;
437
+ display: grid;
438
+ width: max-content;
439
+ min-width: 180px;
440
+ gap: 4px;
441
+ border: 1px solid var(--site-color-border);
442
+ border-radius: var(--site-radius-lg);
443
+ background: var(--site-color-surface);
444
+ padding: 6px;
445
+ box-shadow: var(--site-shadow-soft);
446
+ }
447
+
448
+ .site-preference__menu button {
449
+ display: flex;
450
+ min-height: 44px;
451
+ align-items: center;
452
+ gap: 8px;
453
+ border: 0;
454
+ border-radius: var(--site-radius-md);
455
+ background: transparent;
456
+ padding: 0 10px;
457
+ color: var(--site-color-text);
458
+ cursor: pointer;
459
+ text-align: start;
460
+ }
461
+
462
+ .site-preference__menu button:hover,
463
+ .site-preference__menu button[aria-pressed="true"] {
464
+ background: var(--site-color-surface-muted);
465
+ }
466
+
467
+ .site-preference__check {
468
+ margin-inline-start: auto;
349
469
  }
350
470
 
351
471
  .marketing-header__menu-button {
@@ -358,7 +478,7 @@ summary {
358
478
 
359
479
  .marketing-footer {
360
480
  border-top: 1px solid var(--site-color-border);
361
- background: linear-gradient(180deg, var(--site-color-surface), #edf5f7), var(--site-color-bg);
481
+ background: linear-gradient(180deg, var(--site-color-surface), var(--site-color-bg-soft));
362
482
  color: var(--site-color-text);
363
483
  }
364
484
 
@@ -397,15 +517,25 @@ summary {
397
517
  }
398
518
 
399
519
  .marketing-footer__social a,
400
- .marketing-footer__legal a {
520
+ .marketing-footer__legal a,
521
+ .consent-settings-button {
401
522
  color: var(--site-color-muted);
402
523
  }
403
524
 
404
525
  .marketing-footer__social a:hover,
405
- .marketing-footer__legal a:hover {
526
+ .marketing-footer__legal a:hover,
527
+ .consent-settings-button:hover {
406
528
  color: var(--site-color-accent);
407
529
  }
408
530
 
531
+ .consent-settings-button {
532
+ min-height: 44px;
533
+ border: 0;
534
+ background: transparent;
535
+ padding: 0;
536
+ cursor: pointer;
537
+ }
538
+
409
539
  .marketing-footer__nav {
410
540
  display: grid;
411
541
  grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -450,9 +580,6 @@ summary {
450
580
  }
451
581
 
452
582
  .back-to-top {
453
- position: fixed;
454
- right: 24px;
455
- z-index: 45;
456
583
  display: grid;
457
584
  width: 44px;
458
585
  height: 44px;
@@ -572,19 +699,60 @@ summary {
572
699
  }
573
700
 
574
701
  .site-consent {
575
- position: fixed;
576
- right: 24px;
577
- bottom: 24px;
578
- z-index: 50;
579
702
  display: grid;
580
- width: calc(100vw - 32px);
581
- max-width: 430px;
703
+ width: 100%;
704
+ max-height: min(70dvh, 560px);
582
705
  gap: 14px;
583
706
  border: 1px solid var(--site-color-border);
584
707
  border-radius: var(--site-radius-lg);
585
- background: rgb(255 255 255 / 96%);
708
+ background: color-mix(in srgb, var(--site-color-surface) 96%, transparent);
586
709
  padding: 16px;
587
710
  box-shadow: var(--site-shadow-strong);
711
+ overflow-y: auto;
712
+ }
713
+
714
+ .site-floating-stack {
715
+ position: fixed;
716
+ inset-block-end: max(24px, env(safe-area-inset-bottom));
717
+ inset-inline-end: max(24px, env(safe-area-inset-right));
718
+ z-index: 50;
719
+ display: flex;
720
+ width: min(430px, calc(100vw - 48px));
721
+ max-height: calc(100dvh - 48px - env(safe-area-inset-bottom));
722
+ align-items: flex-end;
723
+ flex-direction: column;
724
+ gap: 12px;
725
+ pointer-events: none;
726
+ }
727
+
728
+ .site-floating-stack > * {
729
+ pointer-events: auto;
730
+ }
731
+
732
+ .site-consent__header {
733
+ display: flex;
734
+ align-items: flex-start;
735
+ justify-content: space-between;
736
+ gap: 12px;
737
+ }
738
+
739
+ .site-consent__header h2 {
740
+ margin: 0;
741
+ color: var(--site-color-text);
742
+ font-size: 18px;
743
+ }
744
+
745
+ .site-consent__close {
746
+ display: grid;
747
+ width: 44px;
748
+ height: 44px;
749
+ flex: 0 0 auto;
750
+ place-items: center;
751
+ border: 0;
752
+ border-radius: 50%;
753
+ background: var(--site-color-surface-muted);
754
+ color: var(--site-color-text);
755
+ cursor: pointer;
588
756
  }
589
757
 
590
758
  .site-consent__message {
@@ -593,6 +761,23 @@ summary {
593
761
  line-height: 1.55;
594
762
  }
595
763
 
764
+ .site-consent__policy {
765
+ width: fit-content;
766
+ color: var(--site-color-primary);
767
+ font-weight: 650;
768
+ text-decoration: underline;
769
+ text-underline-offset: 3px;
770
+ }
771
+
772
+ .site-consent__status {
773
+ margin: 0;
774
+ border-inline-start: 3px solid var(--site-color-warning);
775
+ padding-inline-start: 10px;
776
+ color: var(--site-color-muted);
777
+ font-size: 14px;
778
+ line-height: 1.5;
779
+ }
780
+
596
781
  .site-consent__actions {
597
782
  display: flex;
598
783
  flex-wrap: wrap;
@@ -600,14 +785,6 @@ summary {
600
785
  gap: 10px;
601
786
  }
602
787
 
603
- @media (width <= 520px) {
604
- .site-consent {
605
- right: 16px;
606
- left: 16px;
607
- width: auto;
608
- }
609
- }
610
-
611
788
  .application-shell {
612
789
  height: 100dvh;
613
790
  min-height: 0;
@@ -625,7 +802,7 @@ summary {
625
802
  align-items: center;
626
803
  gap: 16px;
627
804
  border-bottom: 1px solid var(--site-color-border);
628
- background: rgb(255 255 255 / 90%);
805
+ background: color-mix(in srgb, var(--site-color-surface) 90%, transparent);
629
806
  padding: 0 20px;
630
807
  backdrop-filter: blur(16px);
631
808
  }
@@ -851,6 +1028,38 @@ summary {
851
1028
  font-weight: 450;
852
1029
  }
853
1030
 
1031
+ .storage-inventory {
1032
+ width: 100%;
1033
+ margin: 20px 0;
1034
+ overflow-x: auto;
1035
+ }
1036
+
1037
+ .storage-inventory table {
1038
+ width: 100%;
1039
+ min-width: 720px;
1040
+ border-collapse: collapse;
1041
+ color: var(--site-color-muted);
1042
+ font-size: 14px;
1043
+ }
1044
+
1045
+ .storage-inventory th,
1046
+ .storage-inventory td {
1047
+ border-bottom: 1px solid var(--site-color-border);
1048
+ padding: 12px;
1049
+ text-align: start;
1050
+ vertical-align: top;
1051
+ }
1052
+
1053
+ .storage-inventory thead th {
1054
+ color: var(--site-color-text);
1055
+ font-weight: 650;
1056
+ }
1057
+
1058
+ .storage-inventory code {
1059
+ color: var(--site-color-text);
1060
+ white-space: nowrap;
1061
+ }
1062
+
854
1063
  @media (width <= 860px) {
855
1064
  :root {
856
1065
  --site-shell-header-height: 64px;
@@ -870,6 +1079,7 @@ summary {
870
1079
  }
871
1080
 
872
1081
  .marketing-header__nav,
1082
+ .marketing-header__preferences,
873
1083
  .marketing-header__actions > .base-button {
874
1084
  display: none;
875
1085
  }
@@ -886,7 +1096,7 @@ summary {
886
1096
  display: none;
887
1097
  width: min(100%, 100vw);
888
1098
  border-top: 1px solid var(--site-color-border);
889
- background: rgb(255 255 255 / 98%);
1099
+ background: color-mix(in srgb, var(--site-color-surface) 98%, transparent);
890
1100
  padding: 10px 12px 18px;
891
1101
  }
892
1102
 
@@ -915,6 +1125,25 @@ summary {
915
1125
  cursor: pointer;
916
1126
  }
917
1127
 
1128
+ .mobile-nav__preferences {
1129
+ padding: 8px 12px 0;
1130
+ }
1131
+
1132
+ .mobile-nav__preferences .site-preference {
1133
+ flex: 1;
1134
+ }
1135
+
1136
+ .mobile-nav__preferences .site-preference > summary {
1137
+ width: 100%;
1138
+ justify-content: center;
1139
+ }
1140
+
1141
+ .mobile-nav__preferences .site-preference__menu {
1142
+ right: auto;
1143
+ left: 0;
1144
+ width: 100%;
1145
+ }
1146
+
918
1147
  .marketing-footer__inner,
919
1148
  .marketing-footer__nav,
920
1149
  .content-list__grid {
@@ -998,11 +1227,6 @@ summary {
998
1227
  .content-article__related > h2 {
999
1228
  width: min(100% - 32px, 960px);
1000
1229
  }
1001
-
1002
- .site-consent {
1003
- right: 16px;
1004
- bottom: 102px;
1005
- }
1006
1230
  }
1007
1231
 
1008
1232
  @media (width <= 520px) {
@@ -1022,6 +1246,17 @@ summary {
1022
1246
  .site-consent__actions .base-button {
1023
1247
  flex: 1;
1024
1248
  }
1249
+
1250
+ .site-floating-stack {
1251
+ inset-block-end: max(16px, env(safe-area-inset-bottom));
1252
+ inset-inline: 16px;
1253
+ width: auto;
1254
+ max-height: calc(100dvh - 32px - env(safe-area-inset-bottom));
1255
+ }
1256
+
1257
+ .site-preference__current {
1258
+ display: none;
1259
+ }
1025
1260
  }
1026
1261
 
1027
1262
  @media (prefers-reduced-motion: reduce) {
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  const site = useSiteShell();
3
+ const { t } = useI18n();
3
4
 
4
5
  defineEmits<{
5
6
  toggleSidebar: [];
@@ -12,7 +13,7 @@ defineEmits<{
12
13
  <button
13
14
  type="button"
14
15
  class="icon-button application-header__menu-button"
15
- aria-label="Toggle sidebar"
16
+ :aria-label="t('navigation.toggleSidebar')"
16
17
  @click="$emit('toggleSidebar')"
17
18
  >
18
19
  <span aria-hidden="true">☰</span>
@@ -26,9 +27,11 @@ defineEmits<{
26
27
 
27
28
  <div class="application-header__right">
28
29
  <slot name="actions">
29
- <NuxtLink :to="site.homePath" class="base-button base-button--secondary">
30
- Site
31
- </NuxtLink>
30
+ <ThemeSwitcher />
31
+ <LocaleSwitcher />
32
+ <NuxtLinkLocale :to="site.homePath" class="base-button base-button--secondary">
33
+ {{ t("site.openMarketingSite") }}
34
+ </NuxtLinkLocale>
32
35
  </slot>
33
36
  </div>
34
37
  </header>
@@ -48,7 +48,7 @@ watch(
48
48
  </slot>
49
49
  </div>
50
50
 
51
- <main ref="appContentRef" class="application-shell__content" tabindex="-1">
51
+ <main id="site-content" ref="appContentRef" class="application-shell__content" tabindex="-1">
52
52
  <slot />
53
53
  </main>
54
54
  </div>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  const { appNavGroups } = useNavigation();
3
+ const { t } = useI18n();
3
4
 
4
5
  defineEmits<{
5
6
  navigate: [];
@@ -7,7 +8,7 @@ defineEmits<{
7
8
  </script>
8
9
 
9
10
  <template>
10
- <aside class="application-sidebar" aria-label="Application">
11
+ <aside class="application-sidebar" :aria-label="t('navigation.application')">
11
12
  <nav class="application-sidebar__nav">
12
13
  <section
13
14
  v-for="group in appNavGroups"
@@ -15,7 +16,7 @@ defineEmits<{
15
16
  class="application-sidebar__group"
16
17
  >
17
18
  <h2 v-if="group.label">{{ group.label }}</h2>
18
- <NuxtLink
19
+ <NuxtLinkLocale
19
20
  v-for="item in group.items"
20
21
  :key="item.to"
21
22
  :to="item.to"
@@ -33,7 +34,7 @@ defineEmits<{
33
34
  }}</span>
34
35
  <span>{{ item.label }}</span>
35
36
  <small v-if="item.badge">{{ item.badge }}</small>
36
- </NuxtLink>
37
+ </NuxtLinkLocale>
37
38
  </section>
38
39
  </nav>
39
40
  </aside>
@@ -1,21 +1,14 @@
1
1
  <script setup lang="ts">
2
- const props = withDefaults(
3
- defineProps<{
4
- bottomOffset?: number;
5
- }>(),
6
- {
7
- bottomOffset: 24,
8
- },
9
- );
10
-
11
2
  const isVisible = ref(false);
3
+ const { t } = useI18n();
12
4
 
13
5
  function updateVisibility(): void {
14
6
  isVisible.value = window.scrollY > 320;
15
7
  }
16
8
 
17
9
  function scrollToTop(): void {
18
- window.scrollTo({ top: 0, behavior: "smooth" });
10
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
11
+ window.scrollTo({ top: 0, behavior: reducedMotion ? "auto" : "smooth" });
19
12
  }
20
13
 
21
14
  onMounted(() => {
@@ -33,8 +26,7 @@ onBeforeUnmount(() => {
33
26
  type="button"
34
27
  class="back-to-top"
35
28
  :class="{ 'back-to-top--visible': isVisible }"
36
- :style="{ bottom: `${props.bottomOffset}px` }"
37
- aria-label="Back to top"
29
+ :aria-label="t('site.backToTop')"
38
30
  @click="scrollToTop"
39
31
  >
40
32
  <UIcon name="i-lucide-chevron-up" class="back-to-top__icon" />
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ const site = useSiteShell();
3
+ const analytics = useAnalytics();
4
+ const consent = useConsent();
5
+ const { t } = useI18n();
6
+
7
+ const visible = computed(
8
+ () =>
9
+ site.value.consent.enabled &&
10
+ analytics.status.value !== "disabled" &&
11
+ consent.state.value.ready,
12
+ );
13
+ </script>
14
+
15
+ <template>
16
+ <button
17
+ v-if="visible"
18
+ type="button"
19
+ class="consent-settings-button"
20
+ @click="consent.openSettings"
21
+ >
22
+ {{ t("consent.settings") }}
23
+ </button>
24
+ </template>
@@ -5,6 +5,7 @@ defineProps<{
5
5
  article: ContentArticle;
6
6
  related?: ContentListItem[];
7
7
  }>();
8
+ const { t } = useI18n();
8
9
  </script>
9
10
 
10
11
  <template>
@@ -13,9 +14,16 @@ defineProps<{
13
14
  <p v-if="article.category" class="page-heading__eyebrow">{{ article.category }}</p>
14
15
  <h1>{{ article.title }}</h1>
15
16
  <p v-if="article.description">{{ article.description }}</p>
16
- <span v-if="article.author || article.date || article.updatedAt" class="content-article__meta">
17
+ <span
18
+ v-if="article.author || article.date || article.updatedAt"
19
+ class="content-article__meta"
20
+ >
17
21
  {{
18
- [article.author, article.date, article.updatedAt && `Updated ${article.updatedAt}`]
22
+ [
23
+ article.author,
24
+ article.date,
25
+ article.updatedAt && t("content.updated", { date: article.updatedAt }),
26
+ ]
19
27
  .filter(Boolean)
20
28
  .join(" · ")
21
29
  }}
@@ -26,14 +34,18 @@ defineProps<{
26
34
  <slot />
27
35
  </div>
28
36
 
29
- <aside v-if="related?.length" class="content-article__related" aria-label="Related content">
30
- <h2>Related</h2>
37
+ <aside
38
+ v-if="related?.length"
39
+ class="content-article__related"
40
+ :aria-label="t('content.relatedRegion')"
41
+ >
42
+ <h2>{{ t("content.related") }}</h2>
31
43
  <div class="content-list__grid">
32
- <NuxtLink v-for="item in related" :key="item.to" :to="item.to" class="content-card">
44
+ <NuxtLinkLocale v-for="item in related" :key="item.to" :to="item.to" class="content-card">
33
45
  <small v-if="item.category">{{ item.category }}</small>
34
46
  <h3>{{ item.title }}</h3>
35
47
  <p v-if="item.description">{{ item.description }}</p>
36
- </NuxtLink>
48
+ </NuxtLinkLocale>
37
49
  </div>
38
50
  </aside>
39
51
  </article>
@@ -17,14 +17,14 @@ defineProps<{
17
17
  </header>
18
18
 
19
19
  <div class="content-list__grid">
20
- <NuxtLink v-for="item in items" :key="item.to" :to="item.to" class="content-card">
20
+ <NuxtLinkLocale v-for="item in items" :key="item.to" :to="item.to" class="content-card">
21
21
  <small v-if="item.category">{{ item.category }}</small>
22
22
  <h2>{{ item.title }}</h2>
23
23
  <p v-if="item.description">{{ item.description }}</p>
24
24
  <span v-if="item.date || item.author">
25
25
  {{ [item.author, item.date].filter(Boolean).join(" · ") }}
26
26
  </span>
27
- </NuxtLink>
27
+ </NuxtLinkLocale>
28
28
  </div>
29
29
  </section>
30
30
  </template>