Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vitepress/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const navItemOrder: Record<string, string[]> = {
"develop-from-scratch",
],
sdk: ["quickstart", "cli", "services", "generator"],
reference: ["api", "platform", "concepts", "infrastructure"],
reference: ["changelog","security", "api", "platform", "concepts", "infrastructure"],
"getting-started": ["quickstart", "core-concepts", "console"],
"app-shell": [
"introduction",
Expand Down Expand Up @@ -93,6 +93,7 @@ export const sidebarItemOrder: Record<string, string[]> = {
"login",
],
function: ["overview", "builtin-interfaces"],
reference: ["changelog","api", "platform", "concepts", "infrastructure", "security"],
"app-shell": [
"introduction",
"quickstart",
Expand Down
13 changes: 10 additions & 3 deletions .vitepress/config/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,23 @@ export function generateSidebar(
for (const dirName of sortedDirs) {
const subdirPath = path.join(dir, dirName);
const subdirBasePath = `${basePath}/${dirName}`;
const dirIndexPath = path.join(fullPath, dirName, "index.md");
const hasIndex = fs.existsSync(dirIndexPath);
const subItems = generateSidebar(docsDir, subdirPath, subdirBasePath, depth + 1);

// Add directory if it has sub-items OR if it has an index.md
if (subItems.length > 0) {
// Check if directory has an index.md to read title from
const dirIndexPath = path.join(fullPath, dirName, "index.md");
items.push({
text: toTitle(dirName, { filePath: fs.existsSync(dirIndexPath) ? dirIndexPath : undefined }),
text: toTitle(dirName, { filePath: hasIndex ? dirIndexPath : undefined }),
collapsed: depth > 0,
items: subItems,
});
} else if (hasIndex) {
// Directory with only index.md - add as direct link
items.push({
text: toTitle(dirName, { filePath: dirIndexPath }),
link: subdirBasePath,
});
}
}

Expand Down
1 change: 1 addition & 0 deletions .vitepress/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ declare module "*.vue" {
const component: DefineComponent;
export default component;
}

100 changes: 100 additions & 0 deletions .vitepress/theme/components/Changelog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<template>
<div class="changelog-container">
<FilterTabs v-model="selectedProduct" :options="PRODUCTS" :counts="productCounts" />

<div v-if="paginatedEntries.length > 0">
<div
v-for="entry in paginatedEntries"
:key="entry.id"
class="release-card"
:id="anchorId(entry.id)"
>
<div class="release-card-header">
<div class="release-card-meta">
<span class="entry-date">{{ formatDate(entry.date) }}</span>
<template v-if="entry.product !== 'Platform Core'">
<span class="entry-separator">•</span>
<a :href="entry.githubUrl" target="_blank" rel="noopener" class="entry-github-link">
View on GitHub →
</a>
</template>
</div>
<div class="release-card-badges">
<span class="product-badge" :data-product="entry.product.toLowerCase().replace(/\s+/g, '-')">
{{ entry.product }}
</span>
<span v-if="entry.product !== 'Platform Core'" class="version-badge" :data-type="entry.versionType">
v{{ extractVersion(entry.version) }}
</span>
<span v-if="entry.breaking" class="breaking-badge">⚠️ Breaking</span>
</div>
</div>

<h3 class="entry-title">{{ entry.title }}</h3>

<div v-if="entry.narrative" class="entry-narrative">
<div class="narrative-summary">
<strong>What's new:</strong> {{ entry.narrative.summary }}
</div>
<div v-if="entry.narrative.impact" class="narrative-impact">
<strong>Impact:</strong> {{ entry.narrative.impact }}
</div>
<div v-if="entry.narrative.details?.length" class="narrative-details">
<strong>Key changes:</strong>
<ul>
<li v-for="(detail, i) in entry.narrative.details" :key="i">{{ detail }}</li>
</ul>
</div>
<div v-if="entry.narrative.migration" class="narrative-migration">
<strong>⚠️ Migration required:</strong>
<div v-html="entry.narrative.migration" />
</div>
</div>

<div v-else class="release-body">
<p v-for="(para, i) in entry.body.split('\n\n')" :key="i">{{ para }}</p>
</div>

</div>
</div>

<div v-else class="empty-state">
<p>No releases found for {{ selectedProduct }}.</p>
</div>

<div v-if="hasMore" class="load-more">
<button class="load-more-btn" @click="loadMore">
Load More <span class="remaining">({{ remaining }} remaining)</span>
</button>
</div>

<div class="changelog-footer">
<p class="stats">
Showing {{ paginatedEntries.length }} of {{ filteredEntries.length }} releases
<span v-if="selectedProduct !== 'All'"> for {{ selectedProduct }}</span>
</p>
<p class="last-updated">Last updated: {{ formatDate(changelogData.lastUpdated) }}</p>
</div>
</div>
</template>

<script setup lang="ts">
import { data as changelogData } from '../../../docs/reference/changelog/releases.data'
import { useChangelog, formatDate, PRODUCTS } from '../composables/useChangelog'
import FilterTabs from './FilterTabs.vue'

const { selectedProduct, filteredEntries, paginatedEntries, hasMore, remaining, loadMore, productCounts } =
useChangelog(changelogData)

function extractVersion(version?: string): string {
if (!version) return ''
const lastAt = version.lastIndexOf('@')
return lastAt > 0 ? version.slice(lastAt + 1) : version
}

function anchorId(id: string): string {
return id.replace(/[@/]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
}
</script>

<style src="../styles/changelog.css" />
29 changes: 29 additions & 0 deletions .vitepress/theme/components/FilterTabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div class="filter-tabs">
<button
v-for="option in options"
:key="option"
class="filter-tab"
:class="{ active: modelValue === option }"
:aria-pressed="modelValue === option"
@click="$emit('update:modelValue', option)"
>
{{ option }}<span v-if="counts?.[option] !== undefined" class="filter-tab-count">{{ counts[option] }}</span>
</button>
</div>
</template>

<script setup lang="ts">
// Generic single-select filter tab bar. Stateless — active value is owned by the parent via v-model.
defineProps<{
options: string[]
modelValue: string
counts?: Record<string, number>
}>()

defineEmits<{
'update:modelValue': [value: string]
}>()
</script>

<style src="../styles/filter-tabs.css" />
93 changes: 93 additions & 0 deletions .vitepress/theme/composables/useChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { ref, computed, watch } from 'vue'

export interface ChangelogNarrative {
summary: string
impact?: string
details?: string[]
migration?: string | null
}

export interface ChangelogItem {
id: string
date: string
product: string
version: string
versionType: 'major' | 'minor' | 'patch'
title: string
breaking: boolean
githubUrl: string
body: string
narrative: ChangelogNarrative
createdAt: string
updatedAt?: string
}

export interface ChangelogData {
lastUpdated: string
entries: ChangelogItem[]
}

// Products shown in the UI — Platform Core is intentionally excluded
const VISIBLE_PRODUCTS = ['SDK', 'AppShell'] as const

export const PRODUCTS = ['All', ...VISIBLE_PRODUCTS]

export function formatDate(dateString: string): string {
if (!dateString) return ''
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}

export function useChangelog(data: ChangelogData) {
const selectedProduct = ref('All')
const currentPage = ref(1)
const itemsPerPage = 20

watch(selectedProduct, () => {
currentPage.value = 1
})

// Only surface entries for visible products
const visibleEntries = data.entries.filter((e) =>
(VISIBLE_PRODUCTS as readonly string[]).includes(e.product),
)

const filteredEntries = computed(() =>
selectedProduct.value === 'All'
? visibleEntries
: visibleEntries.filter((e) => e.product === selectedProduct.value),
)

const paginatedEntries = computed(() =>
filteredEntries.value.slice(0, currentPage.value * itemsPerPage),
)

const hasMore = computed(() => paginatedEntries.value.length < filteredEntries.value.length)

const remaining = computed(() => filteredEntries.value.length - paginatedEntries.value.length)

const productCounts = computed(() => {
const counts: Record<string, number> = { All: visibleEntries.length }
for (const entry of visibleEntries) {
counts[entry.product] = (counts[entry.product] ?? 0) + 1
}
return counts
})

function loadMore() {
currentPage.value++
}

return {
selectedProduct,
filteredEntries,
paginatedEntries,
hasMore,
remaining,
loadMore,
productCounts,
}
}
Loading
Loading