Generate color meta, document functions and tidy.

Nate Butler created

Change summary

styles/src/system/algorithm.ts    | 170 ---------
styles/src/system/curves.ts       | 175 ---------
styles/src/system/lib/convert.ts  |  11 
styles/src/system/lib/curve.ts    |  21 +
styles/src/system/lib/generate.ts | 157 ++++++++
styles/src/system/ref/color.ts    | 608 +++++++++++++++++---------------
styles/src/system/ref/curves.ts   |  25 +
styles/src/system/reference.ts    |   3 
styles/src/system/system.ts       |  23 +
styles/src/system/types.ts        |  60 ++
styles/theme-tool/app/page.tsx    |  40 -
11 files changed, 629 insertions(+), 664 deletions(-)

Detailed changes

styles/src/system/algorithm.ts 🔗

@@ -1,170 +0,0 @@
-// Adapted from @k-vyn/coloralgorithm
-
-import bezier from "bezier-easing";
-import chroma, { Scale } from "chroma-js";
-import { Curve } from "./curves";
-import { ColorFamily, ColorProps, ColorSet } from "./types";
-
-function validColor(color: string) {
-  if (chroma.valid(color)) {
-    return color;
-  } else {
-    throw new Error(`Invalid color: ${color}`);
-  }
-}
-
-function assignColor(scale: Scale, steps: number, step: number) {
-  const color = scale(step / steps);
-  const lch = color.lch();
-  const rgbaArray = color.rgba();
-  const hex = color.hex();
-
-  // Roughly  calculate if a color is dark or light
-  const isLight = lch[0] > 50;
-
-  const result = {
-    step,
-    hex,
-    lch,
-    rgbaArray,
-    isLight,
-  };
-
-  return result;
-}
-
-/** Outputs 101 colors (0-100) */
-export function generateColors(props: ColorProps, inverted: boolean) {
-  const steps = 101;
-  const colors: ColorSet = [];
-
-  const { start, middle, end } = props.color;
-
-  const startColor = typeof start === "string" ? validColor(start) : start;
-  const middleColor = typeof middle === "string" ? validColor(middle) : middle;
-  const endColor = typeof end === "string" ? validColor(end) : end;
-
-  // TODO: Use curve when generating colors
-
-  let scale: Scale;
-
-  if (inverted) {
-    scale = chroma.scale([endColor, middleColor, startColor]).mode("lch");
-  } else {
-    scale = chroma.scale([startColor, middleColor, endColor]).mode("lch");
-  }
-  for (let i = 0; i < steps; i++) {
-    const color = assignColor(scale, steps, i);
-    colors.push(color);
-  }
-  return colors;
-}
-
-export function generateColorsUsingCurve(
-  startColor: string,
-  endColor: string,
-  curve: number[]
-) {
-  const NUM_STEPS = 101;
-
-  const easing = bezier(curve[0], curve[1], curve[2], curve[3]);
-  const curveProgress = [];
-  for (let i = 0; i <= NUM_STEPS; i++) {
-    curveProgress.push(easing(i / NUM_STEPS));
-  }
-
-  const colors: chroma.Color[] = [];
-  for (let i = 0; i < NUM_STEPS; i++) {
-    // Use HSL as an input as it is easier to construct programatically
-    // const color = chroma.hsl();
-    const color = chroma.mix(startColor, endColor, curveProgress[i], "lch");
-    colors.push(color);
-  }
-
-  return colors;
-}
-
-export function generateColors2(
-  hue: {
-    start: number;
-    end: number;
-    curve: Curve;
-  },
-  saturation: {
-    start: number;
-    end: number;
-    curve: Curve;
-  },
-  lightness: {
-    start: number;
-    end: number;
-    curve: Curve;
-  }
-) {
-  const NUM_STEPS = 9;
-
-  const hueEasing = bezier(
-    hue.curve.value[0],
-    hue.curve.value[1],
-    hue.curve.value[2],
-    hue.curve.value[3]
-  );
-  const saturationEasing = bezier(
-    saturation.curve.value[0],
-    saturation.curve.value[1],
-    saturation.curve.value[2],
-    saturation.curve.value[3]
-  );
-  const lightnessEasing = bezier(
-    lightness.curve.value[0],
-    lightness.curve.value[1],
-    lightness.curve.value[2],
-    lightness.curve.value[3]
-  );
-
-  const colors: chroma.Color[] = [];
-  for (let i = 0; i < NUM_STEPS; i++) {
-    const hueValue =
-      hueEasing(i / NUM_STEPS) * (hue.end - hue.start) + hue.start;
-    const saturationValue =
-      saturationEasing(i / NUM_STEPS) * (saturation.end - saturation.start) +
-      saturation.start;
-    const lightnessValue =
-      lightnessEasing(i / NUM_STEPS) * (lightness.end - lightness.start) +
-      lightness.start;
-
-    const color = chroma.hsl(
-      hueValue,
-      saturationValue / 100,
-      lightnessValue / 100
-    );
-    colors.push(color);
-  }
-
-  const scale = chroma.scale(colors).mode("lch");
-  return scale;
-}
-
-/** Generates two color ramps:
- * One for for light, and one for dark.
- * By generating two ramps, rather than two default themes, we can use the same reference palette values for tokens in components.
- *
- * Each ramp has 101 colors (0-100)
- */
-export function generateColorSet(props: ColorProps) {
-  const generatedColors = generateColors(props, false);
-  const generatedInvertedColors = generateColors(props, true);
-
-  const colors = generatedColors.map((color) => color.hex);
-  const invertedColors = generatedInvertedColors.map((color) => color.hex);
-
-  const result: ColorFamily = {
-    name: props.name,
-    colors: colors,
-    invertedColors: invertedColors,
-    colorsMeta: generatedColors,
-    invertedMeta: generatedInvertedColors,
-  };
-
-  return result;
-}

