mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat(docs): changelog page overhaul
This commit is contained in:
68
docs/plugins/mdxListCountsPlugin.ts
Normal file
68
docs/plugins/mdxListCountsPlugin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
const VIRTUAL_MODULE_ID = 'virtual:mdx-list-counts'
|
||||
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID
|
||||
|
||||
function countListItems(mdxSource: string): number {
|
||||
const listItemRegex = /^[\t ]*- /gm
|
||||
const matches = mdxSource.match(listItemRegex)
|
||||
return matches ? matches.length : 0
|
||||
}
|
||||
|
||||
function mdxListCountsPlugin(): Plugin {
|
||||
return {
|
||||
name: 'vite-plugin-mdx-list-counts',
|
||||
resolveId(id) {
|
||||
if (id === VIRTUAL_MODULE_ID) {
|
||||
return RESOLVED_VIRTUAL_MODULE_ID
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
||||
const versionsDir = path.resolve(
|
||||
__dirname,
|
||||
'../src/contents/04.progress/versions'
|
||||
)
|
||||
const counts: Record<string, number> = {}
|
||||
|
||||
// Read all MDX files recursively
|
||||
function readMdxFiles(dir: string, basePath = '') {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
const relativePath = path.join(basePath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
readMdxFiles(fullPath, relativePath)
|
||||
} else if (entry.name.endsWith('.mdx')) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8')
|
||||
const key = `../versions/${relativePath.replace(/\\/g, '/')}`
|
||||
counts[key] = countListItems(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readMdxFiles(versionsDir)
|
||||
|
||||
return `export default ${JSON.stringify(counts)};`
|
||||
}
|
||||
},
|
||||
handleHotUpdate({ file, server }) {
|
||||
// Invalidate the virtual module when any MDX file changes
|
||||
if (file.endsWith('.mdx') && file.includes('versions')) {
|
||||
const module = server.moduleGraph.getModuleById(
|
||||
RESOLVED_VIRTUAL_MODULE_ID
|
||||
)
|
||||
if (module) {
|
||||
server.moduleGraph.invalidateModule(module)
|
||||
return [module]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default mdxListCountsPlugin
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from 'react'
|
||||
import Scrollbars from 'react-custom-scrollbars'
|
||||
import { useLocation } from 'shared'
|
||||
|
||||
import { BLACKLISTED_PAGES } from '../Rightbar'
|
||||
import NavigationBar from './components/NavigationBar'
|
||||
|
||||
function Boilerplate({ children }: { children: React.ReactNode }) {
|
||||
@@ -40,7 +41,9 @@ function Boilerplate({ children }: { children: React.ReactNode }) {
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full min-w-0 flex-col sm:pl-8 lg:w-[calc(100%-20rem)]">
|
||||
<div
|
||||
className={`flex h-full w-full min-w-0 flex-col sm:pl-8 ${BLACKLISTED_PAGES.some(page => location.pathname.startsWith(page)) ? '' : 'lg:w-[calc(100%-20rem)]'}`}
|
||||
>
|
||||
{children}
|
||||
<NavigationBar />
|
||||
<hr className="border-bg-200 dark:border-bg-800 my-12 border-t-[1.5px]" />
|
||||
|
||||
@@ -3,6 +3,8 @@ import _ from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useLocation } from 'shared'
|
||||
|
||||
export const BLACKLISTED_PAGES = ['/progress/changelog']
|
||||
|
||||
function Rightbar() {
|
||||
const [allSections, setAllSections] = useState<string[]>([])
|
||||
|
||||
@@ -135,6 +137,10 @@ function Rightbar() {
|
||||
}, 1000) // 1 second delay to allow scroll to finish
|
||||
}
|
||||
|
||||
if (BLACKLISTED_PAGES.some(page => location.pathname.startsWith(page))) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="fixed top-20 right-0 hidden h-full min-h-0 w-80 overflow-y-auto p-12 lg:block">
|
||||
<h2 className="text-lg font-semibold">On This Page</h2>
|
||||
|
||||
@@ -15,15 +15,8 @@ Unlike traditional software development projects which adapt Semantic versioning
|
||||
```plaintext
|
||||
dev <year>w<week>
|
||||
```
|
||||
|
||||
</Alert>
|
||||
|
||||
<Alert className="mt-6" type="warning">
|
||||
This page only covers changes made from version 25w33 onwards. For earlier
|
||||
versions, please refer to the legacy
|
||||
[CHANGELOG.md](https://github.com/lifeforge-app/lifeforge/blob/main/CHANGELOG.md).
|
||||
Also, minor changes may not be documented here.
|
||||
</Alert>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import mdxListCounts from 'virtual:mdx-list-counts'
|
||||
|
||||
import { components } from '@/components/MdxComponents'
|
||||
|
||||
import Version from './Version'
|
||||
|
||||
const compiledModules = import.meta.glob('../versions/**/*.mdx', {
|
||||
eager: true
|
||||
}) as Record<
|
||||
string,
|
||||
{ default: React.ComponentType<{ components: typeof components }> }
|
||||
>
|
||||
|
||||
const entries = Object.entries(
|
||||
Object.groupBy(
|
||||
Object.entries(import.meta.glob('../versions/**/*.mdx', { eager: true })),
|
||||
Object.entries(compiledModules),
|
||||
([path]) => path.match(/\.\.\/versions\/(\d+)\/.*/)![1]
|
||||
)
|
||||
)
|
||||
@@ -14,22 +23,28 @@ const entries = Object.entries(
|
||||
versions: modules!
|
||||
.map(([path, module]) => ({
|
||||
week: Number(path.match(/\.\.\/versions\/\d+\/(\d+)\.mdx/)![1]),
|
||||
Component: (module as any).default
|
||||
Component: module.default,
|
||||
liCount: mdxListCounts[path] ?? 0
|
||||
}))
|
||||
.sort((a, b) => b.week - a.week)
|
||||
}))
|
||||
|
||||
function ChangelogEntries() {
|
||||
return (
|
||||
<>
|
||||
<div className="divide-bg-500/20 divide-y-[1.5px]">
|
||||
{entries.map(({ year, versions }) =>
|
||||
versions.map(({ week, Component }) => (
|
||||
<Version key={`${year}-week-${week}`} week={week} year={year}>
|
||||
versions.map(({ week, Component, liCount }) => (
|
||||
<Version
|
||||
key={`${year}-week-${week}`}
|
||||
liCount={liCount}
|
||||
week={week}
|
||||
year={year}
|
||||
>
|
||||
<Component components={components} />
|
||||
</Version>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import dayjs from 'dayjs'
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear'
|
||||
import React from 'react'
|
||||
import { Card } from 'lifeforge-ui'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
dayjs.extend(weekOfYear)
|
||||
|
||||
@@ -9,47 +10,87 @@ function Version({
|
||||
prefix = 'dev',
|
||||
year = dayjs().year(),
|
||||
week,
|
||||
liCount,
|
||||
children
|
||||
}: {
|
||||
prefix?: string
|
||||
year?: number
|
||||
week: number
|
||||
liCount?: number
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
const [debouncedCollapsed, setDebouncedCollapsed] = useState(true)
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const version = `${prefix} ${dayjs().year(year).format('YY')}w${week.toString().padStart(2, '0')}`
|
||||
|
||||
const startOfWeek = dayjs()
|
||||
.year(year)
|
||||
.week(week)
|
||||
.startOf('week')
|
||||
.format('DD MMM YYYY')
|
||||
// Start from January 4th of the year (guaranteed to be in week 1 per ISO 8601)
|
||||
// then add the weeks offset
|
||||
const weekDate = dayjs(`${year}-01-04`).week(week)
|
||||
|
||||
const endOfWeek = dayjs()
|
||||
.year(year)
|
||||
.week(week)
|
||||
.endOf('week')
|
||||
.format('DD MMM YYYY')
|
||||
const startOfWeek = weekDate.startOf('week').format('DD MMM YYYY')
|
||||
|
||||
const endOfWeek = weekDate.endOf('week').format('DD MMM YYYY')
|
||||
|
||||
function toggleCollapsed() {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
|
||||
if (collapsed) {
|
||||
// Expanding: immediately show content, then animate
|
||||
setDebouncedCollapsed(false)
|
||||
setCollapsed(false)
|
||||
} else {
|
||||
// Collapsing: animate first, then hide content after animation completes
|
||||
setCollapsed(true)
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setDebouncedCollapsed(true)
|
||||
}, 200) // Match the duration-200 animation
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id={`${prefix}-${dayjs().year(year).format('YY')}-w-${week.toString().padStart(2, '0')}`}
|
||||
>
|
||||
<header className="space-y-2">
|
||||
<div className="flex items-center gap-3 text-2xl font-semibold sm:text-3xl">
|
||||
<div className="bg-custom-500/20 text-custom-500 rounded-lg p-3">
|
||||
<Icon className="size-10" icon="tabler:history" />
|
||||
</div>
|
||||
<div>
|
||||
<Card
|
||||
className="overflow-y-hidden p-0!"
|
||||
id={`${prefix}-${dayjs().year(year).format('YY')}-w-${week.toString().padStart(2, '0')}`}
|
||||
>
|
||||
<header
|
||||
className="hover:bg-bg-100 dark:hover:bg-bg-800/50 flex cursor-pointer items-center justify-between gap-4 p-4 transition-colors select-none"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<div>
|
||||
<h2 className="block">{version}</h2>
|
||||
<span className="text-bg-500 block text-base">
|
||||
<h2 className="text-xl font-medium sm:text-2xl">{version}</h2>
|
||||
<span className="text-bg-500 text-sm">{liCount} entries</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-bg-500 hidden text-sm sm:block">
|
||||
{startOfWeek} - {endOfWeek}
|
||||
</span>
|
||||
<Icon
|
||||
className={`text-bg-500 size-5 shrink-0 transition-transform duration-500 ${
|
||||
!collapsed ? 'rotate-180' : ''
|
||||
}`}
|
||||
icon="tabler:chevron-down"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className={`grid px-4 transition-all duration-200 ${
|
||||
collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'
|
||||
}`}
|
||||
>
|
||||
{!debouncedCollapsed && (
|
||||
<div className="overflow-hidden pb-8">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
<hr className="border-bg-200 dark:border-bg-800 mt-8 mb-4 border-t-[1.5px] sm:mt-12 sm:mb-8" />
|
||||
</section>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
5
docs/src/vite-env.d.ts
vendored
5
docs/src/vite-env.d.ts
vendored
@@ -1 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'virtual:mdx-list-counts' {
|
||||
const counts: Record<string, number>
|
||||
export default counts
|
||||
}
|
||||
|
||||
@@ -7,5 +7,8 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"plugins/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -2,17 +2,18 @@ import mdx, { Options } from '@mdx-js/rollup'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import mdxListCountsPlugin from './plugins/mdxListCountsPlugin'
|
||||
|
||||
const options: Options = {
|
||||
remarkPlugins: [remarkGfm]
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), mdx(options), tailwindcss()],
|
||||
plugins: [react(), mdx(options), tailwindcss(), mdxListCountsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
|
||||
Reference in New Issue
Block a user