Skip to content

Commit 5cf01fb

Browse files
committed
Redesign release notes with consistent layout alignment
Rewrite release notes layouts and CSS to integrate seamlessly with the VitePress docs theme. Key changes: - Replace gradient hero and blog-specific typography with clean card feed using VitePress design tokens - Fix content alignment: pull post grid into VPContent padding area so article text starts at the same horizontal position as doc pages - Add VideoEmbed component to fix video loading on SPA navigation - Convert language switcher from dropdown to inline segmented control - Add custom sidebar navigation matching VitePress sidebar visual style - Replicate has-sidebar CSS rules for navbar/content positioning on release notes pages via html.rn-page class toggle
1 parent 760c97a commit 5cf01fb

16 files changed

Lines changed: 570 additions & 801 deletions
Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup>
22
import { data as posts } from './release-notes.data.mts'
3-
import LegalFooter from './components/LegalFooter.vue'
43
54
function formatDate(dateStr) {
65
if (!dateStr) return ''
@@ -14,44 +13,38 @@ function formatDate(dateStr) {
1413
</script>
1514

1615
<template>
17-
<div class="blog-layout">
18-
<div class="blog-hero blog-hero--centered">
19-
<div class="blog-hero-copy">
20-
<h1 style="color: #fff; margin: 0; font-size: 2em;">Release Notes</h1>
21-
<p style="opacity: 0.85; margin: 10px 0 0; font-size: 1em;">
22-
New features and updates for Mobile ID
23-
</p>
24-
</div>
25-
</div>
16+
<div class="rn-index">
17+
<header class="rn-index-header">
18+
<h1>Release Notes</h1>
19+
<p>New features and updates for Mobile ID</p>
20+
</header>
2621

27-
<div style="padding: 32px 24px; max-width: 800px; margin: 0 auto;">
22+
<div class="rn-feed">
2823
<a
2924
v-for="post in posts"
3025
:key="post.url"
3126
:href="post.url"
32-
class="blog-card"
33-
style="display: block; text-decoration: none; margin-bottom: 24px;"
27+
class="rn-card"
3428
>
35-
<div class="blog-card-thumbnail">
36-
<img
37-
v-if="post.thumbnail"
38-
:src="post.thumbnail"
39-
:alt="post.title"
40-
/>
29+
<div v-if="post.thumbnail" class="rn-card-thumb">
30+
<img :src="post.thumbnail" :alt="post.title" />
4131
</div>
42-
<div class="blog-card-body">
43-
<div class="blog-card-meta">
44-
<span>{{ formatDate(post.date) }}</span>
45-
<span v-if="post.readingTime">·</span>
46-
<span v-if="post.readingTime">{{ post.readingTime }} min read</span>
32+
<div class="rn-card-body">
33+
<div class="rn-card-meta">
34+
{{ formatDate(post.date) }}
35+
<template v-if="post.readingTime"> · {{ post.readingTime }} min read</template>
4736
</div>
48-
<div class="blog-card-title">{{ post.title }}</div>
49-
<div class="blog-card-description">{{ post.description }}</div>
50-
<span class="blog-card-link">Read more →</span>
37+
<h2>{{ post.title }}</h2>
38+
<p>{{ post.description }}</p>
39+
<span class="rn-card-cta">Read more →</span>
5140
</div>
5241
</a>
5342
</div>
5443

55-
<LegalFooter />
44+
<footer class="rn-footer">
45+
<a href="/legal/imprint">Imprint</a>
46+
<span aria-hidden="true">·</span>
47+
<a href="/legal/privacy">Privacy Notice</a>
48+
</footer>
5649
</div>
5750
</template>
Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
11
<script setup>
22
import { useData } from 'vitepress'
33
import { computed } from 'vue'
4-
import LegalFooter from './components/LegalFooter.vue'
54
import ReleaseNotesSidebar from './components/ReleaseNotesSidebar.vue'
65
76
const { frontmatter, page } = useData()
87
98
const META_COPY = {
10-
en: {
11-
section: 'Release Notes',
12-
readingTime: 'min read',
13-
locale: 'en-US',
14-
},
15-
de: {
16-
section: 'Release Notes',
17-
readingTime: 'Min. Lesezeit',
18-
locale: 'de-CH',
19-
},
20-
fr: {
21-
section: 'Release Notes',
22-
readingTime: 'min de lecture',
23-
locale: 'fr-CH',
24-
},
25-
it: {
26-
section: 'Release Notes',
27-
readingTime: 'min di lettura',
28-
locale: 'it-CH',
29-
},
9+
en: { readingTime: 'min read', locale: 'en-US' },
10+
de: { readingTime: 'Min. Lesezeit', locale: 'de-CH' },
11+
fr: { readingTime: 'min de lecture', locale: 'fr-CH' },
12+
it: { readingTime: 'min di lettura', locale: 'it-CH' },
3013
}
3114
3215
const currentLang = computed(() => {
3316
const rel = page.value.relativePath || ''
34-
const langFromPath = rel.match(/\.(de|fr|it)\.md$/)?.[1]
35-
return langFromPath || frontmatter.value.lang || 'en'
17+
return rel.match(/\.(de|fr|it)\.md$/)?.[1] || frontmatter.value.lang || 'en'
3618
})
3719
3820
const metaCopy = computed(() => META_COPY[currentLang.value] ?? META_COPY.en)
3921
4022
function formatDate(dateStr) {
4123
if (!dateStr) return ''
42-
const d = new Date(dateStr)
43-
return d.toLocaleDateString(metaCopy.value.locale, {
24+
return new Date(dateStr).toLocaleDateString(metaCopy.value.locale, {
4425
year: 'numeric',
4526
month: 'long',
4627
day: 'numeric',
@@ -49,32 +30,17 @@ function formatDate(dateStr) {
4930
</script>
5031
5132
<template>
52-
<div class="blog-layout">
53-
<!-- Hero Header -->
54-
<div class="blog-hero">
55-
<div style="max-width: 800px; margin: 0 auto;">
56-
<div style="font-size: 0.75em; letter-spacing: 1px; text-transform: uppercase; opacity: 0.7; margin-bottom: 12px; font-family: var(--vp-font-family-base);">
57-
{{ metaCopy.section }}
58-
</div>
59-
<h1 style="color: #fff; margin: 0 0 16px; font-size: 2em; line-height: 1.2;">
60-
{{ frontmatter.title }}
61-
</h1>
62-
<div style="display: flex; gap: 20px; font-size: 0.85em; opacity: 0.85; font-family: var(--vp-font-family-base); flex-wrap: wrap;">
63-
<span v-if="frontmatter.date">📅 {{ formatDate(frontmatter.date) }}</span>
64-
<span v-if="frontmatter.readingTime">⏱ {{ frontmatter.readingTime }} {{ metaCopy.readingTime }}</span>
65-
</div>
66-
</div>
67-
</div>
68-
69-
<div class="blog-post-shell">
70-
<ReleaseNotesSidebar />
71-
72-
<!-- Content rendered from Markdown -->
73-
<div class="blog-content blog-content--post">
74-
<Content />
75-
</div>
76-
</div>
77-
78-
<LegalFooter />
33+
<div class="rn-post-layout">
34+
<ReleaseNotesSidebar />
35+
<article class="rn-article vp-doc">
36+
<header class="rn-post-header">
37+
<p class="rn-post-meta">
38+
<span v-if="frontmatter.date">{{ formatDate(frontmatter.date) }}</span>
39+
<template v-if="frontmatter.readingTime"> · {{ frontmatter.readingTime }} {{ metaCopy.readingTime }}</template>
40+
</p>
41+
<h1>{{ frontmatter.title }}</h1>
42+
</header>
43+
<Content />
44+
</article>
7945
</div>
8046
</template>
Lines changed: 106 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
<script setup>
22
import { useData, useRouter } from 'vitepress'
3-
import { computed, ref, onMounted, onUnmounted } from 'vue'
3+
import { computed } from 'vue'
44
55
const props = defineProps({
66
languages: {
77
type: Object,
8-
default: () => ({ en: 'English', de: 'Deutsch', fr: 'Français', it: 'Italiano' })
9-
}
8+
default: () => ({ en: 'English', de: 'Deutsch', fr: 'Français', it: 'Italiano' }),
9+
},
1010
})
1111
12+
const codes = { en: 'EN', de: 'DE', fr: 'FR', it: 'IT' }
1213
const flags = { en: '🇬🇧', de: '🇩🇪', fr: '🇫🇷', it: '🇮🇹' }
1314
1415
const { frontmatter, page } = useData()
1516
const router = useRouter()
16-
const open = ref(false)
17-
const dropdownRef = ref(null)
1817
1918
const currentLang = computed(() => {
2019
const rel = page.value.relativePath || ''
@@ -27,70 +26,118 @@ const baseUrl = computed(() => {
2726
return '/' + rel.replace(/\.(de|fr|it|en)$/, '')
2827
})
2928
30-
function toggle() {
31-
open.value = !open.value
29+
function langUrl(lang) {
30+
const suffix = lang === 'en' ? '' : `.${lang}`
31+
return `${baseUrl.value}${suffix}.html`
3232
}
3333
34-
function switchLanguage(lang) {
34+
function switchLanguage(e, lang) {
3535
if (lang === currentLang.value) {
36-
open.value = false
36+
e.preventDefault()
3737
return
3838
}
39-
open.value = false
40-
const suffix = lang === 'en' ? '' : `.${lang}`
41-
router.go(`${baseUrl.value}${suffix}.html`)
39+
e.preventDefault()
40+
router.go(langUrl(lang))
4241
}
42+
</script>
4343
44-
function onClickOutside(e) {
45-
if (dropdownRef.value && !dropdownRef.value.contains(e.target)) {
46-
open.value = false
47-
}
44+
<template>
45+
<nav class="lang-seg" aria-label="Language">
46+
<a
47+
v-for="(label, code) in languages"
48+
:key="code"
49+
:href="langUrl(code)"
50+
class="lang-seg-pill"
51+
:class="{ 'is-active': code === currentLang }"
52+
:aria-current="code === currentLang ? 'page' : undefined"
53+
:title="label"
54+
@click="switchLanguage($event, code)"
55+
>
56+
<span class="lang-seg-flag" aria-hidden="true">{{ flags[code] }}</span>
57+
<span class="lang-seg-code">{{ codes[code] }}</span>
58+
</a>
59+
</nav>
60+
</template>
61+
62+
<style scoped>
63+
.lang-seg {
64+
display: flex;
65+
justify-content: flex-end;
66+
margin-bottom: 1.5em;
4867
}
4968
50-
function onKeydown(e) {
51-
if (e.key === 'Escape') open.value = false
69+
.lang-seg {
70+
display: inline-flex;
71+
gap: 2px;
72+
padding: 3px;
73+
margin-left: auto;
74+
background: var(--vp-c-bg-soft);
75+
border: 1px solid var(--vp-c-divider);
76+
border-radius: 10px;
5277
}
5378
54-
onMounted(() => {
55-
document.addEventListener('click', onClickOutside)
56-
document.addEventListener('keydown', onKeydown)
57-
})
79+
.lang-seg-pill {
80+
display: inline-flex;
81+
align-items: center;
82+
gap: 6px;
83+
padding: 5px 12px;
84+
border-radius: 7px;
85+
font-family: var(--vp-font-family-base);
86+
font-size: 0.82rem;
87+
font-weight: 500;
88+
color: var(--vp-c-text-2);
89+
text-decoration: none !important;
90+
transition: color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
91+
cursor: pointer;
92+
user-select: none;
93+
line-height: 1;
94+
white-space: nowrap;
95+
}
5896
59-
onUnmounted(() => {
60-
document.removeEventListener('click', onClickOutside)
61-
document.removeEventListener('keydown', onKeydown)
62-
})
63-
</script>
97+
.lang-seg-pill:hover:not(.is-active) {
98+
color: var(--vp-c-text-1);
99+
background: rgba(0, 0, 0, 0.04);
100+
}
64101
65-
<template>
66-
<div class="lang-switcher" ref="dropdownRef">
67-
<button
68-
class="lang-trigger"
69-
:class="{ 'lang-trigger--open': open }"
70-
@click="toggle"
71-
:aria-expanded="open"
72-
aria-haspopup="listbox"
73-
>
74-
<span class="lang-flag">{{ flags[currentLang] }}</span>
75-
<span class="lang-name">{{ languages[currentLang] }}</span>
76-
<svg class="lang-chevron" :class="{ 'lang-chevron--open': open }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
77-
</button>
78-
<Transition name="lang-dropdown">
79-
<ul v-if="open" class="lang-menu" role="listbox">
80-
<li
81-
v-for="(label, code) in languages"
82-
:key="code"
83-
class="lang-option"
84-
:class="{ 'lang-option--active': code === currentLang }"
85-
role="option"
86-
:aria-selected="code === currentLang"
87-
@click="switchLanguage(code)"
88-
>
89-
<span class="lang-flag">{{ flags[code] }}</span>
90-
<span class="lang-name">{{ label }}</span>
91-
<svg v-if="code === currentLang" class="lang-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
92-
</li>
93-
</ul>
94-
</Transition>
95-
</div>
96-
</template>
102+
:global(html.dark) .lang-seg-pill:hover:not(.is-active) {
103+
background: rgba(255, 255, 255, 0.05);
104+
}
105+
106+
.lang-seg-pill.is-active {
107+
color: var(--vp-c-text-1);
108+
background: var(--vp-c-bg);
109+
font-weight: 600;
110+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0.5px 1px rgba(0, 0, 0, 0.06);
111+
cursor: default;
112+
}
113+
114+
:global(html.dark) .lang-seg-pill.is-active {
115+
background: var(--vp-c-bg-elv);
116+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
117+
}
118+
119+
.lang-seg-pill:focus-visible {
120+
outline: 2px solid var(--vp-c-brand-3);
121+
outline-offset: 1px;
122+
}
123+
124+
.lang-seg-flag {
125+
font-size: 1.15em;
126+
line-height: 1;
127+
}
128+
129+
.lang-seg-code {
130+
letter-spacing: 0.03em;
131+
}
132+
133+
@media (max-width: 480px) {
134+
.lang-seg-pill {
135+
padding: 5px 8px;
136+
gap: 4px;
137+
}
138+
139+
.lang-seg-code {
140+
font-size: 0.78rem;
141+
}
142+
}
143+
</style>

0 commit comments

Comments
 (0)