styles/src/system/curves.ts 🔗

@@ -1,175 +0,0 @@
-// Adapted from @k-vyn/coloralgorithm
-
-export interface Curve {
-  name: string;
-  formatted_name: string;
-  value: number[];
-}
-
-export interface Curves {
-  lightness: Curve;
-  saturation: Curve;
-  linear: Curve;
-  easeInCubic: Curve;
-  easeOutCubic: Curve;
-  easeInOutCubic: Curve;
-  easeInSine: Curve;
-  easeOutSine: Curve;
-  easeInOutSine: Curve;
-  easeInQuad: Curve;
-  easeOutQuad: Curve;
-  easeInOutQuad: Curve;
-  easeInQuart: Curve;
-  easeOutQuart: Curve;
-  easeInOutQuart: Curve;
-  easeInQuint: Curve;
-  easeOutQuint: Curve;
-  easeInOutQuint: Curve;
-  easeInExpo: Curve;
-  easeOutExpo: Curve;
-  easeInOutExpo: Curve;
-  easeInCirc: Curve;
-  easeOutCirc: Curve;
-  easeInOutCirc: Curve;
-  easeInBack: Curve;
-  easeOutBack: Curve;
-  easeInOutBack: Curve;
-}
-
-export const curve: Curves = {
-  lightness: {
-    name: "nate",
-    formatted_name: "Nate",
-    value: [0.2, 0, 0.85, 1.1],
-  },
-  saturation: {
-    name: "nate",
-    formatted_name: "Nate",
-    value: [0.67, 0.6, 0.55, 1.0],
-  },
-  linear: {
-    name: "linear",
-    formatted_name: "Linear",
-    value: [0.5, 0.5, 0.5, 0.5],
-  },
-  easeInCubic: {
-    name: "easeInCubic",
-    formatted_name: "Cubic - EaseIn",
-    value: [0.55, 0.055, 0.675, 0.19],
-  },
-  easeOutCubic: {
-    name: "easeOutCubic",
-    formatted_name: "Cubic - EaseOut",
-    value: [0.215, 0.61, 0.355, 1],
-  },
-  easeInOutCubic: {
-    name: "easeInOutCubic",
-    formatted_name: "Cubic - EaseInOut",
-    value: [0.645, 0.045, 0.355, 1],
-  },
-  easeInSine: {
-    name: "easeInSine",
-    formatted_name: "Sine - EaseIn",
-    value: [0.47, 0, 0.745, 0.715],
-  },
-  easeOutSine: {
-    name: "easeOutSine",
-    formatted_name: "Sine - EaseOut",
-    value: [0.39, 0.575, 0.565, 1],
-  },
-  easeInOutSine: {
-    name: "easeInOutSine",
-    formatted_name: "Sine - EaseInOut",
-    value: [0.445, 0.05, 0.55, 0.95],
-  },
-  easeInQuad: {
-    name: "easeInQuad",
-    formatted_name: "Quad - EaseIn",
-    value: [0.55, 0.085, 0.68, 0.53],
-  },
-  easeOutQuad: {
-    name: "easeOutQuad",
-    formatted_name: "Quad - EaseOut",
-    value: [0.25, 0.46, 0.45, 0.94],
-  },
-  easeInOutQuad: {
-    name: "easeInOutQuad",
-    formatted_name: "Quad - EaseInOut",
-    value: [0.455, 0.03, 0.515, 0.955],
-  },
-  easeInQuart: {
-    name: "easeInQuart",
-    formatted_name: "Quart - EaseIn",
-    value: [0.895, 0.03, 0.685, 0.22],
-  },
-  easeOutQuart: {
-    name: "easeOutQuart",
-    formatted_name: "Quart - EaseOut",
-    value: [0.165, 0.84, 0.44, 1],
-  },
-  easeInOutQuart: {
-    name: "easeInOutQuart",
-    formatted_name: "Quart - EaseInOut",
-    value: [0.77, 0, 0.175, 1],
-  },
-  easeInQuint: {
-    name: "easeInQuint",
-    formatted_name: "Quint - EaseIn",
-    value: [0.755, 0.05, 0.855, 0.06],
-  },
-  easeOutQuint: {
-    name: "easeOutQuint",
-    formatted_name: "Quint - EaseOut",
-    value: [0.23, 1, 0.32, 1],
-  },
-  easeInOutQuint: {
-    name: "easeInOutQuint",
-    formatted_name: "Quint - EaseInOut",
-    value: [0.86, 0, 0.07, 1],
-  },
-  easeInCirc: {
-    name: "easeInCirc",
-    formatted_name: "Circ - EaseIn",
-    value: [0.6, 0.04, 0.98, 0.335],
-  },
-  easeOutCirc: {
-    name: "easeOutCirc",
-    formatted_name: "Circ - EaseOut",
-    value: [0.075, 0.82, 0.165, 1],
-  },
-  easeInOutCirc: {
-    name: "easeInOutCirc",
-    formatted_name: "Circ - EaseInOut",
-    value: [0.785, 0.135, 0.15, 0.86],
-  },
-  easeInExpo: {
-    name: "easeInExpo",
-    formatted_name: "Expo - EaseIn",
-    value: [0.95, 0.05, 0.795, 0.035],
-  },
-  easeOutExpo: {
-    name: "easeOutExpo",
-    formatted_name: "Expo - EaseOut",
-    value: [0.19, 1, 0.22, 1],
-  },
-  easeInOutExpo: {
-    name: "easeInOutExpo",
-    formatted_name: "Expo - EaseInOut",
-    value: [1, 0, 0, 1],
-  },
-  easeInBack: {
-    name: "easeInBack",
-    formatted_name: "Back - EaseIn",
-    value: [0.6, -0.28, 0.735, 0.045],
-  },
-  easeOutBack: {
-    name: "easeOutBack",
-    formatted_name: "Back - EaseOut",
-    value: [0.175, 0.885, 0.32, 1.275],
-  },
-  easeInOutBack: {
-    name: "easeInOutBack",
-    formatted_name: "Back - EaseInOut",
-    value: [0.68, -0.55, 0.265, 1.55],
-  },
-};

