@sethyrung/tailshade 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/cli.js +67 -29
- package/package.json +1 -1
- package/src/cli.ts +24 -9
- package/src/core.ts +35 -21
- package/src/targets.ts +15 -1
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ $ tailshade '#ff0000'
|
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Your base color lands verbatim at `<name>-500
|
|
20
|
-
Tailwind's own: lightness targets and chroma
|
|
21
|
-
palettes, every step gamut-mapped to sRGB.
|
|
19
|
+
Your base color lands verbatim at `<name>-500` (or your chosen `--step`),
|
|
20
|
+
and the ramp reads like one of Tailwind's own: lightness targets and chroma
|
|
21
|
+
taper derived from v4's real palettes, every step gamut-mapped to sRGB.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -51,6 +51,7 @@ The base color accepts any CSS format — hex, `rgb()`, `hsl()`, named colors, o
|
|
|
51
51
|
| Flag | Effect |
|
|
52
52
|
| --------------------------------- | ------------------------------------------------------------------- |
|
|
53
53
|
| _(none)_ | Tailwind v4 `@theme` block, oklch values (default) |
|
|
54
|
+
| `--step <50..950>` | Anchor base color to a specific shade (default: 500) |
|
|
54
55
|
| `--name <name>` | Override the auto-detected palette name |
|
|
55
56
|
| `--v3` | Export a `tailwind.config.js` snippet instead of the `@theme` block |
|
|
56
57
|
| `--format <oklch\|hex\|rgb\|hsl>` | Color notation of the emitted values (default: oklch) |
|
|
@@ -60,6 +61,7 @@ The base color accepts any CSS format — hex, `rgb()`, `hsl()`, named colors, o
|
|
|
60
61
|
Examples:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
64
|
+
tailshade '#111410' --step 700
|
|
63
65
|
tailshade 'oklch(0.6 0.1 29)' --name brand
|
|
64
66
|
tailshade '#ff0000' --v3 --format hex
|
|
65
67
|
tailshade 'teal' --preview
|
|
@@ -94,11 +96,12 @@ module.exports = {
|
|
|
94
96
|
## How it works
|
|
95
97
|
|
|
96
98
|
- Parse any CSS color (via [culori](https://culorijs.org/)) and normalize to OKLCH.
|
|
97
|
-
- Anchor the base color verbatim at step 500
|
|
98
|
-
ladder around it — natural spacing near the
|
|
99
|
-
compressed for extreme bases, never two identical
|
|
99
|
+
- Anchor the base color verbatim at step 500 (or the step given by `--step`);
|
|
100
|
+
scale a v4-derived lightness ladder around it — natural spacing near the
|
|
101
|
+
archetypal base, stretched or compressed for extreme bases, never two identical
|
|
102
|
+
steps.
|
|
100
103
|
- Apply a v4-derived chroma taper (peak at 500, whisper at 50, moderate at 950),
|
|
101
|
-
then gamut-map every step so nothing clips.
|
|
104
|
+
scaled relative to the anchor step, then gamut-map every step so nothing clips.
|
|
102
105
|
|
|
103
106
|
Decisions are recorded in [docs/adr](docs/adr); the domain vocabulary lives in
|
|
104
107
|
[CONTEXT.md](CONTEXT.md).
|
package/dist/cli.js
CHANGED
|
@@ -49,8 +49,19 @@ var TARGETS = {
|
|
|
49
49
|
};
|
|
50
50
|
var L_MAX = 0.985;
|
|
51
51
|
var L_MIN = 0.02;
|
|
52
|
-
var
|
|
53
|
-
|
|
52
|
+
var LIGHTNESS_BOUNDS = {
|
|
53
|
+
50: { min: 0.04, max: 0.985 },
|
|
54
|
+
100: { min: 0.035, max: 0.98 },
|
|
55
|
+
200: { min: 0.03, max: 0.98 },
|
|
56
|
+
300: { min: 0.03, max: 0.975 },
|
|
57
|
+
400: { min: 0.03, max: 0.97 },
|
|
58
|
+
500: { min: 0.03, max: 0.97 },
|
|
59
|
+
600: { min: 0.03, max: 0.97 },
|
|
60
|
+
700: { min: 0.025, max: 0.97 },
|
|
61
|
+
800: { min: 0.025, max: 0.97 },
|
|
62
|
+
900: { min: 0.025, max: 0.965 },
|
|
63
|
+
950: { min: 0.02, max: 0.96 }
|
|
64
|
+
};
|
|
54
65
|
var CHROMA_RATIOS = {
|
|
55
66
|
50: 0.0867,
|
|
56
67
|
100: 0.2082,
|
|
@@ -78,12 +89,13 @@ function target(step) {
|
|
|
78
89
|
}
|
|
79
90
|
return t;
|
|
80
91
|
}
|
|
81
|
-
function taperRatio(step) {
|
|
92
|
+
function taperRatio(step, anchorStep = 500) {
|
|
82
93
|
const ratio = CHROMA_RATIOS[step];
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const anchorRatio = CHROMA_RATIOS[anchorStep];
|
|
95
|
+
if (ratio === undefined || anchorRatio === undefined) {
|
|
96
|
+
throw new Error(`missing chroma taper ratio for step ${step} or anchor ${anchorStep}`);
|
|
85
97
|
}
|
|
86
|
-
return ratio;
|
|
98
|
+
return ratio / anchorRatio;
|
|
87
99
|
}
|
|
88
100
|
function parseBase(input) {
|
|
89
101
|
const color = parse2(input);
|
|
@@ -98,28 +110,39 @@ function parseBase(input) {
|
|
|
98
110
|
const c = Number.isFinite(oklch.c) ? oklch.c : 0;
|
|
99
111
|
return { l: oklch.l ?? 0, c, h };
|
|
100
112
|
}
|
|
101
|
-
function lightnessLadder(baseL) {
|
|
102
|
-
|
|
113
|
+
function lightnessLadder(baseL, anchorStep = 500) {
|
|
114
|
+
const bounds = LIGHTNESS_BOUNDS[anchorStep] ?? { min: 0.03, max: 0.97 };
|
|
115
|
+
if (baseL > bounds.max) {
|
|
103
116
|
throw new PaletteError("base color is too light to build a 50–950 ramp");
|
|
104
117
|
}
|
|
105
|
-
if (baseL <
|
|
118
|
+
if (baseL < bounds.min) {
|
|
106
119
|
throw new PaletteError("base color is too dark to build a 50–950 ramp");
|
|
107
120
|
}
|
|
108
|
-
const
|
|
109
|
-
const upExtent = target(50) -
|
|
110
|
-
const downExtent = target(950) -
|
|
121
|
+
const tAnchor = target(anchorStep);
|
|
122
|
+
const upExtent = target(50) - tAnchor;
|
|
123
|
+
const downExtent = target(950) - tAnchor;
|
|
111
124
|
const end50 = Math.min(Math.max(baseL + upExtent, target(50)), L_MAX);
|
|
112
125
|
const end950 = Math.max(Math.min(baseL + downExtent, target(950)), L_MIN);
|
|
113
|
-
const scaleUp = (end50 - baseL) / upExtent;
|
|
114
|
-
const scaleDown = (end950 - baseL) / downExtent;
|
|
126
|
+
const scaleUp = upExtent === 0 ? 1 : (end50 - baseL) / upExtent;
|
|
127
|
+
const scaleDown = downExtent === 0 ? 1 : (end950 - baseL) / downExtent;
|
|
115
128
|
const ladder = new Map;
|
|
116
129
|
for (const step of STEPS) {
|
|
117
|
-
if (step ===
|
|
130
|
+
if (step === anchorStep) {
|
|
118
131
|
ladder.set(step, baseL);
|
|
119
132
|
continue;
|
|
120
133
|
}
|
|
121
|
-
const scale = step <
|
|
122
|
-
ladder.set(step, baseL + (target(step) -
|
|
134
|
+
const scale = step < anchorStep ? scaleUp : scaleDown;
|
|
135
|
+
ladder.set(step, baseL + (target(step) - tAnchor) * scale);
|
|
136
|
+
}
|
|
137
|
+
const values = STEPS.map((s) => round3(ladder.get(s)));
|
|
138
|
+
for (let i = 1;i < values.length; i++) {
|
|
139
|
+
if (values[i] >= values[i - 1]) {
|
|
140
|
+
if (baseL > target(anchorStep)) {
|
|
141
|
+
throw new PaletteError("base color is too light to build a 50–950 ramp");
|
|
142
|
+
} else {
|
|
143
|
+
throw new PaletteError("base color is too dark to build a 50–950 ramp");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
123
146
|
}
|
|
124
147
|
return ladder;
|
|
125
148
|
}
|
|
@@ -149,16 +172,16 @@ function maxInGamutChroma(l, c, h) {
|
|
|
149
172
|
}
|
|
150
173
|
var round3 = (n) => Math.round(n * 1000) / 1000;
|
|
151
174
|
var floor3 = (n) => Math.floor(n * 1000) / 1000;
|
|
152
|
-
function generatePalette(input) {
|
|
175
|
+
function generatePalette(input, anchorStep = 500) {
|
|
153
176
|
const base = parseBase(input);
|
|
154
|
-
const ladder = lightnessLadder(base.l);
|
|
177
|
+
const ladder = lightnessLadder(base.l, anchorStep);
|
|
155
178
|
const steps = STEPS.map((step) => {
|
|
156
179
|
const l = round3(ladderLightness(ladder, step));
|
|
157
180
|
const h = round3(base.h);
|
|
158
|
-
const c = floor3(maxInGamutChroma(l, base.c * taperRatio(step), h));
|
|
181
|
+
const c = floor3(maxInGamutChroma(l, base.c * taperRatio(step, anchorStep), h));
|
|
159
182
|
return { step, l, c, h };
|
|
160
183
|
});
|
|
161
|
-
return { name: detectName(base), base, steps };
|
|
184
|
+
return { name: detectName(base), base, anchorStep, steps };
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
// src/format.ts
|
|
@@ -210,23 +233,26 @@ function toTailwindV3(palette, notation = "oklch") {
|
|
|
210
233
|
|
|
211
234
|
// src/cli.ts
|
|
212
235
|
var NOTATIONS2 = ["oklch", "hex", "rgb", "hsl"];
|
|
213
|
-
var USAGE = `Usage: tailshade '<color>' [--name <name>] [--v3] [--format <oklch|hex|rgb|hsl>] [--preview]
|
|
236
|
+
var USAGE = `Usage: tailshade '<color>' [--step <50..950>] [--name <name>] [--v3] [--format <oklch|hex|rgb|hsl>] [--preview]
|
|
214
237
|
|
|
215
238
|
Generate a Tailwind v4 palette from a base color (any CSS color format).
|
|
216
239
|
|
|
217
|
-
The
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
the
|
|
240
|
+
The base color anchors at step 500 by default — pass --step to anchor at any
|
|
241
|
+
shade (50, 100, 200, ..., 950). The palette name defaults to the nearest CSS
|
|
242
|
+
color name, which can shadow Tailwind's built-in colors (red, teal, ...) inside
|
|
243
|
+
@theme — pass --name to choose your own. Pass --v3 to export a tailwind.config.js
|
|
244
|
+
snippet instead of the v4 @theme block. Values default to oklch; pass --format
|
|
245
|
+
to switch the notation (hex, rgb, hsl). Pass --preview to print an ANSI swatch
|
|
246
|
+
strip above the output.
|
|
223
247
|
|
|
224
248
|
Example: tailshade '#ff0000'
|
|
249
|
+
tailshade '#111410' --step 700
|
|
225
250
|
tailshade '#ff0000' --name brand
|
|
226
251
|
tailshade '#ff0000' --v3 --format hex --preview`;
|
|
227
252
|
function main(argv) {
|
|
228
253
|
let color;
|
|
229
254
|
let name;
|
|
255
|
+
let step = 500;
|
|
230
256
|
let v3 = false;
|
|
231
257
|
let preview = false;
|
|
232
258
|
let notation;
|
|
@@ -235,6 +261,18 @@ function main(argv) {
|
|
|
235
261
|
if (arg === "--help" || arg === "-h") {
|
|
236
262
|
return { output: USAGE, exitCode: 0 };
|
|
237
263
|
}
|
|
264
|
+
if (arg === "--step") {
|
|
265
|
+
const value = argv[++i];
|
|
266
|
+
if (value === undefined || value.startsWith("--")) {
|
|
267
|
+
return usage("--step requires a value");
|
|
268
|
+
}
|
|
269
|
+
const num = Number(value);
|
|
270
|
+
if (!Number.isInteger(num) || !STEPS.includes(num)) {
|
|
271
|
+
return usage(`--step '${value}' expects one of: ${STEPS.join(", ")}`);
|
|
272
|
+
}
|
|
273
|
+
step = num;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
238
276
|
if (arg === "--name") {
|
|
239
277
|
const value = argv[++i];
|
|
240
278
|
if (value === undefined || value.startsWith("--")) {
|
|
@@ -274,7 +312,7 @@ function main(argv) {
|
|
|
274
312
|
return usage("expected exactly one color argument");
|
|
275
313
|
}
|
|
276
314
|
try {
|
|
277
|
-
const palette = generatePalette(color);
|
|
315
|
+
const palette = generatePalette(color, step);
|
|
278
316
|
if (name !== undefined) {
|
|
279
317
|
const kebab = kebabCase(name);
|
|
280
318
|
if (!kebab) {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generatePalette, PaletteError } from "@/core";
|
|
1
|
+
import { generatePalette, PaletteError, STEPS, type Step } from "@/core";
|
|
2
2
|
import { toPreviewStrip, toTailwindV3, toThemeCss, type Notation } from "@/format";
|
|
3
3
|
import { kebabCase } from "@/name";
|
|
4
4
|
|
|
@@ -6,18 +6,20 @@ const NOTATIONS: Notation[] = ["oklch", "hex", "rgb", "hsl"];
|
|
|
6
6
|
|
|
7
7
|
export type CliResult = { output: string; exitCode: number };
|
|
8
8
|
|
|
9
|
-
const USAGE = `Usage: tailshade '<color>' [--name <name>] [--v3] [--format <oklch|hex|rgb|hsl>] [--preview]
|
|
9
|
+
const USAGE = `Usage: tailshade '<color>' [--step <50..950>] [--name <name>] [--v3] [--format <oklch|hex|rgb|hsl>] [--preview]
|
|
10
10
|
|
|
11
11
|
Generate a Tailwind v4 palette from a base color (any CSS color format).
|
|
12
12
|
|
|
13
|
-
The
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
the
|
|
13
|
+
The base color anchors at step 500 by default — pass --step to anchor at any
|
|
14
|
+
shade (50, 100, 200, ..., 950). The palette name defaults to the nearest CSS
|
|
15
|
+
color name, which can shadow Tailwind's built-in colors (red, teal, ...) inside
|
|
16
|
+
@theme — pass --name to choose your own. Pass --v3 to export a tailwind.config.js
|
|
17
|
+
snippet instead of the v4 @theme block. Values default to oklch; pass --format
|
|
18
|
+
to switch the notation (hex, rgb, hsl). Pass --preview to print an ANSI swatch
|
|
19
|
+
strip above the output.
|
|
19
20
|
|
|
20
21
|
Example: tailshade '#ff0000'
|
|
22
|
+
tailshade '#111410' --step 700
|
|
21
23
|
tailshade '#ff0000' --name brand
|
|
22
24
|
tailshade '#ff0000' --v3 --format hex --preview`;
|
|
23
25
|
|
|
@@ -28,6 +30,7 @@ Example: tailshade '#ff0000'
|
|
|
28
30
|
export function main(argv: string[]): CliResult {
|
|
29
31
|
let color: string | undefined;
|
|
30
32
|
let name: string | undefined;
|
|
33
|
+
let step: Step = 500;
|
|
31
34
|
let v3 = false;
|
|
32
35
|
let preview = false;
|
|
33
36
|
let notation: Notation | undefined;
|
|
@@ -37,6 +40,18 @@ export function main(argv: string[]): CliResult {
|
|
|
37
40
|
if (arg === "--help" || arg === "-h") {
|
|
38
41
|
return { output: USAGE, exitCode: 0 };
|
|
39
42
|
}
|
|
43
|
+
if (arg === "--step") {
|
|
44
|
+
const value = argv[++i];
|
|
45
|
+
if (value === undefined || value.startsWith("--")) {
|
|
46
|
+
return usage("--step requires a value");
|
|
47
|
+
}
|
|
48
|
+
const num = Number(value);
|
|
49
|
+
if (!Number.isInteger(num) || !STEPS.includes(num as Step)) {
|
|
50
|
+
return usage(`--step '${value}' expects one of: ${STEPS.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
step = num as Step;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
40
55
|
if (arg === "--name") {
|
|
41
56
|
const value = argv[++i];
|
|
42
57
|
if (value === undefined || value.startsWith("--")) {
|
|
@@ -77,7 +92,7 @@ export function main(argv: string[]): CliResult {
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
try {
|
|
80
|
-
const palette = generatePalette(color);
|
|
95
|
+
const palette = generatePalette(color, step);
|
|
81
96
|
if (name !== undefined) {
|
|
82
97
|
const kebab = kebabCase(name);
|
|
83
98
|
if (!kebab) {
|
package/src/core.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { converter, displayable, parse } from "culori";
|
|
2
2
|
import { detectName, type Oklch } from "@/name";
|
|
3
|
-
import { CHROMA_RATIOS, L_MAX, L_MIN,
|
|
3
|
+
import { CHROMA_RATIOS, L_MAX, L_MIN, LIGHTNESS_BOUNDS, TARGETS } from "@/targets";
|
|
4
4
|
|
|
5
5
|
export const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;
|
|
6
6
|
export type Step = (typeof STEPS)[number];
|
|
@@ -8,7 +8,7 @@ export type Step = (typeof STEPS)[number];
|
|
|
8
8
|
const toOklch = converter("oklch");
|
|
9
9
|
|
|
10
10
|
export type PaletteEntry = { step: Step; l: number; c: number; h: number };
|
|
11
|
-
export type Palette = { name: string; base: Oklch; steps: PaletteEntry[] };
|
|
11
|
+
export type Palette = { name: string; base: Oklch; anchorStep: Step; steps: PaletteEntry[] };
|
|
12
12
|
|
|
13
13
|
export class PaletteError extends Error {}
|
|
14
14
|
|
|
@@ -20,12 +20,13 @@ function target(step: Step): number {
|
|
|
20
20
|
return t;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function taperRatio(step: Step): number {
|
|
23
|
+
function taperRatio(step: Step, anchorStep: Step = 500): number {
|
|
24
24
|
const ratio = CHROMA_RATIOS[step];
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const anchorRatio = CHROMA_RATIOS[anchorStep];
|
|
26
|
+
if (ratio === undefined || anchorRatio === undefined) {
|
|
27
|
+
throw new Error(`missing chroma taper ratio for step ${step} or anchor ${anchorStep}`);
|
|
27
28
|
}
|
|
28
|
-
return ratio;
|
|
29
|
+
return ratio / anchorRatio;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -51,31 +52,44 @@ function parseBase(input: string): Oklch {
|
|
|
51
52
|
* each endpoint keeps archetypal bases on the universal ladder and stretches
|
|
52
53
|
* or compresses extreme bases to span the range without duplicate steps.
|
|
53
54
|
*/
|
|
54
|
-
export function lightnessLadder(baseL: number): Map<Step, number> {
|
|
55
|
-
|
|
55
|
+
export function lightnessLadder(baseL: number, anchorStep: Step = 500): Map<Step, number> {
|
|
56
|
+
const bounds = LIGHTNESS_BOUNDS[anchorStep] ?? { min: 0.03, max: 0.97 };
|
|
57
|
+
if (baseL > bounds.max) {
|
|
56
58
|
throw new PaletteError("base color is too light to build a 50–950 ramp");
|
|
57
59
|
}
|
|
58
|
-
if (baseL <
|
|
60
|
+
if (baseL < bounds.min) {
|
|
59
61
|
throw new PaletteError("base color is too dark to build a 50–950 ramp");
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
const upExtent = target(50) -
|
|
63
|
-
const downExtent = target(950) -
|
|
63
|
+
const tAnchor = target(anchorStep);
|
|
64
|
+
const upExtent = target(50) - tAnchor;
|
|
65
|
+
const downExtent = target(950) - tAnchor;
|
|
64
66
|
|
|
65
67
|
const end50 = Math.min(Math.max(baseL + upExtent, target(50)), L_MAX);
|
|
66
68
|
const end950 = Math.max(Math.min(baseL + downExtent, target(950)), L_MIN);
|
|
67
|
-
const scaleUp = (end50 - baseL) / upExtent;
|
|
68
|
-
const scaleDown = (end950 - baseL) / downExtent;
|
|
69
|
+
const scaleUp = upExtent === 0 ? 1 : (end50 - baseL) / upExtent;
|
|
70
|
+
const scaleDown = downExtent === 0 ? 1 : (end950 - baseL) / downExtent;
|
|
69
71
|
|
|
70
72
|
const ladder = new Map<Step, number>();
|
|
71
73
|
for (const step of STEPS) {
|
|
72
|
-
if (step ===
|
|
74
|
+
if (step === anchorStep) {
|
|
73
75
|
ladder.set(step, baseL);
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
|
-
const scale = step <
|
|
77
|
-
ladder.set(step, baseL + (target(step) -
|
|
78
|
+
const scale = step < anchorStep ? scaleUp : scaleDown;
|
|
79
|
+
ladder.set(step, baseL + (target(step) - tAnchor) * scale);
|
|
78
80
|
}
|
|
81
|
+
|
|
82
|
+
const values = STEPS.map((s) => round3(ladder.get(s)!));
|
|
83
|
+
for (let i = 1; i < values.length; i++) {
|
|
84
|
+
if (values[i]! >= values[i - 1]!) {
|
|
85
|
+
if (baseL > target(anchorStep)) {
|
|
86
|
+
throw new PaletteError("base color is too light to build a 50–950 ramp");
|
|
87
|
+
} else {
|
|
88
|
+
throw new PaletteError("base color is too dark to build a 50–950 ramp");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
return ladder;
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -112,14 +126,14 @@ const floor3 = (n: number) => Math.floor(n * 1000) / 1000;
|
|
|
112
126
|
* v4 taper ratios, is gamut-mapped at the rounded L/H and floored so the
|
|
113
127
|
* printed triple can't round its way out of the sRGB gamut.
|
|
114
128
|
*/
|
|
115
|
-
export function generatePalette(input: string): Palette {
|
|
129
|
+
export function generatePalette(input: string, anchorStep: Step = 500): Palette {
|
|
116
130
|
const base = parseBase(input);
|
|
117
|
-
const ladder = lightnessLadder(base.l);
|
|
131
|
+
const ladder = lightnessLadder(base.l, anchorStep);
|
|
118
132
|
const steps: PaletteEntry[] = STEPS.map((step) => {
|
|
119
133
|
const l = round3(ladderLightness(ladder, step));
|
|
120
134
|
const h = round3(base.h);
|
|
121
|
-
const c = floor3(maxInGamutChroma(l, base.c * taperRatio(step), h));
|
|
135
|
+
const c = floor3(maxInGamutChroma(l, base.c * taperRatio(step, anchorStep), h));
|
|
122
136
|
return { step, l, c, h };
|
|
123
137
|
});
|
|
124
|
-
return { name: detectName(base), base, steps };
|
|
138
|
+
return { name: detectName(base), base, anchorStep, steps };
|
|
125
139
|
}
|
package/src/targets.ts
CHANGED
|
@@ -20,10 +20,24 @@ export const TARGETS: Record<number, number> = {
|
|
|
20
20
|
export const L_MAX = 0.985;
|
|
21
21
|
export const L_MIN = 0.02;
|
|
22
22
|
|
|
23
|
-
/** Bases beyond this cannot fit
|
|
23
|
+
/** Bases beyond this cannot fit distinct steps above/below. */
|
|
24
24
|
export const L_TOO_LIGHT = 0.97;
|
|
25
25
|
export const L_TOO_DARK = 0.03;
|
|
26
26
|
|
|
27
|
+
export const LIGHTNESS_BOUNDS: Record<number, { min: number; max: number }> = {
|
|
28
|
+
50: { min: 0.04, max: 0.985 },
|
|
29
|
+
100: { min: 0.035, max: 0.98 },
|
|
30
|
+
200: { min: 0.03, max: 0.98 },
|
|
31
|
+
300: { min: 0.03, max: 0.975 },
|
|
32
|
+
400: { min: 0.03, max: 0.97 },
|
|
33
|
+
500: { min: 0.03, max: 0.97 },
|
|
34
|
+
600: { min: 0.03, max: 0.97 },
|
|
35
|
+
700: { min: 0.025, max: 0.97 },
|
|
36
|
+
800: { min: 0.025, max: 0.97 },
|
|
37
|
+
900: { min: 0.025, max: 0.965 },
|
|
38
|
+
950: { min: 0.02, max: 0.96 },
|
|
39
|
+
};
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Chroma taper ratios per step (C_step / C_500): mean ratio across the 17
|
|
29
43
|
* chromatic families of Tailwind v4's theme.css. Chroma peaks at the base
|