Skip to content
Merged
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
93 changes: 93 additions & 0 deletions src/app/(dashboard-layout)/doc/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client"

import { useEffect } from "react"

// shadcn
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"

// project
import branding from "@/branding.json"
import DocsNavigation from "@/components/doc-bottom-nav"
import ChangelogEntry from "@/components/uiable/changelog/changelog-entry"
import ChangelogMobileToc from "@/components/uiable/changelog/changelog-mobile-toc"
import ChangelogToc from "@/components/uiable/changelog/changelog-toc"
import { CHANGELOG_DATA } from "@/data/changelog-data"

// ------------------------------ | PAGE - CHANGELOG | ------------------------------ //

export default function ChangelogPage() {
// Deep linking: scroll to anchor from URL hash on mount
useEffect(() => {
const hash = window.location.hash.replace("#", "")
if (hash) {
// Allow the DOM to settle first
requestAnimationFrame(() => {
const element = document.getElementById(hash)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
}
})
}
}, [])

return (
<div className="flex flex-col gap-6">
<Card>
<CardContent>
<div className="flex flex-row gap-6 text-muted-foreground">
<div className="flex grow flex-col gap-8">
<section
id="changelog"
className="flex scroll-mt-24 flex-col gap-8"
>
<div className="flex flex-col gap-2">
<h4 className="group relative scroll-mt-20">Changelog</h4>
<p className="text-muted-foreground">
Latest updates and announcements for {branding.brandName}.
Track new components, improvements, and breaking changes
across releases.
</p>
</div>

<ChangelogMobileToc />

<Separator />

<div className="flex flex-col gap-10">
{CHANGELOG_DATA.map((release, index) => (
<ChangelogEntry
key={release.version}
release={release}
isLast={index === CHANGELOG_DATA.length - 1}
/>
))}
</div>
</section>
<DocsNavigation
previousItem={{
name: "Components",
url: "/doc/components",
}}
nextItem={null}
/>
</div>
<Separator orientation="vertical" className="max-xl:hidden" />
<div className="hidden w-sidebar-width shrink-0 xl:block">
<div className="sticky top-20">
<Card className="border-0 bg-transparent">
<CardHeader className="px-0 py-3">
<h5>On this page</h5>
</CardHeader>
<CardContent className="px-0 py-3">
<ChangelogToc />
</CardContent>
</Card>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
1 change: 1 addition & 0 deletions src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function AppSidebar({ ...props }: ComponentProps<typeof Sidebar>) {
{ title: "Installation", href: "/doc/installation" },
{ title: "Shadcn CLI", href: "/doc/cli" },
{ title: "Components", href: "/doc/components" },
{ title: "Changelog", href: "/doc/changelog" },
]

const filteredDocs = docs.filter((doc) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
"use client"

import { ReactNode } from "react"
import { ReactLenis } from "lenis/react"
import { ReactNode, useEffect, useRef } from "react"
import { ReactLenis, useLenis } from "lenis/react"

// ------------------------------ | COMPONENT - SMOOTH SCROLL | ------------------------------ //

const LENIS_OPTIONS = { lerp: 0.1, duration: 2.2 }

function isScrollLocked() {
return (
document.body.hasAttribute("data-scroll-locked") ||
document.body.style.overflow === "hidden"
)
}

function ScrollLocker() {
const lenis = useLenis()
const lockedRef = useRef(false)

useEffect(() => {
if (!lenis) return

const applyState = () => {
const locked = isScrollLocked()
if (locked === lockedRef.current) return
lockedRef.current = locked
locked ? lenis.stop() : lenis.start()
}

const observer = new MutationObserver(applyState)

observer.observe(document.body, {
attributes: true,
attributeFilter: ["data-scroll-locked", "style"],
})

applyState()

return () => observer.disconnect()
}, [lenis])

return null
}

export default function SmoothScroll({ children }: { children: ReactNode }) {
return (
<ReactLenis root options={{ lerp: 0.1, duration: 2.2 }}>
<ReactLenis root options={LENIS_OPTIONS}>
<ScrollLocker />
{children}
</ReactLenis>
)
Expand Down
99 changes: 99 additions & 0 deletions src/components/uiable/changelog/changelog-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client"

// shadcn
import { Separator } from "@/components/ui/separator"
import { ExternalLink } from "lucide-react"

// project
import { CHANGELOG_DATA } from "@/data/changelog-data"

// types
type ChangelogRelease = (typeof CHANGELOG_DATA)[number]

interface ChangelogEntryProps {
release: ChangelogRelease
isLast?: boolean
}

// ------------------------------ | COMPONENT - CHANGELOG ENTRY | ------------------------------ //

export default function ChangelogEntry({
release,
isLast = false,
}: ChangelogEntryProps) {
return (
<article aria-labelledby={release.anchor} className="flex flex-col gap-10">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2
id={release.anchor}
className="group relative scroll-mt-24 text-xl font-semibold tracking-tight text-foreground"
>
<a
href={`#${release.anchor}`}
className="inline-flex items-center gap-2 transition-colors hover:text-primary"
aria-label={`Version ${release.version}`}
>
v{release.version}
{release.title && (
<span className="text-base font-normal text-muted-foreground">
— {release.title}
</span>
)}
</a>
</h2>

<p className="text-sm text-muted-foreground">{release.date}</p>
</div>

<div className="flex flex-col gap-6">
{release.categories.map((category) => (
<div key={category.title} className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-foreground">
{category.title}
</h3>
<span className="text-xs text-muted-foreground">
{category.items.length}{" "}
{category.items.length === 1 ? "change" : "changes"}
</span>
</div>
<ul
className="ml-6 flex flex-col gap-2 list-disc marker:text-muted-foreground"
role="list"
>
{category.items.map((item, index) => {
const text = typeof item === "string" ? item : item.text
const previewUrl =
typeof item === "string" ? undefined : item.previewUrl

return (
<li
key={index}
className="text-sm leading-relaxed text-muted-foreground"
>
{text}
{previewUrl && (
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-2 inline-flex items-center gap-1 font-medium text-primary hover:underline"
>
Preview
<ExternalLink className="size-3" />
</a>
)}
</li>
)
})}
</ul>
</div>
))}
</div>
</div>

{!isLast && <Separator />}
</article>
)
}
103 changes: 103 additions & 0 deletions src/components/uiable/changelog/changelog-mobile-toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client"

import { useEffect, useState } from "react"

// shadcn
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"

// project
import { cn } from "@/lib/utils"
import { CHANGELOG_DATA } from "@/data/changelog-data"

// assets
import { ListTree } from "lucide-react"

// ------------------------------ | COMPONENT - CHANGELOG MOBILE TOC | ------------------------------ //

export default function ChangelogMobileToc() {
const [activeId, setActiveId] = useState<string | null>(null)
const [open, setOpen] = useState(false)

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{ rootMargin: "0% 0% -80% 0%" }
)

CHANGELOG_DATA.forEach((release) => {
const element = document.getElementById(release.anchor)
if (element) observer.observe(element)
})

return () => observer.disconnect()
}, [])

const handleClick = (anchor: string) => {
setOpen(false)
// Small delay to let sheet close before scrolling
requestAnimationFrame(() => {
const element = document.getElementById(anchor)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
}
})
}