styles/src/system/lib/convert.ts 🔗

@@ -0,0 +1,11 @@
+/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */
+export function percentageToNormalized(value: number) {
+  const normalized = value / 100;
+  return normalized;
+}
+
+/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */
+export function normalizedToPercetage(value: number) {
+  const percentage = value * 100;
+  return percentage;
+}

styles/src/system/lib/curve.ts 🔗

@@ -0,0 +1,21 @@
+import bezier from "bezier-easing";
+import { Curve } from "../ref/curves";
+
+/**
+ * Formats our Curve data structure into a bezier easing function.
+ * @param {Curve} curve - The curve to format.
+ * @param {Boolean} inverted - Whether or not to invert the curve.
+ * @returns {EasingFunction} The formatted easing function.
+ */
+export function curve(curve: Curve, inverted?: Boolean) {
+  if (inverted) {
+    return bezier(
+      curve.value[3],
+      curve.value[2],
+      curve.value[1],
+      curve.value[0]
+    );
+  }
+
+  return bezier(curve.value[0], curve.value[1], curve.value[2], curve.value[3]);
+}

styles/src/system/lib/generate.ts 🔗

@@ -0,0 +1,157 @@
+import bezier from "bezier-easing";
+import chroma from "chroma-js";
+import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types";
+import { percentageToNormalized } from "./convert";
+import { curve } from "./curve";
+
+// Re-export interface in a more standard format
+export type EasingFunction = bezier.EasingFunction;
+
+/**
+ * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
+ *
+ * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
+ * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
+ * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
+ * @param {ColorFamilyConfig} family - Configuration for the color family.
+ * @param {number} step - The current step.
+ * @param {number} steps - The total number of steps in the color scale.
+ *
+ * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
+ */
+function generateColor(
+  hueEasing: EasingFunction,
+  saturationEasing: EasingFunction,
+  lightnessEasing: EasingFunction,
+  family: ColorFamilyConfig,
+  step: number,
+  steps: number
+) {
+  const { hue, saturation, lightness } = family.color;
+
+  const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start;
+  const stepSaturation =
+    saturationEasing(step / steps) * (saturation.end - saturation.start) +
+    saturation.start;
+  const stepLightness =
+    lightnessEasing(step / steps) * (lightness.end - lightness.start) +
+    lightness.start;
+
+  const color = chroma.hsl(
+    stepHue,
+    percentageToNormalized(stepSaturation),
+    percentageToNormalized(stepLightness)
+  );
+
+  const contrast = {
+    black: {
+      value: chroma.contrast(color, "black"),
+      aaPass: chroma.contrast(color, "black") >= 4.5,
+      aaaPass: chroma.contrast(color, "black") >= 7,
+    },
+    white: {
+      value: chroma.contrast(color, "white"),
+      aaPass: chroma.contrast(color, "white") >= 4.5,
+      aaaPass: chroma.contrast(color, "white") >= 7,
+    },
+  };
+
+  const lch = color.lch();
+  const rgba = color.rgba();
+  const hex = color.hex();
+
+  const isLight = lch[0] > 50;
+
+  const result: Color = {
+    step,
+    lch,
+    hex,
+    rgba,
+    contrast,
+    isLight,
+  };
+
+  return result;
+}
+
+/**
+ * Generates a color scale based on a color family configuration.
+ *
+ * @param {ColorFamilyConfig} config - The configuration for the color family.
+ * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
+ *
+ * @returns {ColorScale} The generated color scale.
+ *
+ * @example
+ * ```ts
+ * const colorScale = generateColorScale({
+ *   name: "blue",
+ *   color: {
+ *     hue: {
+ *       start: 210,
+ *       end: 240,
+ *       curve: "easeInOut"
+ *     },
+ *     saturation: {
+ *       start: 100,
+ *       end: 100,
+ *       curve: "easeInOut"
+ *     },
+ *     lightness: {
+ *       start: 50,
+ *       end: 50,
+ *       curve: "easeInOut"
+ *     }
+ *   }
+ * });
+ * ```
+ */
+
+export function generateColorScale(
+  config: ColorFamilyConfig,
+  inverted: Boolean = false
+) {
+  const { hue, saturation, lightness } = config.color;
+
+  // 101 steps means we get values from 0-100
+  const NUM_STEPS = 101;
+
+  const hueEasing = curve(hue.curve, inverted);
+  const saturationEasing = curve(saturation.curve, inverted);
+  const lightnessEasing = curve(lightness.curve, inverted);
+
+  let scale: ColorScale = {
+    colors: [],
+    values: [],
+  };
+
+  for (let i = 0; i < NUM_STEPS; i++) {
+    const color = generateColor(
+      hueEasing,
+      saturationEasing,
+      lightnessEasing,
+      config,
+      i,
+      NUM_STEPS
+    );
+
+    scale.colors.push(color);
+    scale.values.push(color.hex);
+  }
+
+  return scale;
+}
+
+/** Generates a color family with a scale and an inverted scale. */
+export function generateColorFamily(config: ColorFamilyConfig) {
+  const scale = generateColorScale(config, false);
+  const invertedScale = generateColorScale(config, true);
+
+  const family: ColorFamily = {
+    name: config.name,
+    scale,
+    invertedScale,
+  };
+
+  return family;
+}

