Skip to content

Commit

Permalink
feat: support custom fonts (#96)
Browse files Browse the repository at this point in the history
Co-authored-by: Tom Lienard <[email protected]>
  • Loading branch information
rgodha24 and QuiiBz authored Sep 1, 2024
1 parent edc00ec commit 008cbe5
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 80 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ db.sqlite*
.DS_Store
*.log
.env
.env*
*.tsbuildinfo
2 changes: 2 additions & 0 deletions apps/dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vercel
.env*.local
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@vercel/kv": "^1.0.1",
"@vercel/speed-insights": "^1.0.10",
"clsx": "^2.1.1",
"fuse.js": "^7.0.0",
"next": "14.2.7",
"next-themes": "^0.3.0",
"react": "^18",
Expand Down
7 changes: 7 additions & 0 deletions apps/dashboard/src/app/api/fonts/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getFontData } from "../../../lib/fonts";

export const dynamic = "force-static";

export async function GET() {
return Response.json(await getFontData());
}
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/FontPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect } from "react";
import { maybeLoadFont, type Font } from "../lib/fonts";
import { maybeLoadFont } from "../lib/fonts";

interface FontPreviewProps {
font: Font;
font: string;
}

export function FontPreview({ font }: FontPreviewProps) {
Expand Down
64 changes: 36 additions & 28 deletions apps/dashboard/src/components/RightPanel/FontSection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Flex, Grid, Text, Select, TextField, Tooltip } from "@radix-ui/themes";
import type { OGElement } from "../../lib/types";
import type { Font } from "../../lib/fonts";
import { FONTS, FONT_WEIGHTS } from "../../lib/fonts";
import { FontSizeIcon } from "../icons/FontSizeIcon";
import { LineHeightIcon } from "../icons/LineHeightIcon";
import { LetterSpacingIcon } from "../icons/LetterSpacingIcon";
import { useElementsStore } from "../../stores/elementsStore";
import { ColorPicker } from "../ColorPicker";
import { FontPreview } from "../FontPreview";
import { useFontsStore } from "../../stores/fontsStore";
import { FontSelector } from "./FontSelector";

const SPACES_REGEX = /\s+/g;

