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
3 changes: 2 additions & 1 deletion apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DocsRoot, isAssistantEnabled } from 'docora';
import docsConfig from '../docs.config';
import { rootMetadata, rootViewport } from '../lib/metadata';
import { flattenSections } from '../lib/navigation';
import { isNewHref } from '../lib/new-items';
import { source } from '../lib/source';
import './globals.css';

Expand All @@ -14,7 +15,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
return (
<DocsRoot
config={docsConfig}
navigation={flattenSections(await source.getNavigation())}
navigation={flattenSections(await source.getNavigation(), isNewHref)}
assistantEnabled={isAssistantEnabled(docsConfig.assistant)}
>
{children}
Expand Down
5 changes: 3 additions & 2 deletions apps/docs/components/mdx/component-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { prefersReducedMotion } from '@/lib/motion';
import { isNewItem } from '@/lib/new-items';
import { hrefFor } from '@/lib/registry';
import { cn } from '@/lib/utils';
import type { RegistryEntry } from '@/registry/schema';
Expand Down Expand Up @@ -63,8 +64,8 @@ export function ComponentCard({ entry }: { entry: RegistryEntry }) {
>
<div className="flex items-start justify-between gap-2">
<h3 className="text-highlighted text-sm font-semibold">{entry.title}</h3>
{entry.isNew && (
<span className="rounded-full border border-[#045f64]/30 bg-[#045f64]/10 px-2 py-0.5 text-[10px] font-semibold tracking-wide text-[#045f64] uppercase">
{isNewItem(entry.name) && (
<span className="border-tween-accent/30 bg-tween-accent/10 text-tween-accent rounded-full border px-2 py-0.5 text-[10px] font-semibold tracking-wide uppercase">
New
</span>
)}
Expand Down
15 changes: 2 additions & 13 deletions apps/docs/components/mdx/component-gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,15 @@ const GRID = 'grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3';
const byTitle = (a: RegistryEntry, b: RegistryEntry) => a.title.localeCompare(b.title);

/**
* Gallery page. Renders the registry as cards, with a "New" section first
* (like the reference layout), then all components, then all blocks.
* Gallery page. Renders the registry as cards: all components, then all blocks.
* The newest of each wear a "New" badge on their card and in the sidebar.
*/
export function ComponentGallery() {
const components = registry.filter((e) => e.type === 'component').sort(byTitle);
const blocks = registry.filter((e) => e.type === 'block').sort(byTitle);
const newest = components.filter((e) => e.isNew);

return (
<div className="not-prose mt-10 flex flex-col gap-14">
{newest.length > 0 && (
<GallerySection title="New" count={newest.length}>
<div className={GRID}>
{newest.map((entry) => (
<ComponentCard key={entry.name} entry={entry} />
))}
</div>
</GallerySection>
)}

<GallerySection title="All components" count={components.length}>
<div className={GRID}>
{components.map((entry) => (
Expand Down
8 changes: 7 additions & 1 deletion apps/docs/components/nav/sidebar-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { NavItem } from 'docora';
import gsap from 'gsap';
import type { TweenNavItem } from '@/lib/navigation';
import { cn } from '@/lib/utils';
import { AnimatedIcon } from './animated-icon';

Expand All @@ -24,7 +25,7 @@ function NavTree({
onNavigate,
rows,
}: Readonly<{
items: NavItem[];
items: TweenNavItem[];
level: number;
onNavigate?: () => void;
rows: RowState;
Expand Down Expand Up @@ -72,6 +73,11 @@ function NavTree({
/>
)}
<span className="truncate">{item.label}</span>
{item.isNew && (
<span className="border-tween-accent/30 bg-tween-accent/10 text-tween-accent ms-auto shrink-0 rounded-full border px-1.5 py-px text-[9px] leading-[14px] font-semibold tracking-wide uppercase">
New
</span>
)}
</Link>
) : (
<span className={cn(ROW, 'text-highlighted font-semibold')}>
Expand Down
18 changes: 16 additions & 2 deletions apps/docs/lib/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { NavItem } from 'docora';

/** A sidebar row, plus whether its page is one of the newest items. */
export type TweenNavItem = Omit<NavItem, 'children'> & {
isNew?: boolean;
children?: TweenNavItem[];
};

/** Collect every linkable leaf page beneath the given nav items, depth-first. */
function collectLeaves(items: NavItem[]): NavItem[] {
return items.flatMap((item) =>
Expand All @@ -26,8 +32,14 @@ const isCatalog = (leaves: NavItem[]) =>
*
* The on-disk folder structure (and therefore page URLs) is untouched — this
* only reshapes what the sidebar renders.
*
* `isNew` picks the catalog pages that get a "New" badge; it is passed in so
* this stays a pure reshape of whatever tree it is given.
*/
export function flattenSections(items: NavItem[]): NavItem[] {
export function flattenSections(
items: NavItem[],
isNew: (href: string) => boolean = () => false
): TweenNavItem[] {
return items.map((item) => {
// Only transform section groups: no link of their own, but with children.
if (item.href !== undefined || !item.children || item.children.length === 0) {
Expand All @@ -42,7 +54,9 @@ export function flattenSections(items: NavItem[]): NavItem[] {
return {
...item,
label: `${item.label} (${leaves.length})`,
children: leaves.sort((a, b) => a.label.localeCompare(b.label)),
children: leaves
.sort((a, b) => a.label.localeCompare(b.label))
.map((leaf) => (leaf.href && isNew(leaf.href) ? { ...leaf, isNew: true } : leaf)),
};
});
}
41 changes: 41 additions & 0 deletions apps/docs/lib/new-items.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { hrefFor, registry } from '@/registry/index';
import type { RegistryEntry, RegistryType } from '@/registry/schema';

/** How many items of each type wear the "New" badge at once. */
export const NEW_LIMIT = 4;

/**
* The newest `limit` entries of one type, newest first. Ordered by `addedAt`;
* items added the same day fall back to registry order, later entries first,
* so the one appended last always counts as the newest. Adding a fifth item
* therefore pushes the oldest of the four out.
*/
export function newestOfType(
entries: readonly RegistryEntry[],
type: RegistryType,
limit = NEW_LIMIT
): RegistryEntry[] {
return entries
.map((entry, index) => ({ entry, index }))
.filter(({ entry }) => entry.type === type)
.sort((a, b) => b.entry.addedAt.localeCompare(a.entry.addedAt) || b.index - a.index)
.slice(0, limit)
.map(({ entry }) => entry);
}

/** Names of the newest components and the newest blocks, `NEW_LIMIT` of each. */
export function newestNames(entries: readonly RegistryEntry[], limit = NEW_LIMIT): Set<string> {
return new Set(
(['component', 'block'] as const).flatMap((type) =>
newestOfType(entries, type, limit).map((entry) => entry.name)
)
);
}

const NEW_NAMES = newestNames(registry);
const NEW_HREFS = new Set(registry.filter((e) => NEW_NAMES.has(e.name)).map(hrefFor));

export const isNewItem = (name: string) => NEW_NAMES.has(name);

/** Doc URL form, for the sidebar, which only knows each page by its href. */
export const isNewHref = (href: string) => NEW_HREFS.has(href);
Loading
Loading