styles/src/system/ref/color.ts 🔗

@@ -1,336 +1,376 @@
-import chroma from "chroma-js";
-import {
-  generateColors2,
-  generateColorSet,
-  generateColorsUsingCurve,
-} from "../algorithm";
-import { curve } from "../curves";
-import { ColorFamily } from "../types";
+import { generateColorFamily } from "../lib/generate";
+import { curve } from "./curves";
 
 // These are the source colors for the color scales in the system.
-// This should never directly be used in the system, or exported to be used in a component or theme
-// As it will generate thousands of lines of code.
+// These should never directly be used directly in components or themes as they generate thousands of lines of code.
 // Instead, use the outputs from the reference palette which exports a smaller subset of colors.
 
-// Token or user-facing colors should use short, clear names
-// and a 100-900 scale to match the font weight scale.
+// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale.
 
 // Red ======================================== //
 
-export const red = generateColors2(
-  {
-    start: 0,
-    end: 0,
-    curve: curve.linear,
-  },
-  {
-    start: 95,
-    end: 75,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const red = generateColorFamily({
+  name: "red",
+  color: {
+    hue: {
+      start: 0,
+      end: 0,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 95,
+      end: 75,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Sunset ======================================== //
 
-export const sunset = generateColors2(
-  {
-    start: 12,
-    end: 12,
-    curve: curve.linear,
-  },
-  {
-    start: 100,
-    end: 80,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const sunset = generateColorFamily({
+  name: "sunset",
+  color: {
+    hue: {
+      start: 12,
+      end: 12,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 100,
+      end: 80,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Orange ======================================== //
 
-export const orange = generateColors2(
-  {
-    start: 25,
-    end: 25,
-    curve: curve.linear,
-  },
-  {
-    start: 100,
-    end: 100,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const orange = generateColorFamily({
+  name: "orange",
+  color: {
+    hue: {
+      start: 25,
+      end: 25,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 100,
+      end: 100,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Amber ======================================== //
 
-export const amber = generateColors2(
-  {
-    start: 34,
-    end: 34,
-    curve: curve.linear,
-  },
-  {
-    start: 100,
-    end: 100,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const amber = generateColorFamily({
+  name: "amber",
+  color: {
+    hue: {
+      start: 34,
+      end: 34,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 100,
+      end: 100,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Yellow ======================================== //
 
-export const yellow = generateColors2(
-  {
-    start: 48,
-    end: 48,
-    curve: curve.linear,
-  },
-  {
-    start: 90,
-    end: 100,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 32,
-    curve: curve.lightness,
-  }
-);
+export const yellow = generateColorFamily({
+  name: "yellow",
+  color: {
+    hue: {
+      start: 48,
+      end: 48,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 90,
+      end: 100,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 28,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Citron ======================================== //
 
-export const citron = generateColors2(
-  {
-    start: 65,
-    end: 65,
-    curve: curve.linear,
-  },
-  {
-    start: 85,
-    end: 70,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const citron = generateColorFamily({
+  name: "citron",
+  color: {
+    hue: {
+      start: 65,
+      end: 65,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 85,
+      end: 70,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Lime ======================================== //
 
-export const lime = generateColors2(
-  {
-    start: 85,
-    end: 85,
-    curve: curve.linear,
-  },
-  {
-    start: 85,
-    end: 70,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const lime = generateColorFamily({
+  name: "lime",
+  color: {
+    hue: {
+      start: 85,
+      end: 85,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 85,
+      end: 70,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Green ======================================== //
 
-export const green = generateColors2(
-  {
-    start: 108,
-    end: 108,
-    curve: curve.linear,
-  },
-  {
-    start: 60,
-    end: 50,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 25,
-    curve: curve.lightness,
-  }
-);
+export const green = generateColorFamily({
+  name: "green",
+  color: {
+    hue: {
+      start: 108,
+      end: 108,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 60,
+      end: 50,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 25,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Mint ======================================== //
 
-export const mint = generateColors2(
-  {
-    start: 142,
-    end: 142,
-    curve: curve.linear,
-  },
-  {
-    start: 60,
-    end: 50,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const mint = generateColorFamily({
+  name: "mint",
+  color: {
+    hue: {
+      start: 142,
+      end: 142,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 60,
+      end: 50,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Cyan ======================================== //
 
-export const cyan = generateColors2(
-  {
-    start: 179,
-    end: 179,
-    curve: curve.linear,
-  },
-  {
-    start: 70,
-    end: 60,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const cyan = generateColorFamily({
+  name: "cyan",
+  color: {
+    hue: {
+      start: 179,
+      end: 179,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 70,
+      end: 60,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Sky ======================================== //
 
-export const sky = generateColors2(
-  {
-    start: 190,
-    end: 190,
-    curve: curve.linear,
-  },
-  {
-    start: 85,
-    end: 75,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const sky = generateColorFamily({
+  name: "sky",
+  color: {
+    hue: {
+      start: 195,
+      end: 195,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 85,
+      end: 75,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Blue ======================================== //
 
-export const blue = generateColors2(
-  {
-    start: 210,
-    end: 210,
-    curve: curve.linear,
-  },
-  {
-    start: 90,
-    end: 60,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const blue = generateColorFamily({
+  name: "blue",
+  color: {
+    hue: {
+      start: 210,
+      end: 210,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 90,
+      end: 75,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Indigo ======================================== //
 
-export const indigo = generateColors2(
-  {
-    start: 240,
-    end: 240,
-    curve: curve.linear,
-  },
-  {
-    start: 80,
-    end: 40,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const indigo = generateColorFamily({
+  name: "indigo",
+  color: {
+    hue: {
+      start: 230,
+      end: 230,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 80,
+      end: 50,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Purple ======================================== //
 
-export const purple = generateColors2(
-  {
-    start: 260,
-    end: 265,
-    curve: curve.linear,
-  },
-  {
-    start: 80,
-    end: 50,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const purple = generateColorFamily({
+  name: "purple",
+  color: {
+    hue: {
+      start: 260,
+      end: 265,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 80,
+      end: 50,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Pink ======================================== //
 
-export const pink = generateColors2(
-  {
-    start: 310,
-    end: 310,
-    curve: curve.linear,
-  },
-  {
-    start: 80,
-    end: 70,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const pink = generateColorFamily({
+  name: "pink",
+  color: {
+    hue: {
+      start: 310,
+      end: 310,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 80,
+      end: 75,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});
 
 // Rose ======================================== //
 
-export const rose = generateColors2(
-  {
-    start: 345,
-    end: 345,
-    curve: curve.linear,
-  },
-  {
-    start: 90,
-    end: 65,
-    curve: curve.saturation,
-  },
-  {
-    start: 97,
-    end: 20,
-    curve: curve.lightness,
-  }
-);
+export const rose = generateColorFamily({
+  name: "rose",
+  color: {
+    hue: {
+      start: 345,
+      end: 345,
+      curve: curve.linear,
+    },
+    saturation: {
+      start: 90,
+      end: 65,
+      curve: curve.saturation,
+    },
+    lightness: {
+      start: 97,
+      end: 20,
+      curve: curve.lightness,
+    },
+  },
+});

styles/src/system/ref/curves.ts 🔗

@@ -0,0 +1,25 @@
+export interface Curve {
+  name: string;
+  value: number[];
+}
+
+export interface Curves {
+  lightness: Curve;
+  saturation: Curve;
+  linear: Curve;
+}
+
+export const curve: Curves = {
+  lightness: {
+    name: "lightnessCurve",
+    value: [0.2, 0, 0.85, 1.0],
+  },
+  saturation: {
+    name: "saturationCurve",
+    value: [0.67, 0.6, 0.55, 1.0],
+  },
+  linear: {
+    name: "linear",
+    value: [0.5, 0.5, 0.5, 0.5],
+  },
+};

styles/src/system/system.ts 🔗

@@ -0,0 +1,23 @@
+import chroma from "chroma-js";
+import * as colorFamily from "./ref/color";
+
+const color = {
+  red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9),
+  sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9),
+  orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9),
+  amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9),
+  yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9),
+  citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9),
+  lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9),
+  green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9),
+  mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9),
+  cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9),
+  sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9),
+  blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9),
+  indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9),
+  purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9),
+  pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9),
+  rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9),
+};
+
+export { color };

styles/src/system/types.ts 🔗

@@ -1,27 +1,67 @@
 import { Color as ChromaColor } from "chroma-js";
+import { Curve } from "./ref/curves";
+
+export interface ColorAccessiblityValue {
+  value: number;
+  aaPass: boolean;
+  aaaPass: boolean;
+}
+
+/**
+ * Calculates the color contrast between a specified color and its corresponding background and foreground colors.
+ *
+ * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette.
+ * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information.
+ */
+export interface ColorAccessiblity {
+  black: ColorAccessiblityValue;
+  white: ColorAccessiblityValue;
+}
 
 export type Color = {
   step: number;
+  contrast: ColorAccessiblity;
   hex: string;
   lch: number[];
-  rgbaArray: number[];
+  rgba: number[];
+  isLight: boolean;
 };
 
-export type ColorSet = Color[];
+export interface ColorScale {
+  colors: Color[];
+  // An array of hex values for each color in the scale
+  values: string[];
+}
 
 export type ColorFamily = {
   name: string;
-  colors: string[];
-  invertedColors: string[];
-  colorsMeta: ColorSet;
-  invertedMeta: ColorSet;
+  scale: ColorScale;
+  invertedScale: ColorScale;
 };
 
-export interface ColorProps {
+export interface ColorFamilyHue {
+  start: number;
+  end: number;
+  curve: Curve;
+}
+
+export interface ColorFamilySaturation {
+  start: number;
+  end: number;
+  curve: Curve;
+}
+
+export interface ColorFamilyLightness {
+  start: number;
+  end: number;
+  curve: Curve;
+}
+
+export interface ColorFamilyConfig {
   name: string;
   color: {
-    start: string | ChromaColor;
-    middle: string | ChromaColor;
-    end: string | ChromaColor;
+    hue: ColorFamilyHue;
+    saturation: ColorFamilySaturation;
+    lightness: ColorFamilyLightness;
   };
 }

styles/theme-tool/app/page.tsx 🔗

@@ -1,12 +1,8 @@
 /* eslint-disable import/no-relative-packages */
-import { Scale } from 'chroma-js';
-
-import { color } from '../../src/system/reference';
+import { color } from '../../src/system/system';
 import styles from './page.module.css';
 
-function ColorChips({ colorScale }: { colorScale: Scale }) {
-    const colors = colorScale.colors(11);
-
+function ColorChips({ colors }: { colors: string[] }) {
     return (
         <div
             style={{
@@ -38,22 +34,22 @@ export default function Home() {
     return (
         <main>
             <div style={{ display: 'flex', gap: '1px' }}>
-                <ColorChips colorScale={color.red} />
-                <ColorChips colorScale={color.sunset} />
-                <ColorChips colorScale={color.orange} />
-                <ColorChips colorScale={color.amber} />
-                <ColorChips colorScale={color.yellow} />
-                <ColorChips colorScale={color.citron} />
-                <ColorChips colorScale={color.lime} />
-                <ColorChips colorScale={color.green} />
-                <ColorChips colorScale={color.mint} />
-                <ColorChips colorScale={color.cyan} />
-                <ColorChips colorScale={color.sky} />
-                <ColorChips colorScale={color.blue} />
-                <ColorChips colorScale={color.indigo} />
-                <ColorChips colorScale={color.purple} />
-                <ColorChips colorScale={color.pink} />
-                <ColorChips colorScale={color.rose} />
+                <ColorChips colors={color.red} />
+                <ColorChips colors={color.sunset} />
+                <ColorChips colors={color.orange} />
+                <ColorChips colors={color.amber} />
+                <ColorChips colors={color.yellow} />
+                <ColorChips colors={color.citron} />
+                <ColorChips colors={color.lime} />
+                <ColorChips colors={color.green} />
+                <ColorChips colors={color.mint} />
+                <ColorChips colors={color.cyan} />
+                <ColorChips colors={color.sky} />
+                <ColorChips colors={color.blue} />
+                <ColorChips colors={color.indigo} />
+                <ColorChips colors={color.purple} />
+                <ColorChips colors={color.pink} />
+                <ColorChips colors={color.rose} />
             </div>
         </main>
     );