* chore(release): v5.1.0

* feat: implement resume thumbnails

* fix: remove unused mcp tools

* docs: fix formatting of docs
This commit is contained in:
Amruth Pillai
2026-05-07 15:12:33 +02:00
committed by GitHub
parent 51c366310e
commit 50ba37a27f
1015 changed files with 106087 additions and 141872 deletions
@@ -0,0 +1,78 @@
import type { MultiComboboxProps, SingleComboboxProps } from "@/components/ui/combobox";
import { useMemo } from "react";
import { fontList, getFont, getFontDisplayName, getFontSearchKeywords } from "@reactive-resume/fonts";
import { cn } from "@reactive-resume/utils/style";
import { Combobox } from "@/components/ui/combobox";
import { FontDisplay } from "./font-display";
type Weight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
export function getNextWeights(fontFamily: string): Weight[] | null {
const fontData = getFont(fontFamily);
if (!fontData || !Array.isArray(fontData.weights) || fontData.weights.length === 0) return null;
const uniqueWeights = Array.from(new Set(fontData.weights)) as Weight[];
// Try to pick 400 and 600 if available
const weights: Weight[] = [];
if (uniqueWeights.includes("400")) weights.push("400");
if (uniqueWeights.includes("600")) weights.push("600");
// If we didn't find both, fill in with first/last, ensuring uniqueness
while (weights.length < 2 && uniqueWeights.length > 0) {
// candidateIndex: 0 (first), 1 (last)
const lastIndex = uniqueWeights.length - 1;
const candidate = weights.length === 0 ? uniqueWeights[0] : uniqueWeights[lastIndex];
if (!weights.includes(candidate)) weights.push(candidate);
else break;
}
return weights.length > 0 ? weights : null;
}
type FontFamilyComboboxProps = Omit<SingleComboboxProps, "options">;
export function FontFamilyCombobox({ className, ...props }: FontFamilyComboboxProps) {
const options = useMemo(() => {
return fontList.map((font) => ({
value: font.family,
keywords: getFontSearchKeywords(font.family),
label: (
<FontDisplay
family={font.family}
label={getFontDisplayName(font.family)}
type={font.type}
url={"preview" in font ? font.preview : undefined}
/>
),
}));
}, []);
return <Combobox {...props} options={options} className={cn("w-full", className)} />;
}
type FontWeightComboboxProps = Omit<MultiComboboxProps, "options" | "multiple"> & { fontFamily: string };
export function FontWeightCombobox({ fontFamily, ...props }: FontWeightComboboxProps) {
const options = useMemo(() => {
const fontData = getFont(fontFamily);
let weights: string[] = [];
if (fontData && Array.isArray(fontData.weights) && fontData.weights.length > 0) {
weights = fontData.weights as string[];
} else {
// Fallback to all possible weights
weights = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
}
return weights.map((variant: string) => ({
value: variant,
label: variant,
keywords: [variant],
}));
}, [fontFamily]);
return <Combobox {...props} multiple options={options} />;
}
@@ -0,0 +1,44 @@
import { useInView } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@reactive-resume/utils/style";
interface FontDisplayProps {
family: string;
label: string;
type: "standard" | "web";
url?: string;
}
const loadedFonts = new Set<string>();
export function FontDisplay({ family, label, type, url }: FontDisplayProps) {
const previewName = type === "standard" ? family : `${family} Preview`;
const containerRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(() => type === "standard" || loadedFonts.has(previewName));
const isInView = useInView(containerRef, { once: true, amount: 0.1, margin: "50px" });
useEffect(() => {
if (!isInView || isLoaded || !url) return;
const fontFace = new FontFace(previewName, `url(${url})`, { display: "swap" });
void fontFace.load().then((loadedFace) => {
if (!document.fonts.has(loadedFace)) document.fonts.add(loadedFace);
loadedFonts.add(previewName);
setIsLoaded(true);
});
}, [isInView, isLoaded, previewName, url]);
return (
<div ref={containerRef} className="inline-flex items-baseline gap-2">
<span
style={{ fontFamily: isLoaded ? `'${previewName}', sans-serif` : "sans-serif" }}
className={cn(isLoaded ? "opacity-100" : "opacity-50", "transition-opacity duration-200 ease-in")}
>
{label}
</span>
{label !== family && <span className="text-muted-foreground text-xs">{family}</span>}
</div>
);
}