Skip to content

TypeScript #4

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

Merged
merged 8 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": 1
}
}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules/

.DS_Store
dist/
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "always",
"bracketSameLine": false,
"printWidth": 120,
"tabWidth": 2,
"semi": true
}
154 changes: 0 additions & 154 deletions index.js

This file was deleted.

193 changes: 193 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import chroma, { InterpolationMode } from 'chroma-js';
import builtInColors from 'tailwindcss/colors';

function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}

function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}

function hasOwn<T extends object>(obj: T, key: keyof T): key is keyof T {
return Object.prototype.hasOwnProperty.call(obj, key);
}

// valid color modes for chroma-js
const validColorModes = [
'rgb',
'lab',
'lch',
'lrgb',
'hcl',
'num',
'hcg',
'oklch',
'hsi',
'hsl',
'hsv',
'oklab',
] as const;

// types for tailwind-lerp-colors
type NumericObjKey = number | `${number}`;
type Shades = Record<NumericObjKey, string>;
type Colors = Record<string, Shades>;
type ColorMode = (typeof validColorModes)[number];
type Options = {
includeBase?: boolean;
includeLegacy?: boolean;
lerpEnds?: boolean;
interval?: number;
mode?: ColorMode;
};
type OptionName = keyof Options;
type Option<T extends OptionName> = Options[T];
type SingularOptions = Pick<Options, 'lerpEnds' | 'interval' | 'mode'>;

// default options for tailwind-lerp-colors -> lerpColor
const defaultSingleOptions: Required<SingularOptions> = {
lerpEnds: true,
interval: 25,
mode: 'lrgb',
};

// default options for tailwind-lerp-colors -> lerpColors
const defaultOptions = {
includeBase: true,
includeLegacy: false,
...defaultSingleOptions,
};

const isOptionInvalid = <T extends OptionName>(options: Options, optionName: T, test: (k: Option<T>) => boolean) => {
return options && hasOwn(options, optionName) && !test(options[optionName]);
};

const throwError = (message: string) => {
throw new Error(message);
};

export const lerpColor = (shades: Shades, options: SingularOptions = {}) => {
if (isOptionInvalid(options, 'lerpEnds', (v) => typeof v === 'boolean'))
throwError('tailwind-lerp-colors option `lerpEnds` must be a boolean.');

if (isOptionInvalid(options, 'interval', (v) => Number.isInteger(v) && typeof v === 'number' && v > 0))
throwError('tailwind-lerp-colors option `interval` must be a positive integer greater than 0.');
if (isOptionInvalid(options, 'mode', (v) => typeof v === 'string' && validColorModes.includes(v)))
throwError(
`tailwind-lerp-colors option \`mode\` must be one of the following values: ${validColorModes.join(', ')}.`
);

const { lerpEnds, interval, mode } = {
...defaultSingleOptions,
...(options ?? {}),
};

const sortByNumericFirstIndex = ([numericKeyA]: [number, string], [numericKeyB]: [number, string]) => {
return numericKeyA - numericKeyB;
};

if (
['null', 'undefined'].includes(typeof shades) ||
!shades.toString ||
typeof shades === 'string' ||
Array.isArray(shades) ||
shades.toString() !== '[object Object]' ||
!keys(shades).every((key) => {
return !isNaN(+key);
})
) {
throwError(
'tailwind-lerp-colors object `shades` must be an object with numeric keys.\n\nvalue used: ' +
JSON.stringify(shades, null, 2)
);
}
const shadesArray = entries(shades)
.map(([numericStringKey, color]) => {
return [Number(numericStringKey), color] as [number, string];
})
.sort(sortByNumericFirstIndex);
if (lerpEnds) {
shadesArray.unshift([0, '#ffffff']);
shadesArray.push([1000, '#000000']);
}
const finalShades = [...shadesArray];
for (let i = 0; i < shadesArray.length - 1; i++) {
const [shade, color] = shadesArray[i];
const [nextShade, nextColor] = shadesArray[i + 1];

// check to make sure both shades being compared
// are evenly divisible by the set interval
const interpolations = (nextShade - shade) / interval - 1;
if (interpolations <= 0 || !Number.isInteger(interpolations)) continue;

const scale = chroma.scale([color, nextColor]).mode(mode as InterpolationMode);
const getColorAt = (percent: number) => scale(percent).hex();
for (let run = 1; run <= interpolations; run++) {
const percent = run / (interpolations + 1);
finalShades.push([shade + interval * run, getColorAt(percent)]);
}
}
finalShades.sort(sortByNumericFirstIndex);

return Object.fromEntries(finalShades);
};

export const lerpColors = (colorsObj: Colors = {}, options: Options = {}) => {
const legacyNames = ['lightBlue', 'warmGray', 'trueGray', 'coolGray', 'blueGray'];

if (isOptionInvalid(options, 'includeBase', (v) => typeof v === 'boolean'))
throwError('tailwind-lerp-colors option `includeBase` must be a boolean.');
if (isOptionInvalid(options, 'includeLegacy', (v) => typeof v === 'boolean'))
throwError('tailwind-lerp-colors option `includeLegacy` must be a boolean.');

const { includeBase, includeLegacy, lerpEnds, interval, mode } = {
...defaultOptions,
...options,
};
const baseColors: Colors = {};
if (includeBase) {
const builtInColorKeys = keys(builtInColors);
for (const key of builtInColorKeys) {
if (!legacyNames.includes(key) || includeLegacy) {
baseColors[key] = builtInColors[key];
}
}
}
const initialColors = entries({
...baseColors,
...colorsObj,
});

const finalColors: Colors = {};

for (const [name, shades] of initialColors) {
if (['null', 'undefined'].includes(typeof shades) || !shades.toString) {
continue;
}
finalColors[`${name}`] = shades;
if (
typeof shades === 'string' ||
Array.isArray(shades) ||
shades.toString() !== '[object Object]' ||
!keys(shades).every((key) => {
return !isNaN(+key);
})
) {
continue;
}
finalColors[name] = lerpColor(shades, { lerpEnds, interval, mode });
}

return finalColors;
};

export type {
Shades as LerpColorsShades,
Colors as LerpColorsColors,
ColorMode as LerpColorsColorMode,
Options as LerpColorsOptions,
OptionName as LerpColorsOptionName,
Option as LerpColorsOption,
SingularOptions as LerpColorsSingularOptions,
};
Loading