Skip to content

Commit

Permalink
feat: add modern system font stacks (#4123)
Browse files Browse the repository at this point in the history
Closes #4121

## Description

We discovered this repository of awesome system fonts with fallbacks,
well organized.

https://github.com/system-fonts/modern-font-stacks

Assumption here is that most people don't actually need a very specific
font rendered, but rather need a type of a font and they are perfectly
fine with fallbacks, but without this awesome, tested fallbacks list,
how can you know which fallback to use.

With this we will encourage more people to use system fonts.

<img width="482" alt="image"
src="https://github.com/user-attachments/assets/2253c198-284b-4e3e-bbc2-44fa9af3c14b">


## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
  • Loading branch information
kof authored Sep 18, 2024
1 parent 45ca543 commit 30981bd
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const FontFamilyControl = () => {
return toValue(value, (value) => value).replace(/"/g, "");
}, [value]);

if (value.type !== "fontFamily") {
return;
}

return (
<Flex>
<Combobox<Item>
Expand All @@ -69,8 +73,8 @@ export const FontFamilyControl = () => {
title="Fonts"
content={
<FontsManager
value={toValue(value)}
onChange={(newValue) => {
value={value}
onChange={(newValue = itemValue) => {
setValue({ type: "fontFamily", value: [newValue] });
}}
/>
Expand Down
99 changes: 81 additions & 18 deletions apps/builder/app/builder/shared/fonts-manager/fonts-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@ import {
theme,
useSearchFieldKeys,
findNextListItemIndex,
Tooltip,
Text,
rawTheme,
Link,
Flex,
} from "@webstudio-is/design-system";
import {
AssetsShell,
deleteAssets,
Separator,
useAssets,
} from "~/builder/shared/assets";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useMenu } from "./item-menu";
import { CheckMarkIcon } from "@webstudio-is/icons";
import { CheckMarkIcon, InfoCircleIcon } from "@webstudio-is/icons";
import {
type Item,
filterIdsByFamily,
filterItems,
groupItemsByType,
toItems,
} from "./item-utils";
import type { FontFamilyValue } from "@webstudio-is/css-engine";

const useLogic = ({
onChange,
value,
}: {
onChange: (value: string) => void;
value: string;
}) => {
const useLogic = ({ onChange, value }: FontsManagerProps) => {
const { assetContainers } = useAssets("font");
const [selectedIndex, setSelectedIndex] = useState(-1);
const fontItems = useMemo(() => toItems(assetContainers), [assetContainers]);
Expand Down Expand Up @@ -61,16 +61,14 @@ const useLogic = ({
() => groupItemsByType(filteredItems),
[filteredItems]
);
const [currentIndex, setCurrentIndex] = useState(-1);

useEffect(() => {
setCurrentIndex(groupedItems.findIndex((item) => item.label === value));
const currentIndex = useMemo(() => {
return groupedItems.findIndex((item) => item.label === value.value[0]);
}, [groupedItems, value]);

const handleChangeCurrent = (nextCurrentIndex: number) => {
const item = groupedItems[nextCurrentIndex];
if (item !== undefined) {
setCurrentIndex(nextCurrentIndex);
onChange(item.label);
}
};
Expand All @@ -88,7 +86,7 @@ const useLogic = ({
const ids = filterIdsByFamily(family, assetContainers);
deleteAssets(ids);
if (index === currentIndex) {
setCurrentIndex(-1);
onChange(undefined);
}
};

Expand All @@ -106,8 +104,8 @@ const useLogic = ({
};

type FontsManagerProps = {
value: string;
onChange: (value: string) => void;
value: FontFamilyValue;
onChange: (value?: string) => void;
};

export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
Expand Down Expand Up @@ -137,7 +135,41 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
{...itemProps}
key={key}
prefix={itemProps.current ? <CheckMarkIcon /> : undefined}
suffix={item.type === "uploaded" ? renderMenu(index) : undefined}
suffix={
item.type === "uploaded" ? (
renderMenu(index)
) : itemProps.state === "selected" && item.description ? (
<Tooltip
variant="wrapped"
content={
<Flex
direction="column"
gap="2"
css={{ maxWidth: theme.spacing[28] }}
>
<Text variant="titles">{item.label}</Text>
<Text
variant="monoBold"
color="moreSubtle"
userSelect="text"
css={{
whiteSpace: "break-spaces",
cursor: "text",
}}
>
{`font-family: ${item.stack.join(", ")};`}
</Text>
<Text>{item.description}</Text>
</Flex>
}
>
<InfoCircleIcon
tabIndex={0}
color={rawTheme.colors.foregroundSubtle}
/>
</Tooltip>
) : undefined
}
>
{item.label}
</DeprecatedListItem>
Expand Down Expand Up @@ -167,7 +199,38 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
{uploadedItems.length !== 0 && (
<Separator css={{ mx: theme.spacing[9] }} />
)}
<DeprecatedListItem state="disabled">{"System"}</DeprecatedListItem>
<DeprecatedListItem
state="disabled"
suffix={
<Tooltip
variant="wrapped"
content={
<Text>
{
"System font stack CSS organized by typeface classification for every modern OS. No downloading, no layout shifts, no flashes— just instant renders. Learn more about "
}
<Link
href="https://github.com/system-fonts/modern-font-stacks"
target="_blank"
color="inherit"
variant="inherit"
>
modern font stacks
</Link>
.
</Text>
}
>
<InfoCircleIcon
tabIndex={0}
color={rawTheme.colors.foregroundSubtle}
style={{ pointerEvents: "auto" }}
/>
</Tooltip>
}
>
System
</DeprecatedListItem>
</>
)}
{systemItems.map((item, index) =>
Expand Down
16 changes: 12 additions & 4 deletions apps/builder/app/builder/shared/fonts-manager/item-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import type { AssetContainer } from "../assets";
export type Item = {
label: string;
type: "uploaded" | "system";
description?: string;
stack: Array<string>;
};

export const toItems = (
assetContainers: Array<AssetContainer>
): Array<Item> => {
const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({
label,
type: "system",
}));
// We can have 2+ assets with the same family name, so we use a map to dedupe.
const uploaded = new Map();
for (const assetContainer of assetContainers) {
Expand All @@ -30,6 +28,16 @@ export const toItems = (
});
}
}

const system = [];
for (const [label, config] of SYSTEM_FONTS) {
system.push({
label,
type: "system",
description: config.description,
stack: config.stack,
});
}
return [...uploaded.values(), ...system];
};

Expand Down
17 changes: 10 additions & 7 deletions packages/css-engine/src/core/to-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ describe("Convert WS CSS Values to native CSS strings", () => {
});

test("fontFamily", () => {
const value = toValue({
type: "fontFamily",
value: ["Courier New"],
});
expect(value).toBe('"Courier New", monospace');
expect(
toValue({
type: "fontFamily",
value: ["Humanist"],
})
).toBe(
'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif'
);
});

test("Transform font family value to override default fallback", () => {
Expand All @@ -64,12 +67,12 @@ describe("Convert WS CSS Values to native CSS strings", () => {
if (styleValue.type === "fontFamily") {
return {
type: "fontFamily",
value: [styleValue.value[0]],
value: ["A B"],
};
}
}
);
expect(value).toBe('"Courier New"');
expect(value).toBe('"A B"');
});

test("array", () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/css-engine/src/core/to-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ export type TransformValue = (styleValue: StyleValue) => undefined | StyleValue;

const fallbackTransform: TransformValue = (styleValue) => {
if (styleValue.type === "fontFamily") {
const firstFontFamily = styleValue.value[0];

const fontFamily = styleValue.value;
const fallbacks = SYSTEM_FONTS.get(firstFontFamily) ?? [
const fonts = SYSTEM_FONTS.get(styleValue.value[0])?.stack ?? [
DEFAULT_FONT_FALLBACK,
];
const value = Array.from(new Set([...fontFamily, ...fallbacks]));
const value = Array.from(new Set(fonts));

return {
type: "fontFamily",
Expand Down
30 changes: 12 additions & 18 deletions packages/design-system/src/components/__DEPRECATED__/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,13 @@ const ListItemBase = styled("li", {
listStyle: "none",
outline: 0,
position: "relative",
variants: {
state: {
disabled: {
pointerEvents: "none",
},
selected: {
"&:before": {
content: "''",
position: "absolute",
pointerEvents: "none",
inset: `0 ${theme.spacing[3]}`,
borderRadius: theme.borderRadius[4],
border: `2px solid ${theme.colors.borderFocus}`,
},
},
},
"&[aria-selected]::before": {
content: "''",
position: "absolute",
pointerEvents: "none",
inset: `0 ${theme.spacing[3]}`,
borderRadius: theme.borderRadius[4],
border: `2px solid ${theme.colors.borderFocus}`,
},
});

Expand All @@ -66,9 +57,9 @@ export const DeprecatedListItem = forwardRef<
return (
<ListItemBase
ref={ref}
state={state}
tabIndex={state === "disabled" ? -1 : 0}
role="option"
{...(state === "disabled" ? { "aria-disabled": true } : undefined)}
{...(state === "selected" ? { "aria-selected": true } : undefined)}
{...(current ? { "aria-current": true } : undefined)}
{...props}
Expand All @@ -82,7 +73,7 @@ export const DeprecatedListItem = forwardRef<
<Text
variant="labelsSentenceCase"
truncate
color={state === "disabled" ? "disabled" : "main"}
color={state === "disabled" ? "subtle" : "main"}
>
{children}
</Text>
Expand Down Expand Up @@ -123,6 +114,9 @@ export const useDeprecatedList = ({
onMouseEnter() {
onSelect(index);
},
onMouseLeave() {
onSelect(-1);
},
onClick() {
onChangeCurrent(index);
},
Expand Down
Loading

0 comments on commit 30981bd

Please sign in to comment.