Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add modern system font stacks #4123

Merged
merged 16 commits into from
Sep 18, 2024
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
Loading