Expand All @@ -17,6 +17,7 @@ interface FontSectionProps {

export function FontSection({ selectedElement }: FontSectionProps) {
const updateElement = useElementsStore((state) => state.updateElement);
const { allFonts, installedFonts, installFont } = useFontsStore();

if (selectedElement.tag !== "p" && selectedElement.tag !== "span") {
return null;
Expand All @@ -25,50 +26,36 @@ export function FontSection({ selectedElement }: FontSectionProps) {
return (
<Flex direction="column" gap="2">
<Text size="1">Font</Text>
<Grid columns="2" gap="2">
<Flex direction="row" gap="2" className="justify-between">
<Select.Root
onValueChange={(value) => {
const font = value as unknown as Font;
onValueChange={(font) => {
const weights = allFonts.find(({ name }) => name === font)?.weights;
if (!installedFonts.has(font)) {
installFont(font);
}

updateElement({
...selectedElement,
fontFamily: font,
fontWeight: FONT_WEIGHTS[font].includes(
selectedElement.fontWeight,
)
fontWeight: weights?.includes(selectedElement.fontWeight)
? selectedElement.fontWeight
: 400,
});
}}
value={selectedElement.fontFamily}
>
<Select.Trigger color="gray" variant="soft" />
<Select.Trigger color="gray" variant="soft" className="flex-1" />
<Select.Content variant="soft">
{FONTS.map((font) => (
{Array.from(installedFonts).map((font) => (
<Select.Item key={font} value={font}>
<FontPreview font={font} />
</Select.Item>
))}
</Select.Content>
</Select.Root>
<Select.Root
onValueChange={(value) => {
updateElement({
...selectedElement,
fontWeight: Number(value),
});
}}
value={String(selectedElement.fontWeight)}
>
<Select.Trigger color="gray" variant="soft" />
<Select.Content variant="soft">
{FONT_WEIGHTS[selectedElement.fontFamily].map((weight) => (
<Select.Item key={weight} value={String(weight)}>
{weight}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<FontSelector selectedElement={selectedElement} />
</Flex>
<Grid columns="2" gap="2">
<TextField.Root
color="gray"
onChange={(event) => {
Expand All @@ -88,6 +75,27 @@ export function FontSection({ selectedElement }: FontSectionProps) {
</Tooltip>
<TextField.Slot>px</TextField.Slot>
</TextField.Root>
<Select.Root
onValueChange={(value) => {
updateElement({
...selectedElement,
fontWeight: Number(value),
});
}}
value={String(selectedElement.fontWeight)}
>
<Select.Trigger color="gray" variant="soft" />
<Select.Content variant="soft">
{allFonts
.find(({ name }) => name === selectedElement.fontFamily)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the ? is definitely required
?.weights?.map((weight) => (
<Select.Item key={weight} value={String(weight)}>
{weight}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<ColorPicker
onChange={(color) => {
updateElement({
Expand Down
83 changes: 83 additions & 0 deletions apps/dashboard/src/components/RightPanel/FontSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Popover, Button, TextField, Flex, Badge } from "@radix-ui/themes";
import Fuse from "fuse.js";
import { useMemo, useState } from "react";
import { useFontsStore } from "../../stores/fontsStore";
import { useElementsStore } from "../../stores/elementsStore";
import { FontPreview } from "../FontPreview";
import type { OGElement } from "../../lib/types";
import { useDebounce } from "../../lib/hooks/useDebounce";
import { DEFAULT_FONTS } from "../../lib/fonts";

interface FontSelectorProps {
selectedElement: OGElement & { tag: "p" | "span" };
}

export function FontSelector({ selectedElement }: FontSelectorProps) {
const { allFonts, installedFonts, installFont } = useFontsStore();
const updateElement = useElementsStore((state) => state.updateElement);
const fuse = useMemo(
() => new Fuse(allFonts, { keys: ["name"] }),
[allFonts],
);
const [search, setSearch] = useState("");
const [isOpen, setIsOpen] = useState(false);
const debouncedSearch = useDebounce(search, 200);

const searchedFonts = fuse
.search(debouncedSearch)
.slice(0, 8)
.map(({ item }) => item.name);

return (
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger>
<Button size="2" variant="soft" color="gray">
+
</Button>
</Popover.Trigger>
<Popover.Content width="300px">
<Flex gap="3" direction="column">
<TextField.Root
placeholder="Search any fontsource font..."
value={search}
onChange={(event) => {
setSearch(event.target.value);
}}
/>
<Flex gap="1" direction="column">
{searchedFonts.map((font) => (
<Button
variant="soft"
color="gray"
key={font}
onClick={() => {
installFont(font);
const weights = allFonts.find(
({ name }) => name === font,
)?.weights;

updateElement({
...selectedElement,
fontFamily: font,
fontWeight: weights?.includes(selectedElement.fontWeight)
? selectedElement.fontWeight
: 400,
});

setIsOpen(false);
}}
>
<FontPreview font={font} />
{DEFAULT_FONTS.includes(font) ? (
<Badge color="blue">Pre-installed</Badge>
) : installedFonts.has(font) ? (
<Badge color="green">Installed</Badge>
) : null}
</Button>
))}
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
72 changes: 69 additions & 3 deletions apps/dashboard/src/lib/__tests__/fonts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ describe("maybeLoadFont", () => {
maybeLoadFont("Roboto", 400);

expect(document.head.innerHTML).toMatchInlineSnapshot(
`"<link id="font-Roboto-400" rel="stylesheet" href="https://fonts.bunny.net/css?family=roboto:400">"`,
`
"<style id="font-roboto-400">
@font-face {
font-family: "Roboto";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff") format("woff");
font-weight: 400;
font-style: normal;
}
</style>"
`,
);
});

Expand All @@ -16,7 +25,16 @@ describe("maybeLoadFont", () => {
maybeLoadFont("Roboto", 400);

expect(document.head.innerHTML).toMatchInlineSnapshot(
`"<link id="font-Roboto-400" rel="stylesheet" href="https://fonts.bunny.net/css?family=roboto:400">"`,
`
"<style id="font-roboto-400">
@font-face {
font-family: "Roboto";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff") format("woff");
font-weight: 400;
font-style: normal;
}
</style>"
`,
);
});

Expand All @@ -26,7 +44,30 @@ describe("maybeLoadFont", () => {
maybeLoadFont("Roboto", 700);

expect(document.head.innerHTML).toMatchInlineSnapshot(
`"<link id="font-Roboto-400" rel="stylesheet" href="https://fonts.bunny.net/css?family=roboto:400"><link id="font-Roboto-500" rel="stylesheet" href="https://fonts.bunny.net/css?family=roboto:500"><link id="font-Roboto-700" rel="stylesheet" href="https://fonts.bunny.net/css?family=roboto:700">"`,
`
"<style id="font-roboto-400">
@font-face {
font-family: "Roboto";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff") format("woff");
font-weight: 400;
font-style: normal;
}
</style><style id="font-roboto-500">
@font-face {
font-family: "Roboto";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-500-normal.woff") format("woff");
font-weight: 500;
font-style: normal;
}
</style><style id="font-roboto-700">
@font-face {
font-family: "Roboto";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-700-normal.woff") format("woff");
font-weight: 700;
font-style: normal;
}
</style>"
`,
);
});
});
Expand Down Expand Up @@ -74,6 +115,26 @@ describe("loadFonts", () => {
fontSize: 50,
align: "left",
},
{
id: createElementId(),
tag: "p",
name: "Text",
x: 0,
y: 0,
width: 100,
height: 50,
visible: true,
rotate: 0,
blur: 0,
content: "Text",
color: "#000000",
fontFamily: "Monaspace Radon",
fontWeight: 500,
lineHeight: 1,
letterSpacing: 0,
fontSize: 50,
align: "left",
},
]);

expect(data).toMatchInlineSnapshot(`
Expand All @@ -88,6 +149,11 @@ describe("loadFonts", () => {
"name": "Roboto",
"weight": 500,
},
{
"data": ArrayBuffer [],
"name": "Monaspace Radon",
"weight": 500,
},
]
`);
});
Expand Down
Loading

0 comments on commit 008cbe5

Please sign in to comment.