mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
v5.1.0 (#2970)
* chore(release): v5.1.0 * feat: implement resume thumbnails * fix: remove unused mcp tools * docs: fix formatting of docs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user