diff --git a/.changeset/strange-pillows-repair.md b/.changeset/strange-pillows-repair.md
new file mode 100644
index 00000000..e993de19
--- /dev/null
+++ b/.changeset/strange-pillows-repair.md
@@ -0,0 +1,5 @@
+---
+"@cambly/syntax-core": minor
+---
+
+Typography/Heading: add Cambio styles & options
diff --git a/packages/syntax-core/src/Checkbox/Checkbox.tsx b/packages/syntax-core/src/Checkbox/Checkbox.tsx
index 0e9fd554..ca376fa0 100644
--- a/packages/syntax-core/src/Checkbox/Checkbox.tsx
+++ b/packages/syntax-core/src/Checkbox/Checkbox.tsx
@@ -125,7 +125,7 @@ const Checkbox = ({
/>
{label}
diff --git a/packages/syntax-core/src/Heading/Heading.stories.tsx b/packages/syntax-core/src/Heading/Heading.stories.tsx
index ddf49440..56d701e4 100644
--- a/packages/syntax-core/src/Heading/Heading.stories.tsx
+++ b/packages/syntax-core/src/Heading/Heading.stories.tsx
@@ -26,6 +26,7 @@ export default {
color: {
options: [
"destructive-primary",
+ "black",
"gray700",
"gray900",
"primary",
@@ -34,10 +35,17 @@ export default {
],
control: { type: "radio" },
},
- size: {
- options: [500, 600, 700, 800],
+ lineClamp: {
+ control: { type: "number", min: 0, max: 10, step: 1 },
+ },
+ fontStyle: {
+ options: ["serif", "sans-serif"],
control: { type: "radio" },
},
+ size: {
+ options: [400, 500, 600, 700, 800, 900, 1000, 1100],
+ control: { type: "select" },
+ },
},
tags: ["autodocs"],
} as Meta;
@@ -51,6 +59,9 @@ export const Default: StoryObj = {
export const Sizes: StoryObj = {
render: (args) => (
<>
+
+ Size 400 (Cambio only)
+
Size 500
@@ -63,6 +74,15 @@ export const Sizes: StoryObj = {
Size 800
+
+ Size 900 (Cambio only)
+
+
+ Size 1000 (Cambio only)
+
+
+ Size 1100 (Cambio only)
+
>
),
};
diff --git a/packages/syntax-core/src/Heading/Heading.tsx b/packages/syntax-core/src/Heading/Heading.tsx
index 2655fd88..101808ca 100644
--- a/packages/syntax-core/src/Heading/Heading.tsx
+++ b/packages/syntax-core/src/Heading/Heading.tsx
@@ -1,6 +1,6 @@
import { type ReactElement, type ReactNode } from "react";
-import { type Color } from "../constants";
import Typography from "../Typography/Typography";
+import { useTheme } from "../ThemeProvider/ThemeProvider";
/**
* [Heading](https://cambly-syntax.vercel.app/?path=/docs/components-heading--docs) enforces a consistent style & accessibility best practices for headings.
@@ -11,6 +11,7 @@ const Heading = ({
children,
color = "gray900",
"data-testid": dataTestId,
+ fontStyle,
lineClamp,
size = 500,
}: {
@@ -37,11 +38,26 @@ const Heading = ({
*
* @defaultValue "gray900"
*/
- color?: (typeof Color)[number];
+ color?:
+ | "gray900"
+ | "gray700"
+ | "primary"
+ | "destructive-primary"
+ | "success"
+ | "white"
+ | "inherit";
/**
* Test id for the text.
*/
"data-testid"?: string;
+ /**
+ * Style of the font
+ *
+ * Classic only supports `sans-serif`
+ *
+ * @defaultValue "sans-serif"
+ */
+ fontStyle?: "serif" | "sans-serif";
/**
* The number of lines we should truncate the text at
*/
@@ -49,25 +65,50 @@ const Heading = ({
/**
* Size of the text.
*
+ * Classic:
* * `500`: 20px
* * `600`: 28px
* * `700`: 40px
* * `800`: 64px
*
+ * Cambio Mobile:
+ * * `400`: 20px
+ * * `500`: 23px
+ * * `600`: 26px
+ * * `700`: 29px
+ * * `800`: 33px
+ * * `900`: 37px
+ * * `1000`: 41px
+ * * `1100`: 46px
+ *
+ * Cambio Desktop (viewport width > 480px):
+ * * `400`: 25px
+ * * `500`: 31px
+ * * `600`: 39px
+ * * `700`: 49px
+ * * `800`: 61px
+ * * `900`: 76px
+ * * `1000`: 95px
+ * * `1100`: 119px
+ *
* @defaultValue 500
*/
- size?: 500 | 600 | 700 | 800;
+ size?: 400 | 500 | 600 | 700 | 800 | 900 | 1000 | 1100;
}): ReactElement => {
- const weight = [700, 800].includes(size) ? "heavy" : "bold";
+ const { themeName } = useTheme();
+ const classicWeight = [700, 800].includes(size) ? "heavy" : "bold";
+ const cambioWeight = fontStyle === "serif" ? "medium" : "regular";
+
return (
{children}
diff --git a/packages/syntax-core/src/RadioButton/RadioButton.tsx b/packages/syntax-core/src/RadioButton/RadioButton.tsx
index 8c56d1ca..8df70228 100644
--- a/packages/syntax-core/src/RadioButton/RadioButton.tsx
+++ b/packages/syntax-core/src/RadioButton/RadioButton.tsx
@@ -138,7 +138,7 @@ const RadioButton = ({
{label && (
{label}
diff --git a/packages/syntax-core/src/ThemeProvider/ThemeProvider.test.tsx b/packages/syntax-core/src/ThemeProvider/ThemeProvider.test.tsx
index afff5c4e..2c7ecc33 100644
--- a/packages/syntax-core/src/ThemeProvider/ThemeProvider.test.tsx
+++ b/packages/syntax-core/src/ThemeProvider/ThemeProvider.test.tsx
@@ -44,6 +44,9 @@ describe("themeProvider", () => {
expect(screen.getByTestId("themeprovider-style")).toContainHTML(
"--color-base-gray-10: rgba(203, 203, 203, 0.5);",
);
+ expect(screen.getByTestId("themeprovider-style")).toContainHTML(
+ "--elevation-400: 0px 16px 32px 0px #00000040;",
+ );
expect(screen.getByTestId("themeprovider-style")).not.toContainHTML(
"cambio",
);
@@ -68,6 +71,9 @@ describe("themeProvider", () => {
expect(screen.getByTestId("themeprovider-style")).toContainHTML(
"--color-base-primary-100: #faf4eb;",
);
+ expect(screen.getByTestId("themeprovider-style")).not.toContainHTML(
+ "elevation",
+ );
expect(screen.getByTestId("themeprovider-style")).toContainHTML("cambio");
});
});
diff --git a/packages/syntax-core/src/ThemeProvider/ThemeProvider.tsx b/packages/syntax-core/src/ThemeProvider/ThemeProvider.tsx
index 033d2cc9..601dcf98 100644
--- a/packages/syntax-core/src/ThemeProvider/ThemeProvider.tsx
+++ b/packages/syntax-core/src/ThemeProvider/ThemeProvider.tsx
@@ -90,9 +90,14 @@ function stylesForTheme(themeName: ThemeName) {
],
];
}
+ // `elevation` is a classic only concept
+ if (themeName === "cambio" && key.includes("elevation")) {
+ return [null, null];
+ }
return [key, value];
})
- .map(([key, value]) => `--${key}: ${value};`)
+ .map(([key, value]) => (key && value ? `--${key}: ${value};` : null))
+ .filter(Boolean)
.join("\n")}
}
`;
diff --git a/packages/syntax-core/src/Typography/Typography.module.css b/packages/syntax-core/src/Typography/Typography.module.css
index a84f136e..4b5ac8ef 100644
--- a/packages/syntax-core/src/Typography/Typography.module.css
+++ b/packages/syntax-core/src/Typography/Typography.module.css
@@ -1,9 +1,32 @@
.typography {
+ margin: 0;
+}
+
+@font-face {
+ font-family: GT-Super-Text-Medium;
+ font-style: normal;
+ font-weight: 500;
+ src: local("GT-Super-Text-Medium"),
+ /* TODO fix CORS issues and change to url("https://static.cambly.com/fonts/GT-Super-Text-Medium.woff2") */
+ url("https://partners.rebelmouse.com/protocol/GT-Super-Text-Medium.woff2")
+ format("woff2");
+
+ /* TODO enable Most performant option - see https://web.dev/articles/font-best-practices */
+
+ /* font-display: optional; */
+}
+
+.sansSerif {
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
margin: 0;
}
+.serif {
+ font-family: GT-Super-Text-Medium, -apple-system, system-ui,
+ BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, serif;
+}
+
/* Sizes */
.size100 {
font-size: 12px;
@@ -33,6 +56,89 @@
font-size: 64px;
}
+/* Sizes - Cambio sizes are responsive */
+.size100Cambio {
+ font-size: 14px;
+}
+
+.size200Cambio {
+ font-size: 16px;
+}
+
+.size300Cambio {
+ font-size: 18px;
+}
+
+.size400Cambio {
+ font-size: 20px;
+}
+
+.size500Cambio {
+ font-size: 23px;
+}
+
+.size600Cambio {
+ font-size: 26px;
+}
+
+.size700Cambio {
+ font-size: 33px;
+}
+
+.size800Cambio {
+ font-size: 41px;
+}
+
+.size900Cambio {
+ font-size: 46px;
+}
+
+@media (min-width: 480px) {
+ .size100Cambio {
+ font-size: 13px;
+ }
+
+ .size200Cambio {
+ font-size: 16px;
+ }
+
+ .size300Cambio {
+ font-size: 20px;
+ }
+
+ .size400Cambio {
+ font-size: 25px;
+ }
+
+ .size500Cambio {
+ font-size: 31px;
+ }
+
+ .size600Cambio {
+ font-size: 39px;
+ }
+
+ .size700Cambio {
+ font-size: 49px;
+ }
+
+ .size800Cambio {
+ font-size: 61px;
+ }
+
+ .size900Cambio {
+ font-size: 76px;
+ }
+
+ .size1000Cambio {
+ font-size: 95px;
+ }
+
+ .size1100Cambio {
+ font-size: 119px;
+ }
+}
+
/* Align */
.center {
text-align: center;
@@ -75,6 +181,18 @@
font-weight: 860;
}
+.regularCambio {
+ font-weight: 400;
+}
+
+.mediumCambio {
+ font-weight: 510;
+}
+
+.semiBoldCambio {
+ font-weight: 590;
+}
+
.underline {
text-decoration: underline;
}
diff --git a/packages/syntax-core/src/Typography/Typography.stories.tsx b/packages/syntax-core/src/Typography/Typography.stories.tsx
index 68d6d4e4..704a8279 100644
--- a/packages/syntax-core/src/Typography/Typography.stories.tsx
+++ b/packages/syntax-core/src/Typography/Typography.stories.tsx
@@ -23,10 +23,12 @@ export default {
color: {
options: [
"destructive-primary",
+ "destructive-darkBackground",
"gray700",
"gray900",
"primary",
"success",
+ "success-darkBackground",
"white",
],
control: { type: "radio" },
@@ -34,6 +36,10 @@ export default {
children: {
control: "text",
},
+ fontStyle: {
+ options: ["serif", "sans-serif"],
+ control: { type: "radio" },
+ },
inline: {
control: "boolean",
},
@@ -41,8 +47,8 @@ export default {
control: { type: "number", min: 0, max: 10, step: 1 },
},
size: {
- options: [100, 200, 300, 500, 600, 700, 800],
- control: { type: "radio" },
+ options: [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100],
+ control: { type: "select" },
},
tooltip: {
control: "text",
@@ -55,7 +61,14 @@ export default {
control: "boolean",
},
weight: {
- options: ["regular", "interactive", "semiBold", "bold", "heavy"],
+ options: [
+ "regular",
+ "interactive",
+ "medium",
+ "semiBold",
+ "bold",
+ "heavy",
+ ],
control: { type: "radio" },
},
},
@@ -63,62 +76,76 @@ export default {
} as Meta;
export const Default: StoryObj = {
- render: (args) => (
- {args.children ?? "Default text"}
- ),
+ args: { children: "Default text" },
};
-export const Sizes: StoryObj = {
- render: (args) => (
+export const sizes: StoryObj = {
+ render: () => (
<>
-
+
Size 100
-
- Size 200 (default)
+
+ Size 200
-
+
Size 300
-
- Size 500 Bold
+
+ Size 400 (Cambio only)
+
+
+ Size 500
-
- Size 600 Bold
+
+ Size 600
-
- Size 700 Heavy
+
+ Size 700
-
- Size 800 Heavy
+
+ Size 800
+
+
+ Size 900 (Cambio only)
+
+
+ Size 1000 (Cambio only)
+
+
+ Size 1100 (Cambio only)
>
),
};
-export const Colors: StoryObj = {
- render: (args) => (
+export const colors: StoryObj = {
+ render: () => (
<>
-
+
Color destructive-primary
-
+
+ Color destructive-darkBackground (Cambio only)
+
+
Color gray700
-
+
Color gray900 (default)
-
+
Color primary
-
+
Color success
-
-
- Color white
-
-
+
+ Color success-darkBackground (Cambio only)
+
+
+ Color white
+
>
),
};
@@ -161,16 +188,19 @@ export const Weight: StoryObj = {
Weight Regular
- Weight interactive
+ Weight interactive (classic only)
+
+
+ Weight medium (cambio only)
Weight semiBold
- Weight bold
+ Weight bold (classic only)
- Weight heavy
+ Weight heavy (classic only)
>
),
diff --git a/packages/syntax-core/src/Typography/Typography.tsx b/packages/syntax-core/src/Typography/Typography.tsx
index 38142c5d..4cc59bb0 100644
--- a/packages/syntax-core/src/Typography/Typography.tsx
+++ b/packages/syntax-core/src/Typography/Typography.tsx
@@ -1,10 +1,21 @@
import classNames from "classnames";
import { forwardRef, type ReactElement, type ReactNode } from "react";
-import { type Color } from "../constants";
import styles from "./Typography.module.css";
import colorStyles from "../colors/colors.module.css";
+import { useTheme } from "../ThemeProvider/ThemeProvider";
-function textColor(color: (typeof Color)[number]): string {
+function classicTextColor(
+ color:
+ | "gray900"
+ | "gray700"
+ | "primary"
+ | "destructive-primary"
+ | "destructive-darkBackground"
+ | "success"
+ | "success-darkBackground"
+ | "white"
+ | "inherit",
+): string {
switch (color) {
case "gray700":
return colorStyles.gray700Color;
@@ -15,6 +26,7 @@ function textColor(color: (typeof Color)[number]): string {
case "primary":
return colorStyles.primary700Color;
case "destructive-primary":
+ case "destructive-darkBackground":
return colorStyles.destructive700Color;
case "success":
return colorStyles.success700Color;
@@ -23,6 +35,64 @@ function textColor(color: (typeof Color)[number]): string {
}
}
+function cambioTextColor(
+ color:
+ | "gray900"
+ | "gray700"
+ | "primary"
+ | "destructive-primary"
+ | "destructive-darkBackground"
+ | "success"
+ | "success-darkBackground"
+ | "white"
+ | "inherit",
+): string {
+ switch (color) {
+ case "gray700":
+ return colorStyles.cambioGray800Color;
+ case "white":
+ return colorStyles.cambioWhiteColor;
+ case "inherit":
+ return colorStyles.inheritColor;
+ case "destructive-primary":
+ return colorStyles.cambioDestructive900Color;
+ case "destructive-darkBackground":
+ return colorStyles.cambioDestructive100Color;
+ case "success":
+ return colorStyles.cambioSuccess900Color;
+ case "success-darkBackground":
+ return colorStyles.cambioSuccess100Color;
+ // primary / gray900
+ default:
+ return colorStyles.cambioBlackColor;
+ }
+}
+
+function classicWeight(
+ weight: "regular" | "interactive" | "medium" | "semiBold" | "bold" | "heavy",
+): "regular" | "interactive" | "semiBold" | "bold" | "heavy" {
+ switch (weight) {
+ case "medium":
+ return "regular";
+ default:
+ return weight;
+ }
+}
+
+function cambioWeight(
+ weight: "regular" | "interactive" | "medium" | "semiBold" | "bold" | "heavy",
+): "regular" | "medium" | "semiBold" {
+ switch (weight) {
+ case "interactive":
+ return "medium";
+ case "bold":
+ case "heavy":
+ return "regular";
+ default:
+ return weight;
+ }
+}
+
/**
* [Typography](https://cambly-syntax.vercel.app/?path=/docs/components-typography--docs) is a component that renders text.
*/
@@ -50,13 +120,32 @@ const Typography = forwardRef<
/**
* The color of the text.
*
+ * Cambio only: `success-darkBackground` / `destructive-darkBackground`
+ *
* @defaultValue "gray900"
*/
- color?: (typeof Color)[number];
+ color?:
+ | "gray900"
+ | "gray700"
+ | "primary"
+ | "destructive-primary"
+ | "destructive-darkBackground"
+ | "success"
+ | "success-darkBackground"
+ | "white"
+ | "inherit";
/**
* Test id for the text
*/
"data-testid"?: string;
+ /**
+ * Style of the font
+ *
+ * Classic only supports `sans-serif`
+ *
+ * @defaultValue "sans-serif"
+ */
+ fontStyle?: "serif" | "sans-serif";
/**
* The id for the element
*/
@@ -74,6 +163,7 @@ const Typography = forwardRef<
/**
* Size of the text.
*
+ * Classic:
* * `100`: 12px
* * `200`: 14px
* * `300`: 16px
@@ -82,9 +172,35 @@ const Typography = forwardRef<
* * `700`: 40px
* * `800`: 64px
*
+ * Cambio Mobile:
+ * * `100`: 14px
+ * * `200`: 16px
+ * * `300`: 18px
+ * * `400`: 20px
+ * * `500`: 23px
+ * * `600`: 26px
+ * * `700`: 29px
+ * * `800`: 33px
+ * * `900`: 37px
+ * * `1000`: 41px
+ * * `1100`: 46px
+ *
+ * Cambio Desktop (viewport width > 480px):
+ * * `100`: 13px
+ * * `200`: 16px
+ * * `300`: 20px
+ * * `400`: 25px
+ * * `500`: 31px
+ * * `600`: 39px
+ * * `700`: 49px
+ * * `800`: 61px
+ * * `900`: 76px
+ * * `1000`: 95px
+ * * `1100`: 119px
+ *
* @defaultValue 200
*/
- size?: 100 | 200 | 300 | 500 | 600 | 700 | 800;
+ size?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 1000 | 1100;
/**
* The tooltip to be displayed when the user hovers the text
*/
@@ -104,9 +220,27 @@ const Typography = forwardRef<
/**
* Indicates the boldness of the text.
*
+ * Classic:
+ * * `regular`: 400
+ * * `interactive`: 500 (Classic only)
+ * * `semiBold`: 600
+ * * `bold`: 700 (Classic only)
+ * * `heavy`: 860 (Classic only)
+ *
+ * Cambio:
+ * * `regular`: 400
+ * * `medium`: 510
+ * * `semiBold`: 590
+ *
* @defaultValue "regular"
*/
- weight?: "regular" | "interactive" | "semiBold" | "bold" | "heavy";
+ weight?:
+ | "regular"
+ | "interactive"
+ | "medium"
+ | "semiBold"
+ | "bold"
+ | "heavy";
}
>(function Typography(
{
@@ -115,6 +249,7 @@ const Typography = forwardRef<
children,
color = "gray900",
"data-testid": dataTestId,
+ fontStyle = "sans-serif",
id,
inline = false,
lineClamp = undefined,
@@ -128,16 +263,45 @@ const Typography = forwardRef<
): ReactElement {
const Tag = as;
+ const { themeName } = useTheme();
+
+ const weightStyles =
+ themeName === "classic"
+ ? styles[classicWeight(weight)]
+ : styles[`${cambioWeight(weight)}Cambio`];
+
return (