mirror of
https://github.com/NVIDIA-NeMo/DataDesigner
synced 2026-05-24 09:48:29 +00:00
Preserves tree from previous docs-website head: 5e47d33ea8. This branch is a CI-managed publish artifact like gh-pages; source provenance is tracked in commit messages rather than Git ancestry.
155 lines
5 KiB
TypeScript
155 lines
5 KiB
TypeScript
/**
|
|
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
import { authors as REGISTRY } from "./devnotes/authors-data";
|
|
|
|
/**
|
|
* Site basepath. Mirrors `instances[0].custom-domain` path in fern/docs.yml.
|
|
* Custom MDX components bypass Fern's link rewriter, so the card's `href`
|
|
* needs the prefix manually to avoid 404s under basepath-aware routing.
|
|
*
|
|
* Image paths are NOT prefixed — they should be passed in as ES-module imports
|
|
* from MDX (e.g. `import hero from "@/assets/foo/hero.png"`), which the
|
|
* bundler resolves to the correct URL in both dev and production.
|
|
*/
|
|
const BASEPATH = "/nemo/datadesigner";
|
|
|
|
/** Prepend BASEPATH to a root-relative path if not already present. */
|
|
function withBasepath(path: string): string {
|
|
if (!path.startsWith("/")) return path;
|
|
if (path.startsWith(BASEPATH + "/") || path === BASEPATH) return path;
|
|
return BASEPATH + path;
|
|
}
|
|
|
|
/**
|
|
* BlogCard — index card for a dev note / blog post.
|
|
*
|
|
* Renders a clickable tile with: optional hero image, ALL-CAPS date eyebrow,
|
|
* title, description, and an author byline (avatar stack + first author + "+N").
|
|
*
|
|
* Designed for the dev-notes landing index — Fern's built-in <Card> only does
|
|
* icon + title + description, which made every card visually identical.
|
|
*
|
|
* Usage in MDX (inside <BlogGrid>):
|
|
*
|
|
* import { BlogCard, BlogGrid } from "@/components/BlogCard";
|
|
*
|
|
* <BlogGrid>
|
|
* <BlogCard
|
|
* href="/dev-notes/push-datasets-to-hugging-face-hub"
|
|
* title="Push Datasets to Hugging Face Hub"
|
|
* description="Call .push_to_hub() and ship a generated dataset…"
|
|
* date="Apr 16, 2026"
|
|
* authors={["nmulepati", "davanstrien"]}
|
|
* image="/assets/push-datasets-to-hugging-face-hub/push-to-hub-hero.png"
|
|
* />
|
|
* </BlogGrid>
|
|
*/
|
|
|
|
export interface BlogCardProps {
|
|
href: string;
|
|
title: string;
|
|
description: string;
|
|
date: string;
|
|
authors?: string[];
|
|
/**
|
|
* Optional hero image element. Pass an `<img>` JSX node from MDX so Fern's
|
|
* MDX rewriter resolves the src to the correct dev/prod path (raw string
|
|
* paths bypass the rewriter and 404 in dev). Falls back to a deterministic
|
|
* hash-based gradient + monogram when omitted.
|
|
*
|
|
* <BlogCard image={<img src="/assets/foo/hero.png" alt="" />} … />
|
|
*/
|
|
image?: ReactNode;
|
|
}
|
|
|
|
/** Deterministic hash → number ∈ [0, 360). Same input → same color. */
|
|
function hashHue(input: string): number {
|
|
let h = 5381;
|
|
for (let i = 0; i < input.length; i++) {
|
|
h = ((h << 5) + h + input.charCodeAt(i)) | 0;
|
|
}
|
|
return Math.abs(h) % 360;
|
|
}
|
|
|
|
/** Build a 2-stop diagonal gradient that reads well in both light/dark.
|
|
* Hue is constrained to a band that pairs with NVIDIA green (avoid muddy
|
|
* yellows by skipping 40-90°). */
|
|
function placeholderGradient(seed: string): string {
|
|
let hue = hashHue(seed);
|
|
if (hue >= 40 && hue < 90) hue = (hue + 60) % 360;
|
|
const a = `hsl(${hue} 55% 38%)`;
|
|
const b = `hsl(${(hue + 35) % 360} 60% 22%)`;
|
|
return `linear-gradient(135deg, ${a} 0%, ${b} 100%)`;
|
|
}
|
|
|
|
/** First grapheme of the title (works for "🎨 Title" too). */
|
|
function monogramOf(title: string): string {
|
|
// Strip leading non-letter punctuation/whitespace then take 1 char.
|
|
const trimmed = title.replace(/^[^\p{L}\p{N}]+/u, "");
|
|
return Array.from(trimmed)[0]?.toUpperCase() ?? "·";
|
|
}
|
|
|
|
export function BlogCard({
|
|
href,
|
|
title,
|
|
description,
|
|
date,
|
|
authors = [],
|
|
image,
|
|
}: BlogCardProps) {
|
|
const validAuthors = authors.map((id) => REGISTRY[id]).filter(Boolean);
|
|
const primary = validAuthors[0];
|
|
const extra = validAuthors.length - 1;
|
|
|
|
return (
|
|
<a className="blog-card" href={withBasepath(href)}>
|
|
<div className="blog-card__media">
|
|
{image ? (
|
|
image
|
|
) : (
|
|
<div
|
|
className="blog-card__placeholder"
|
|
style={{ background: placeholderGradient(href) }}
|
|
aria-hidden="true"
|
|
>
|
|
<span className="blog-card__monogram">{monogramOf(title)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="blog-card__body">
|
|
<span className="blog-card__date">{date}</span>
|
|
<h3 className="blog-card__title">{title}</h3>
|
|
<p className="blog-card__description">{description}</p>
|
|
{primary && (
|
|
<div className="blog-card__byline">
|
|
<div className="blog-card__avatars">
|
|
{validAuthors.slice(0, 3).map((a, i) => (
|
|
<img
|
|
key={i}
|
|
className="blog-card__avatar"
|
|
src={a.avatar}
|
|
alt=""
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="blog-card__authors">
|
|
{primary.name}
|
|
{extra > 0 ? ` +${extra}` : ""}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
export function BlogGrid({ children }: { children: ReactNode }) {
|
|
return <div className="blog-grid">{children}</div>;
|
|
}
|