Skip to content

Commit

Permalink
Merge pull request #510 from ably/reduce-tw-class-generation
Browse files Browse the repository at this point in the history
[WEB-4025] Add useTheming hook, reduce generated TW classes
  • Loading branch information
jamiehenson authored Oct 16, 2024
2 parents 3324e40 + 0a4107b commit 8ef79cc
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 157 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ yarn-error.log
.idea/*
types
index.d.ts
computed-colors-*.json
computed-icons.ts
computed-icons.ts
computed-colors.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ably/ui",
"version": "14.6.9",
"version": "14.7.0",
"description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.",
"repository": {
"type": "git",
Expand Down
129 changes: 70 additions & 59 deletions scripts/compute-colors.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,83 @@
import fs from "fs";
import path from "path";
import {
numericalColors,
variants,
prefixes,
Theme,
ComputedColors,
} from "../src/core/styles/colors/types";
import { invertTailwindClassVariant } from "../src/core/styles/colors/utils";
import { colors, prefixes, variants } from "../src/core/styles/colors/types";

const computeColors = (base: Theme) => {
if (base !== "dark" && base !== "light") {
throw new Error(`Invalid base theme: ${base}. Expected "dark" or "light".`);
}
const directoryPath = path.join(__dirname, "../src");
const outputPath = path.join(
__dirname,
"../src/core/styles/colors",
"computed-colors.json",
);

const joinedVariants = variants.join("|");
const joinedPrefixes = prefixes.join("|");
const joinedColors = colors.join("|");
const regex = new RegExp(
`themeColor\\("((${joinedVariants}${joinedPrefixes})-(${joinedColors})-(000|[1-9]00|1[0-3]00))"\\)`,
"g",
);

const findStringInFiles = (dir: string) => {
const results: string[] = [];

const readDirectory = (dir: string) => {
let files: string[];
try {
files = fs.readdirSync(dir);
} catch (error) {
console.error(`Error reading directory ${dir}:`, error);
return;
}

files.forEach((file) => {
const filePath = path.join(dir, file);
let stat;
try {
stat = fs.statSync(filePath);
} catch (error) {
console.error(`Error accessing ${filePath}:`, error);
return;
}

const colors = {} as ComputedColors;
if (stat.isDirectory()) {
readDirectory(filePath);
} else if (filePath.endsWith(".tsx")) {
let content = "";
try {
content = fs.readFileSync(filePath, "utf-8");
const matches = [...content.matchAll(regex)].map((match) => match[1]);

variants.forEach((variant) =>
prefixes.forEach((property) =>
numericalColors.forEach((colorSet) =>
colorSet.map((color, index) => {
if (base === "dark") {
colors[`${variant}${property}-${colorSet[index]}`] = {
light: `${variant}${property}-${colorSet[colorSet.length - index - 1]}`,
};
} else if (base === "light") {
colors[`${variant}${property}-${colorSet[index]}`] = {
dark: `${variant}${property}-${colorSet[colorSet.length - index - 1]}`,
};
if (matches.length > 0) {
results.push(...matches);
}
}),
),
),
);
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return;
}
}
});
};

return colors;
readDirectory(dir);
return Array.from(new Set(results)).sort();
};

const darkOutputPath = path.join(
__dirname,
"../src/core/styles/colors",
"computed-colors-dark.json",
);
const lightOutputPath = path.join(
__dirname,
"../src/core/styles/colors",
"computed-colors-light.json",
const matches = findStringInFiles(directoryPath);

const flippedMatches = matches.map((match) =>
invertTailwindClassVariant(match),
);

async function writeComputedColors() {
try {
await Promise.all([
fs.promises.writeFile(
darkOutputPath,
JSON.stringify(computeColors("dark"), null, 2),
"utf-8",
),
fs.promises.writeFile(
lightOutputPath,
JSON.stringify(computeColors("light"), null, 2),
"utf-8",
),
]);
console.log(
`🎨 Tailwind theme classes have been computed and written to JSON files.`,
);
} catch {
console.error(`Error persisting computed colors.`);
try {
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputPath, JSON.stringify(flippedMatches));
console.log(
`🎨 Tailwind theme classes have been computed and written to JSON files.`,
);
} catch (error) {
console.error(`Error persisting computed colors:`, error);
}

writeComputedColors();
13 changes: 8 additions & 5 deletions src/core/Icon/EncapsulatedIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import Icon, { IconProps } from "../Icon";
import { determineThemeColor } from "../styles/colors/utils";
import { ColorClass, Theme } from "../styles/colors/types";
import useTheming from "../hooks/useTheming";
import { Theme } from "../styles/colors/types";

type EncapsulatedIconProps = {
theme?: Theme;
Expand All @@ -18,19 +18,22 @@ const EncapsulatedIcon = ({
innerClassName,
...iconProps
}: EncapsulatedIconProps) => {
const t = (color: ColorClass) => determineThemeColor("dark", theme, color);
const { themeColor } = useTheming({
baseTheme: "dark",
theme,
});
const numericalSize = parseInt(size, 10);
const numericalIconSize = iconSize
? parseInt(iconSize, 10)
: numericalSize - 12;

return (
<div
className={`p-1 rounded-lg ${theme === "light" ? "bg-gradient-to-t" : "bg-gradient-to-b"} ${t("from-neutral-900")} ${className ?? ""}`}
className={`p-1 rounded-lg ${theme === "light" ? "bg-gradient-to-t" : "bg-gradient-to-b"} ${themeColor("from-neutral-900")} ${className ?? ""}`}
style={{ width: numericalSize, height: numericalSize }}
>
<div
className={`flex items-center justify-center rounded-lg ${t("bg-neutral-1100")} ${innerClassName ?? ""}`}
className={`flex items-center justify-center rounded-lg ${themeColor("bg-neutral-1100")} ${innerClassName ?? ""}`}
style={{ height: numericalSize - 2 }}
>
<Icon size={`${numericalIconSize}`} {...iconProps} />
Expand Down
50 changes: 29 additions & 21 deletions src/core/Pricing/PricingCards.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { Fragment, useEffect, useRef, useState } from "react";
import throttle from "lodash.throttle";
import type { PricingDataFeature } from "./types";
import { determineThemeColor } from "../styles/colors/utils";
import { ColorClass, Theme } from "../styles/colors/types";
import Icon from "../Icon";
import FeaturedLink from "../FeaturedLink";
import { IconName } from "../Icon/types";
import Tooltip from "../Tooltip";
import useTheming from "../hooks/useTheming";

export type PricingCardsProps = {
data: PricingDataFeature[];
Expand Down Expand Up @@ -45,8 +45,10 @@ const PricingCards = ({
};
}, []);

// work out a dynamic theme colouring, using dark theme colouring as the base
const t = (color: ColorClass) => determineThemeColor("dark", theme, color);
const { themeColor } = useTheming({
baseTheme: "dark",
theme,
});

const delimiterColumn = (index: number) =>
delimiter && index % 2 === 1 ? (
Expand All @@ -57,7 +59,7 @@ const PricingCards = ({
<Icon
name={delimiter}
size="20"
additionalCSS={t("text-neutral-500")}
additionalCSS={themeColor("text-neutral-500")}
/>
) : null}
</div>
Expand Down Expand Up @@ -95,7 +97,7 @@ const PricingCards = ({
<Fragment key={title.content}>
{delimiterColumn(index)}
<div
className={`relative border ${t(borderClasses(border?.color)?.border ?? "border-neutral-1100")} ${border?.style ?? ""} flex-1 px-24 py-32 flex flex-col gap-24 rounded-2xl group ${delimiter ? "@[520px]:flex-row @[920px]:flex-col" : ""} min-w-[272px] backdrop-blur`}
className={`relative border ${themeColor(borderClasses(border?.color)?.border ?? "border-neutral-1100")} ${border?.style ?? ""} flex-1 px-24 py-32 flex flex-col gap-24 rounded-2xl group ${delimiter ? "@[520px]:flex-row @[920px]:flex-col" : ""} min-w-[272px] backdrop-blur`}
data-testid={
delimiter ? "delimited-pricing-card" : "pricing-card"
}
Expand All @@ -108,15 +110,15 @@ const PricingCards = ({
</div>
) : null}
<div
className={`absolute z-0 top-0 left-0 w-full h-full rounded-2xl ${t("bg-neutral-1300")} ${!delimiter ? `${t("group-hover:bg-neutral-1200")} group-hover:opacity-100` : ""} transition-[colors,opacity] opacity-25`}
className={`absolute z-0 top-0 left-0 w-full h-full rounded-2xl ${themeColor("bg-neutral-1300")} ${!delimiter ? `${themeColor("group-hover:bg-neutral-1200")} group-hover:opacity-100` : ""} transition-[colors,opacity] opacity-25`}
></div>
<div
className={`relative z-10 flex flex-col gap-24 ${delimiter ? "@[520px]:flex-1 @[920px]:flex-none" : ""}`}
>
<div>
<div className="flex items-center mb-12">
<p
className={`${title.className ?? ""} ${t(title.color ?? "text-neutral-000")}`}
className={`${title.className ?? ""} ${themeColor(title.color ?? "text-neutral-000")}`}
>
{title.content}
</p>
Expand All @@ -130,7 +132,7 @@ const PricingCards = ({
) : null}
</div>
<p
className={`ui-text-p1 ${description.className ?? ""} ${t(description.color ?? "text-neutral-000")} min-h-20`}
className={`ui-text-p1 ${description.className ?? ""} ${themeColor(description.color ?? "text-neutral-000")} min-h-20`}
style={{ height: descriptionHeight }}
>
<span ref={(el) => (descriptionsRef.current[index] = el)}>
Expand All @@ -142,18 +144,20 @@ const PricingCards = ({
className={`flex items-end gap-8 ${delimiter ? "@[520px]:flex-col @[520px]:items-start @[920px]:flex-row @[920px]:items-end" : ""}`}
>
<p
className={`ui-text-title font-medium tracking-tight leading-none ${t("text-neutral-000")}`}
className={`ui-text-title font-medium tracking-tight leading-none ${themeColor("text-neutral-000")}`}
>
{price.amount}
</p>
<div className={`ui-text-p3 ${t("text-neutral-000")}`}>
<div
className={`ui-text-p3 ${themeColor("text-neutral-000")}`}
>
{price.content}
</div>
</div>
{cta ? (
<div className="group">
<FeaturedLink
additionalCSS={`text-center ui-btn ${t("bg-neutral-000")} ${t("text-neutral-1300")} hover:text-neutral-000 px-24 !py-12 ${cta.className ?? ""} cursor-pointer`}
additionalCSS={`text-center ui-btn ${themeColor("bg-neutral-000")} ${themeColor("text-neutral-1300")} hover:text-neutral-000 px-24 !py-12 ${cta.className ?? ""} cursor-pointer`}
url={cta.url}
onClick={cta.onClick}
disabled={cta.disabled}
Expand All @@ -163,15 +167,17 @@ const PricingCards = ({
</div>
) : delimiter ? null : (
<div className="flex items-center justify-center h-48 w-full">
<hr className={`${t("border-neutral-800")} w-64`} />
<hr
className={`${themeColor("border-neutral-800")} w-64`}
/>
</div>
)}
</div>
<div className="flex-1 flex flex-col gap-24 relative z-10">
{sections.map(({ title, items, listItemColors, cta }) => (
<div key={title} className="flex flex-col gap-12">
<p
className={`${t("text-neutral-500")} font-mono uppercase text-overline2 tracking-[0.16em]`}
className={`${themeColor("text-neutral-500")} font-mono uppercase text-overline2 tracking-[0.16em]`}
>
{title}
</p>
Expand All @@ -180,12 +186,12 @@ const PricingCards = ({
Array.isArray(item) ? (
<div
key={item[0]}
className={`flex justify-between gap-16 px-8 -mx-8 ${index === 0 ? "py-8" : "py-4"} ${index > 0 && index % 2 === 0 ? `${t("bg-blue-900")} rounded-md` : ""}`}
className={`flex justify-between gap-16 px-8 -mx-8 ${index === 0 ? "py-8" : "py-4"} ${index > 0 && index % 2 === 0 ? `${themeColor("bg-blue-900")} rounded-md` : ""}`}
>
{item.map((subItem, subIndex) => (
<span
key={subItem}
className={`ui-text-p3 ${index === 0 ? "font-bold" : "font-medium"} ${t("text-neutral-300")} ${subIndex % 2 === 1 ? "text-right" : ""}`}
className={`ui-text-p3 ${index === 0 ? "font-bold" : "font-medium"} ${themeColor("text-neutral-300")} ${subIndex % 2 === 1 ? "text-right" : ""}`}
>
{subItem}
</span>
Expand All @@ -196,14 +202,16 @@ const PricingCards = ({
{listItemColors ? (
<Icon
name="icon-gui-check-circled-fill"
color={t(listItemColors.background)}
secondaryColor={t(listItemColors.foreground)}
color={themeColor(listItemColors.background)}
secondaryColor={themeColor(
listItemColors.foreground,
)}
size="16"
additionalCSS="mt-2"
/>
) : null}
<div
className={`flex-1 ${listItemColors ? "ui-text-p3" : "ui-text-p2"} font-medium ${t("text-neutral-300")}`}
className={`flex-1 ${listItemColors ? "ui-text-p3" : "ui-text-p2"} font-medium ${themeColor("text-neutral-300")}`}
>
{item}
</div>
Expand All @@ -215,16 +223,16 @@ const PricingCards = ({
<div className="relative -mx-24 flex items-center h-40 overflow-x-hidden">
<FeaturedLink
url={cta.url}
additionalCSS={`absolute sm:-translate-x-120 sm:opacity-0 sm:group-hover:translate-x-24 duration-300 delay-0 sm:group-hover:delay-100 sm:group-hover:opacity-100 transition-[transform,opacity] font-medium ui-text-p3 ${t("text-neutral-500")} hover:${t("text-neutral-000")} cursor-pointer`}
additionalCSS={`absolute sm:-translate-x-120 sm:opacity-0 sm:group-hover:translate-x-24 duration-300 delay-0 sm:group-hover:delay-100 sm:group-hover:opacity-100 transition-[transform,opacity] font-medium ui-text-p3 ${themeColor("text-neutral-500")} hover:${themeColor("text-neutral-000")} cursor-pointer`}
onClick={cta.onClick}
iconColor={t(
iconColor={themeColor(
listItemColors?.foreground ?? "text-white",
)}
>
{cta.text}
</FeaturedLink>
<div
className={`absolute sm:translate-x-24 sm:opacity-100 sm:group-hover:translate-x-120 sm:group-hover:opacity-0 duration-200 delay-100 sm:group-hover:delay-0 transition-[transform,opacity] leading-6 tracking-widen-0.15 font-light text-p3 ${t("text-neutral-800")}`}
className={`absolute sm:translate-x-24 sm:opacity-100 sm:group-hover:translate-x-120 sm:group-hover:opacity-0 duration-200 delay-100 sm:group-hover:delay-0 transition-[transform,opacity] leading-6 tracking-widen-0.15 font-light text-p3 ${themeColor("text-neutral-800")}`}
>
•••
</div>
Expand Down
Loading

0 comments on commit 8ef79cc

Please sign in to comment.