Skip to content

Commit 7badb69

Browse files
Merge pull request #4 from shreyas-jadhav/main
TypeScript
2 parents 73a36e4 + 836cd4e commit 7badb69

11 files changed

+4731
-235
lines changed

.eslintrc.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"env": {
3+
"browser": true,
4+
"es2021": true,
5+
"node": true
6+
},
7+
"extends": [
8+
"eslint:recommended",
9+
"plugin:@typescript-eslint/recommended",
10+
"prettier"
11+
],
12+
"overrides": [
13+
],
14+
"parser": "@typescript-eslint/parser",
15+
"parserOptions": {
16+
"ecmaVersion": "latest",
17+
"sourceType": "module"
18+
},
19+
"plugins": [
20+
"@typescript-eslint",
21+
"prettier"
22+
],
23+
"rules": {
24+
"prettier/prettier": 1
25+
}
26+
}

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
node_modules/
2-
32
.DS_Store
3+
dist/

.prettierrc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"singleQuote": true,
3+
"trailingComma": "es5",
4+
"arrowParens": "always",
5+
"bracketSameLine": false,
6+
"printWidth": 120,
7+
"tabWidth": 2,
8+
"semi": true
9+
}

index.js

-154
This file was deleted.

index.ts

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import chroma, { InterpolationMode } from 'chroma-js';
2+
import builtInColors from 'tailwindcss/colors';
3+
4+
function keys<T extends object>(obj: T): (keyof T)[] {
5+
return Object.keys(obj) as (keyof T)[];
6+
}
7+
8+
function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
9+
return Object.entries(obj) as [keyof T, T[keyof T]][];
10+
}
11+
12+
function hasOwn<T extends object>(obj: T, key: keyof T): key is keyof T {
13+
return Object.prototype.hasOwnProperty.call(obj, key);
14+
}
15+
16+
// valid color modes for chroma-js
17+
const validColorModes = [
18+
'rgb',
19+
'lab',
20+
'lch',
21+
'lrgb',
22+
'hcl',
23+
'num',
24+
'hcg',
25+
'oklch',
26+
'hsi',
27+
'hsl',
28+
'hsv',
29+
'oklab',
30+
] as const;
31+
32+
// types for tailwind-lerp-colors
33+
type NumericObjKey = number | `${number}`;
34+
type Shades = Record<NumericObjKey, string>;
35+
type Colors = Record<string, Shades>;
36+
type ColorMode = (typeof validColorModes)[number];
37+
type Options = {
38+
includeBase?: boolean;
39+
includeLegacy?: boolean;
40+
lerpEnds?: boolean;
41+
interval?: number;
42+
mode?: ColorMode;
43+
};
44+
type OptionName = keyof Options;
45+
type Option<T extends OptionName> = Options[T];
46+
type SingularOptions = Pick<Options, 'lerpEnds' | 'interval' | 'mode'>;
47+
48+
// default options for tailwind-lerp-colors -> lerpColor
49+
const defaultSingleOptions: Required<SingularOptions> = {
50+
lerpEnds: true,
51+
interval: 25,
52+
mode: 'lrgb',
53+
};
54+
55+
// default options for tailwind-lerp-colors -> lerpColors
56+
const defaultOptions = {
57+
includeBase: true,
58+
includeLegacy: false,
59+
...defaultSingleOptions,
60+
};
61+
62+
const isOptionInvalid = <T extends OptionName>(options: Options, optionName: T, test: (k: Option<T>) => boolean) => {
63+
return options && hasOwn(options, optionName) && !test(options[optionName]);
64+
};
65+
66+
const throwError = (message: string) => {
67+
throw new Error(message);
68+
};
69+
70+
export const lerpColor = (shades: Shades, options: SingularOptions = {}) => {
71+
if (isOptionInvalid(options, 'lerpEnds', (v) => typeof v === 'boolean'))
72+
throwError('tailwind-lerp-colors option `lerpEnds` must be a boolean.');
73+
74+
if (isOptionInvalid(options, 'interval', (v) => Number.isInteger(v) && typeof v === 'number' && v > 0))
75+
throwError('tailwind-lerp-colors option `interval` must be a positive integer greater than 0.');
76+
if (isOptionInvalid(options, 'mode', (v) => typeof v === 'string' && validColorModes.includes(v)))
77+
throwError(
78+
`tailwind-lerp-colors option \`mode\` must be one of the following values: ${validColorModes.join(', ')}.`
79+
);
80+
81+
const { lerpEnds, interval, mode } = {
82+
...defaultSingleOptions,
83+
...(options ?? {}),
84+
};
85+
86+
const sortByNumericFirstIndex = ([numericKeyA]: [number, string], [numericKeyB]: [number, string]) => {
87+
return numericKeyA - numericKeyB;
88+
};
89+
90+
if (
91+
['null', 'undefined'].includes(typeof shades) ||
92+
!shades.toString ||
93+
typeof shades === 'string' ||
94+
Array.isArray(shades) ||
95+
shades.toString() !== '[object Object]' ||
96+
!keys(shades).every((key) => {
97+
return !isNaN(+key);
98+
})
99+
) {
100+
throwError(
101+
'tailwind-lerp-colors object `shades` must be an object with numeric keys.\n\nvalue used: ' +
102+
JSON.stringify(shades, null, 2)
103+
);
104+
}
105+
const shadesArray = entries(shades)
106+
.map(([numericStringKey, color]) => {
107+
return [Number(numericStringKey), color] as [number, string];
108+
})
109+
.sort(sortByNumericFirstIndex);
110+
if (lerpEnds) {
111+
shadesArray.unshift([0, '#ffffff']);
112+
shadesArray.push([1000, '#000000']);
113+
}
114+
const finalShades = [...shadesArray];
115+
for (let i = 0; i < shadesArray.length - 1; i++) {
116+
const [shade, color] = shadesArray[i];
117+
const [nextShade, nextColor] = shadesArray[i + 1];
118+
119+
// check to make sure both shades being compared
120+
// are evenly divisible by the set interval
121+
const interpolations = (nextShade - shade) / interval - 1;
122+
if (interpolations <= 0 || !Number.isInteger(interpolations)) continue;
123+
124+
const scale = chroma.scale([color, nextColor]).mode(mode as InterpolationMode);
125+
const getColorAt = (percent: number) => scale(percent).hex();
126+
for (let run = 1; run <= interpolations; run++) {
127+
const percent = run / (interpolations + 1);
128+
finalShades.push([shade + interval * run, getColorAt(percent)]);
129+
}
130+
}
131+
finalShades.sort(sortByNumericFirstIndex);
132+
133+
return Object.fromEntries(finalShades);
134+
};
135+
136+
export const lerpColors = (colorsObj: Colors = {}, options: Options = {}) => {
137+
const legacyNames = ['lightBlue', 'warmGray', 'trueGray', 'coolGray', 'blueGray'];
138+
139+
if (isOptionInvalid(options, 'includeBase', (v) => typeof v === 'boolean'))
140+
throwError('tailwind-lerp-colors option `includeBase` must be a boolean.');
141+
if (isOptionInvalid(options, 'includeLegacy', (v) => typeof v === 'boolean'))
142+
throwError('tailwind-lerp-colors option `includeLegacy` must be a boolean.');
143+
144+
const { includeBase, includeLegacy, lerpEnds, interval, mode } = {
145+
...defaultOptions,
146+
...options,
147+
};
148+
const baseColors: Colors = {};
149+
if (includeBase) {
150+
const builtInColorKeys = keys(builtInColors);
151+
for (const key of builtInColorKeys) {
152+
if (!legacyNames.includes(key) || includeLegacy) {
153+
baseColors[key] = builtInColors[key];
154+
}
155+
}
156+
}
157+
const initialColors = entries({
158+
...baseColors,
159+
...colorsObj,
160+
});
161+
162+
const finalColors: Colors = {};
163+
164+
for (const [name, shades] of initialColors) {
165+
if (['null', 'undefined'].includes(typeof shades) || !shades.toString) {
166+
continue;
167+
}
168+
finalColors[`${name}`] = shades;
169+
if (
170+
typeof shades === 'string' ||
171+
Array.isArray(shades) ||
172+
shades.toString() !== '[object Object]' ||
173+
!keys(shades).every((key) => {
174+
return !isNaN(+key);
175+
})
176+
) {
177+
continue;
178+
}
179+
finalColors[name] = lerpColor(shades, { lerpEnds, interval, mode });
180+
}
181+
182+
return finalColors;
183+
};
184+
185+
export type {
186+
Shades as LerpColorsShades,
187+
Colors as LerpColorsColors,
188+
ColorMode as LerpColorsColorMode,
189+
Options as LerpColorsOptions,
190+
OptionName as LerpColorsOptionName,
191+
Option as LerpColorsOption,
192+
SingularOptions as LerpColorsSingularOptions,
193+
};

0 commit comments

Comments
 (0)