return (
<div className="xl:hidden">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger
render={
<Button
variant="outline"
className="w-fit gap-2 text-muted-foreground"
/>
}
>
<ListTree className="size-4" />
<span>On this page</span>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>On this page</SheetTitle>
</SheetHeader>
<nav
aria-label="Changelog version navigation"
className="overflow-y-auto px-5 pb-5"
>
<ul className="flex flex-col gap-0.5">
{CHANGELOG_DATA.map((release) => (
<li key={release.anchor}>
<button
onClick={() => handleClick(release.anchor)}
className={cn(
"block w-full cursor-pointer rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-muted hover:text-primary",
activeId === release.anchor
? "bg-muted font-medium text-primary"
: "text-muted-foreground"
)}
>
v{release.version} - {release.date}
</button>
</li>
))}
</ul>
</nav>
</SheetContent>
</Sheet>
</div>
)
}
16 changes: 16 additions & 0 deletions src/components/uiable/changelog/changelog-toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client"

// project
import TableOfContents from "@/components/uiable/layout/table-of-contents"
import { CHANGELOG_DATA } from "@/data/changelog-data"

// ------------------------------ | COMPONENT - CHANGELOG TOC | ------------------------------ //

export default function ChangelogToc() {
const tocItems = CHANGELOG_DATA.map((release) => ({
title: `v${release.version} - ${release.date}`,
url: `#${release.anchor}`,
}))

return <TableOfContents items={tocItems} />
}
Loading