@clhaas/palette-kit 0.1.0 → 0.1.2

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.
Files changed (70) hide show
  1. package/README.md +4 -3
  2. package/dist/alpha/generateAlphaScale.d.ts +5 -0
  3. package/dist/alpha/generateAlphaScale.js +34 -0
  4. package/dist/contrast/apca.d.ts +2 -0
  5. package/dist/contrast/apca.js +5 -0
  6. package/dist/contrast/onSolid.d.ts +6 -0
  7. package/dist/contrast/onSolid.js +28 -0
  8. package/dist/contrast/solveText.d.ts +2 -0
  9. package/dist/contrast/solveText.js +31 -0
  10. package/dist/createTheme.d.ts +34 -0
  11. package/dist/createTheme.js +88 -0
  12. package/dist/data/radixSeeds.d.ts +3 -0
  13. package/dist/data/radixSeeds.js +34 -0
  14. package/dist/diagnostics/analyzeScale.d.ts +2 -0
  15. package/dist/diagnostics/analyzeScale.js +7 -0
  16. package/dist/diagnostics/analyzeTheme.d.ts +2 -0
  17. package/dist/diagnostics/analyzeTheme.js +35 -0
  18. package/dist/diagnostics/warnings.d.ts +2 -0
  19. package/dist/diagnostics/warnings.js +20 -0
  20. package/dist/engine/curves.d.ts +9 -0
  21. package/dist/engine/curves.js +48 -0
  22. package/dist/engine/oklch.d.ts +8 -0
  23. package/dist/engine/oklch.js +40 -0
  24. package/dist/engine/templates.d.ts +14 -0
  25. package/dist/engine/templates.js +45 -0
  26. package/dist/exporters/selectColorMode.d.ts +2 -0
  27. package/dist/exporters/selectColorMode.js +19 -0
  28. package/dist/exporters/toCssVars.d.ts +12 -0
  29. package/dist/exporters/toCssVars.js +84 -0
  30. package/dist/exporters/toJson.d.ts +3 -0
  31. package/dist/exporters/toJson.js +25 -0
  32. package/dist/exporters/toReactNative.d.ts +30 -0
  33. package/dist/exporters/toReactNative.js +26 -0
  34. package/dist/exporters/toTailwind.d.ts +16 -0
  35. package/dist/exporters/toTailwind.js +90 -0
  36. package/dist/exporters/toTs.d.ts +3 -0
  37. package/dist/exporters/toTs.js +28 -0
  38. package/dist/generateScale.d.ts +48 -0
  39. package/dist/generateScale.js +274 -0
  40. package/dist/index.d.ts +17 -0
  41. package/{src/index.ts → dist/index.js} +0 -15
  42. package/dist/tokens/presetRadixLikeUi.d.ts +5 -0
  43. package/dist/tokens/presetRadixLikeUi.js +55 -0
  44. package/dist/types.d.ts +59 -0
  45. package/dist/types.js +1 -0
  46. package/package.json +19 -3
  47. package/.markdownlint.json +0 -4
  48. package/biome.json +0 -43
  49. package/src/alpha/generateAlphaScale.ts +0 -43
  50. package/src/contrast/apca.ts +0 -7
  51. package/src/contrast/onSolid.ts +0 -38
  52. package/src/contrast/solveText.ts +0 -49
  53. package/src/createTheme.ts +0 -130
  54. package/src/data/radixSeeds.ts +0 -37
  55. package/src/diagnostics/analyzeScale.ts +0 -6
  56. package/src/diagnostics/analyzeTheme.ts +0 -54
  57. package/src/diagnostics/warnings.ts +0 -25
  58. package/src/engine/curves.ts +0 -64
  59. package/src/engine/oklch.ts +0 -53
  60. package/src/engine/templates.ts +0 -58
  61. package/src/exporters/selectColorMode.ts +0 -25
  62. package/src/exporters/toCssVars.ts +0 -116
  63. package/src/exporters/toJson.ts +0 -31
  64. package/src/exporters/toReactNative.ts +0 -39
  65. package/src/exporters/toTailwind.ts +0 -110
  66. package/src/exporters/toTs.ts +0 -34
  67. package/src/generateScale.ts +0 -163
  68. package/src/tokens/presetRadixLikeUi.ts +0 -75
  69. package/src/types.ts +0 -63
  70. package/tsconfig.json +0 -14
@@ -1,53 +0,0 @@
1
- import Color from "colorjs.io";
2
-
3
- import type { ColorHex, ColorP3, OklchColor } from "../types.js";
4
-
5
- export function hexToOklch(hex: ColorHex): OklchColor {
6
- const color = new Color(hex).to("oklch");
7
- const [l, c, h] = color.coords;
8
- return { l: l ?? 0, c: c ?? 0, h: h ?? 0 };
9
- }
10
-
11
- export function oklchToHex(oklch: OklchColor): ColorHex {
12
- const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
13
- return color.to("srgb").toString({ format: "hex" }) as ColorHex;
14
- }
15
-
16
- export function inSrgbGamut(oklch: OklchColor): boolean {
17
- const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
18
- return color.inGamut("srgb");
19
- }
20
-
21
- export function compressToSrgb(oklch: OklchColor): OklchColor {
22
- let current = { ...oklch };
23
- let iterations = 0;
24
-
25
- while (!inSrgbGamut(current) && current.c > 0 && iterations < 40) {
26
- current = { ...current, c: current.c * 0.95 };
27
- iterations += 1;
28
- }
29
-
30
- return current;
31
- }
32
-
33
- export function inP3Gamut(oklch: OklchColor): boolean {
34
- const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
35
- return color.inGamut("p3");
36
- }
37
-
38
- export function compressToP3(oklch: OklchColor): OklchColor {
39
- let current = { ...oklch };
40
- let iterations = 0;
41
-
42
- while (!inP3Gamut(current) && current.c > 0 && iterations < 40) {
43
- current = { ...current, c: current.c * 0.95 };
44
- iterations += 1;
45
- }
46
-
47
- return current;
48
- }
49
-
50
- export function oklchToP3(oklch: OklchColor): ColorP3 {
51
- const color = new Color("oklch", [oklch.l, oklch.c, oklch.h]);
52
- return color.to("p3").toString({ format: "color" }) as ColorP3;
53
- }
@@ -1,58 +0,0 @@
1
- import type { OklchColor, Step, TemplateId } from "../types.js";
2
-
3
- const steps: Step[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
4
-
5
- const lightnessLight = [0.99, 0.975, 0.95, 0.92, 0.89, 0.84, 0.77, 0.68, 0.58, 0.5, 0.4, 0.3];
6
- const lightnessDark = [0.12, 0.14, 0.18, 0.22, 0.26, 0.32, 0.4, 0.48, 0.58, 0.66, 0.76, 0.86];
7
-
8
- const chromaNeutral = [
9
- 0.01, 0.012, 0.016, 0.02, 0.024, 0.03, 0.04, 0.05, 0.055, 0.045, 0.035, 0.028,
10
- ];
11
- const chromaWarm = [0.02, 0.03, 0.05, 0.08, 0.12, 0.16, 0.18, 0.2, 0.22, 0.19, 0.12, 0.08];
12
- const chromaCool = [0.02, 0.03, 0.05, 0.075, 0.1, 0.14, 0.17, 0.19, 0.21, 0.18, 0.11, 0.08];
13
-
14
- const baseHue: Record<TemplateId, number> = {
15
- neutral: 250,
16
- warm: 40,
17
- cool: 220,
18
- };
19
-
20
- function buildTemplate(
21
- lightness: number[],
22
- chroma: number[],
23
- hue: number,
24
- ): Record<Step, OklchColor> {
25
- const output = {} as Record<Step, OklchColor>;
26
- for (let i = 0; i < steps.length; i += 1) {
27
- const step = steps[i];
28
- output[step] = {
29
- l: lightness[i],
30
- c: chroma[i],
31
- h: hue,
32
- };
33
- }
34
- return output;
35
- }
36
-
37
- export const templates = {
38
- light: {
39
- neutral: buildTemplate(lightnessLight, chromaNeutral, baseHue.neutral),
40
- warm: buildTemplate(lightnessLight, chromaWarm, baseHue.warm),
41
- cool: buildTemplate(lightnessLight, chromaCool, baseHue.cool),
42
- },
43
- dark: {
44
- neutral: buildTemplate(lightnessDark, chromaNeutral, baseHue.neutral),
45
- warm: buildTemplate(lightnessDark, chromaWarm, baseHue.warm),
46
- cool: buildTemplate(lightnessDark, chromaCool, baseHue.cool),
47
- },
48
- };
49
-
50
- export function selectTemplateId(oklch: OklchColor): TemplateId {
51
- if (oklch.c < 0.05) {
52
- return "neutral";
53
- }
54
-
55
- const hue = ((oklch.h % 360) + 360) % 360;
56
- const isWarm = hue <= 60 || hue >= 330;
57
- return isWarm ? "warm" : "cool";
58
- }
@@ -1,25 +0,0 @@
1
- import type { Theme, ThemeColorMode } from "../types.js";
2
-
3
- export function selectThemeColorMode(theme: Theme, mode: "srgb" | "p3"): ThemeColorMode {
4
- if (mode === "srgb") {
5
- return theme;
6
- }
7
-
8
- const scales = Object.fromEntries(
9
- Object.entries(theme.scales).map(([slot, scale]) => {
10
- if (!scale.p3) {
11
- return [slot, scale];
12
- }
13
- return [
14
- slot,
15
- {
16
- ...scale,
17
- light: scale.p3.light,
18
- dark: scale.p3.dark,
19
- },
20
- ];
21
- }),
22
- );
23
-
24
- return { ...theme, scales };
25
- }
@@ -1,116 +0,0 @@
1
- import Color from "colorjs.io";
2
-
3
- import type { ColorHex, Step, Theme } from "../types.js";
4
-
5
- type CssVarsOptions = {
6
- prefix?: string;
7
- includeTokens?: boolean;
8
- includeScales?: boolean;
9
- includeAlpha?: boolean;
10
- includeP3?: boolean;
11
- lightSelector?: string;
12
- darkSelector?: string;
13
- };
14
-
15
- function tokenToCssVar(token: string, prefix: string): string {
16
- const normalized = token.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
17
- const dashed = normalized.replace(/\./g, "-");
18
- return `--${prefix}-${dashed}`;
19
- }
20
-
21
- function scaleToCssVar(slot: string, step: Step, prefix: string): string {
22
- return `--${prefix}-scale-${slot}-${step}`;
23
- }
24
-
25
- function alphaToCssVar(step: Step, prefix: string): string {
26
- return `--${prefix}-alpha-${step}`;
27
- }
28
-
29
- function hexToP3(value: ColorHex): string {
30
- return new Color(value).to("p3").toString({ format: "color" });
31
- }
32
-
33
- export function toCssVars(theme: Theme, options?: CssVarsOptions): string {
34
- const prefix = options?.prefix ?? "pk";
35
- const includeTokens = options?.includeTokens ?? true;
36
- const includeScales = options?.includeScales ?? true;
37
- const includeAlpha = options?.includeAlpha ?? true;
38
- const includeP3 = options?.includeP3 ?? false;
39
- const lightSelector = options?.lightSelector ?? ":root";
40
- const darkSelector = options?.darkSelector ?? ".dark";
41
-
42
- const lines: string[] = [];
43
- const hasP3 = Object.values(theme.scales).some((scale) => scale.p3);
44
-
45
- function emitSelector(selector: string, mode: "light" | "dark") {
46
- lines.push(`${selector} {`);
47
-
48
- if (includeTokens) {
49
- for (const [token, value] of Object.entries(theme.tokens[mode])) {
50
- lines.push(` ${tokenToCssVar(token, prefix)}: ${value};`);
51
- }
52
- }
53
-
54
- if (includeScales) {
55
- for (const [slot, scale] of Object.entries(theme.scales)) {
56
- for (const [step, value] of Object.entries(scale[mode])) {
57
- lines.push(` ${scaleToCssVar(slot, Number(step) as Step, prefix)}: ${value};`);
58
- }
59
- }
60
- }
61
-
62
- if (includeAlpha && theme.alpha) {
63
- for (const [step, value] of Object.entries(theme.alpha[mode])) {
64
- lines.push(` ${alphaToCssVar(Number(step) as Step, prefix)}: ${value};`);
65
- }
66
- }
67
-
68
- lines.push("}");
69
- }
70
-
71
- emitSelector(lightSelector, "light");
72
- lines.push("");
73
- emitSelector(darkSelector, "dark");
74
-
75
- if (includeP3 && hasP3) {
76
- lines.push("");
77
- lines.push("@supports (color: color(display-p3 1 1 1)) {");
78
-
79
- function emitP3Selector(selector: string, mode: "light" | "dark") {
80
- lines.push(` ${selector} {`);
81
-
82
- if (includeTokens) {
83
- for (const [token, value] of Object.entries(theme.tokens[mode])) {
84
- lines.push(` ${tokenToCssVar(token, prefix)}: ${hexToP3(value)};`);
85
- }
86
- }
87
-
88
- if (includeScales) {
89
- for (const [slot, scale] of Object.entries(theme.scales)) {
90
- const p3Scale = scale.p3?.[mode];
91
- if (!p3Scale) {
92
- continue;
93
- }
94
- for (const [step, value] of Object.entries(p3Scale)) {
95
- lines.push(` ${scaleToCssVar(slot, Number(step) as Step, prefix)}: ${value};`);
96
- }
97
- }
98
- }
99
-
100
- if (includeAlpha && theme.alpha) {
101
- for (const [step, value] of Object.entries(theme.alpha[mode])) {
102
- lines.push(` ${alphaToCssVar(Number(step) as Step, prefix)}: ${hexToP3(value)};`);
103
- }
104
- }
105
-
106
- lines.push(" }");
107
- }
108
-
109
- emitP3Selector(lightSelector, "light");
110
- lines.push("");
111
- emitP3Selector(darkSelector, "dark");
112
- lines.push("}");
113
- }
114
-
115
- return lines.join("\n");
116
- }
@@ -1,31 +0,0 @@
1
- import type { Theme, ThemeColorMode } from "../types.js";
2
-
3
- export function toJson(theme: Theme): string {
4
- return JSON.stringify(theme, null, 2);
5
- }
6
-
7
- export function toJsonWithMode(theme: Theme, mode: "srgb" | "p3"): string {
8
- if (mode === "p3") {
9
- return JSON.stringify(toP3Theme(theme), null, 2);
10
- }
11
- return JSON.stringify(theme, null, 2);
12
- }
13
-
14
- function toP3Theme(theme: Theme): ThemeColorMode {
15
- const scales = Object.fromEntries(
16
- Object.entries(theme.scales).map(([slot, scale]) => {
17
- if (!scale.p3) {
18
- return [slot, scale];
19
- }
20
- return [
21
- slot,
22
- {
23
- ...scale,
24
- light: scale.p3.light,
25
- dark: scale.p3.dark,
26
- },
27
- ];
28
- }),
29
- );
30
- return { ...theme, scales };
31
- }
@@ -1,39 +0,0 @@
1
- import type { Theme } from "../types.js";
2
-
3
- type ReactNativeOptions = {
4
- includeTokens?: boolean;
5
- includeScales?: boolean;
6
- includeAlpha?: boolean;
7
- includeP3?: boolean;
8
- };
9
-
10
- export function toReactNative(theme: Theme, options?: ReactNativeOptions) {
11
- const includeTokens = options?.includeTokens ?? true;
12
- const includeScales = options?.includeScales ?? true;
13
- const includeAlpha = options?.includeAlpha ?? true;
14
- const includeP3 = options?.includeP3 ?? false;
15
-
16
- function buildMode(mode: "light" | "dark") {
17
- const scales = includeScales
18
- ? Object.fromEntries(Object.entries(theme.scales).map(([slot, scale]) => [slot, scale[mode]]))
19
- : {};
20
- const p3 = includeP3
21
- ? Object.fromEntries(
22
- Object.entries(theme.scales)
23
- .filter(([, scale]) => scale.p3?.[mode])
24
- .map(([slot, scale]) => [slot, scale.p3?.[mode]]),
25
- )
26
- : undefined;
27
- return {
28
- tokens: includeTokens ? theme.tokens[mode] : {},
29
- scales,
30
- alpha: includeAlpha ? theme.alpha?.[mode] : undefined,
31
- p3,
32
- };
33
- }
34
-
35
- return {
36
- light: buildMode("light"),
37
- dark: buildMode("dark"),
38
- };
39
- }
@@ -1,110 +0,0 @@
1
- import type { Theme } from "../types.js";
2
-
3
- type TailwindOptions = {
4
- mode?: "light" | "dark" | "both";
5
- includeTokens?: boolean;
6
- includeScales?: boolean;
7
- includeAlpha?: boolean;
8
- includeP3?: boolean;
9
- };
10
-
11
- function setNested(target: Record<string, unknown>, path: string[], value: unknown) {
12
- let cursor = target;
13
- for (let i = 0; i < path.length - 1; i += 1) {
14
- const key = path[i];
15
- if (!cursor[key] || typeof cursor[key] !== "object") {
16
- cursor[key] = {};
17
- }
18
- cursor = cursor[key] as Record<string, unknown>;
19
- }
20
- cursor[path[path.length - 1]] = value;
21
- }
22
-
23
- function tokensToNested(tokens: Record<string, string>): Record<string, unknown> {
24
- const output: Record<string, unknown> = {};
25
- for (const [token, value] of Object.entries(tokens)) {
26
- setNested(output, token.split("."), value);
27
- }
28
- return output;
29
- }
30
-
31
- function scalesToNested(scales: Theme["scales"], mode: "light" | "dark", useP3?: boolean) {
32
- const output: Record<string, unknown> = {};
33
- for (const [slot, scale] of Object.entries(scales)) {
34
- const source = useP3 ? scale.p3?.[mode] : scale[mode];
35
- if (!source) {
36
- continue;
37
- }
38
- const stepMap: Record<string, string> = {};
39
- for (const [step, value] of Object.entries(source)) {
40
- stepMap[String(step)] = value;
41
- }
42
- output[slot] = stepMap;
43
- }
44
- return output;
45
- }
46
-
47
- function alphaToNested(alpha: Theme["alpha"], mode: "light" | "dark") {
48
- if (!alpha) {
49
- return undefined;
50
- }
51
- const output: Record<string, string> = {};
52
- for (const [step, value] of Object.entries(alpha[mode])) {
53
- output[String(step)] = value;
54
- }
55
- return output;
56
- }
57
-
58
- export function toTailwind(theme: Theme, options?: TailwindOptions) {
59
- const mode = options?.mode ?? "both";
60
- const includeTokens = options?.includeTokens ?? true;
61
- const includeScales = options?.includeScales ?? false;
62
- const includeAlpha = options?.includeAlpha ?? false;
63
- const includeP3 = options?.includeP3 ?? false;
64
-
65
- function buildModeTokens(modeKey: "light" | "dark", useP3?: boolean) {
66
- const colors: Record<string, unknown> = {};
67
-
68
- if (includeTokens) {
69
- colors.tokens = tokensToNested(theme.tokens[modeKey]);
70
- }
71
- if (includeScales) {
72
- colors.scale = scalesToNested(theme.scales, modeKey, useP3);
73
- }
74
- if (includeAlpha && theme.alpha) {
75
- colors.alpha = alphaToNested(theme.alpha, modeKey);
76
- }
77
-
78
- return colors;
79
- }
80
-
81
- const colors: Record<string, unknown> = {};
82
- if (mode === "light" || mode === "both") {
83
- colors.light = buildModeTokens("light");
84
- }
85
- if (mode === "dark" || mode === "both") {
86
- colors.dark = buildModeTokens("dark");
87
- }
88
-
89
- if (includeP3) {
90
- const hasP3 = Object.values(theme.scales).some((scale) => scale.p3);
91
- if (hasP3) {
92
- const p3: Record<string, unknown> = {};
93
- if (mode === "light" || mode === "both") {
94
- p3.light = buildModeTokens("light", true);
95
- }
96
- if (mode === "dark" || mode === "both") {
97
- p3.dark = buildModeTokens("dark", true);
98
- }
99
- colors.p3 = p3;
100
- }
101
- }
102
-
103
- return {
104
- theme: {
105
- extend: {
106
- colors,
107
- },
108
- },
109
- };
110
- }
@@ -1,34 +0,0 @@
1
- import type { Theme, ThemeColorMode } from "../types.js";
2
-
3
- export function toTs(theme: Theme): string {
4
- const serialized = JSON.stringify(theme, null, 2);
5
- return `export const theme = ${serialized} as const;\n`;
6
- }
7
-
8
- export function toTsWithMode(theme: Theme, mode: "srgb" | "p3"): string {
9
- if (mode === "p3") {
10
- const serialized = JSON.stringify(toP3Theme(theme), null, 2);
11
- return `export const theme = ${serialized} as const;\n`;
12
- }
13
- const serialized = JSON.stringify(theme, null, 2);
14
- return `export const theme = ${serialized} as const;\n`;
15
- }
16
-
17
- function toP3Theme(theme: Theme): ThemeColorMode {
18
- const scales = Object.fromEntries(
19
- Object.entries(theme.scales).map(([slot, scale]) => {
20
- if (!scale.p3) {
21
- return [slot, scale];
22
- }
23
- return [
24
- slot,
25
- {
26
- ...scale,
27
- light: scale.p3.light,
28
- dark: scale.p3.dark,
29
- },
30
- ];
31
- }),
32
- );
33
- return { ...theme, scales };
34
- }
@@ -1,163 +0,0 @@
1
- import { radixSeeds } from "./data/radixSeeds.js";
2
- import { type CurveConfig, resolveCurves } from "./engine/curves.js";
3
- import {
4
- compressToP3,
5
- compressToSrgb,
6
- hexToOklch,
7
- inP3Gamut,
8
- inSrgbGamut,
9
- oklchToHex,
10
- oklchToP3,
11
- } from "./engine/oklch.js";
12
- import { selectTemplateId, templates } from "./engine/templates.js";
13
- import type { ColorHex, ColorP3, ColorSource, Scale, Step, TemplateId } from "./types.js";
14
-
15
- const steps: Step[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
16
-
17
- type GenerateScaleOptions = {
18
- source: ColorSource;
19
- mode?: "light" | "dark" | "both";
20
- anchorStep?: Step;
21
- template?: "auto" | TemplateId;
22
- curves?: CurveConfig;
23
- gamut?: { strategy: "compress" | "clip" };
24
- p3?: boolean;
25
- };
26
-
27
- function getSeedHex(source: ColorSource): ColorHex {
28
- if (source.source === "seed") {
29
- return source.value;
30
- }
31
-
32
- const seed = radixSeeds[source.name];
33
- if (!seed) {
34
- throw new Error(`Unknown Radix seed: ${source.name}`);
35
- }
36
- return seed;
37
- }
38
-
39
- function normalizeHue(hue: number): number {
40
- const normalized = ((hue % 360) + 360) % 360;
41
- return normalized;
42
- }
43
-
44
- function buildScaleForMode(
45
- seedHex: ColorHex,
46
- templateId: TemplateId,
47
- anchorStep: Step,
48
- curves?: CurveConfig,
49
- gamutStrategy: "compress" | "clip" = "compress",
50
- mode: "light" | "dark" = "light",
51
- includeP3 = false,
52
- ): {
53
- scale: Record<Step, ColorHex>;
54
- p3?: Record<Step, ColorP3>;
55
- outOfGamutCount: number;
56
- outOfP3GamutCount: number;
57
- } {
58
- const seedOklch = hexToOklch(seedHex);
59
- const template = templates[mode][templateId];
60
- const anchor = template[anchorStep];
61
- const dL = seedOklch.l - anchor.l;
62
- const dC = seedOklch.c - anchor.c;
63
- const dH = seedOklch.h - anchor.h;
64
- const curveSet = resolveCurves(curves);
65
-
66
- const output = {} as Record<Step, ColorHex>;
67
- const p3Output = includeP3 ? ({} as Record<Step, ColorP3>) : undefined;
68
- let outOfGamutCount = 0;
69
- let outOfP3GamutCount = 0;
70
-
71
- for (const step of steps) {
72
- const base = template[step];
73
- const l = base.l + dL * curveSet.lightness[step];
74
- const c = Math.max(0, base.c + dC * curveSet.chroma[step]);
75
- const h = normalizeHue(base.h + dH);
76
- const candidate = { l, c, h };
77
- let current = candidate;
78
-
79
- if (!inSrgbGamut(current)) {
80
- outOfGamutCount += 1;
81
- if (gamutStrategy === "compress") {
82
- current = compressToSrgb(current);
83
- }
84
- }
85
-
86
- output[step] = oklchToHex(current);
87
-
88
- if (p3Output) {
89
- let p3Current = candidate;
90
- if (!inP3Gamut(p3Current)) {
91
- outOfP3GamutCount += 1;
92
- p3Current = compressToP3(p3Current);
93
- }
94
- p3Output[step] = oklchToP3(p3Current);
95
- }
96
- }
97
-
98
- return {
99
- scale: output,
100
- p3: p3Output,
101
- outOfGamutCount,
102
- outOfP3GamutCount,
103
- };
104
- }
105
-
106
- export function generateScale(options: GenerateScaleOptions): Scale {
107
- const seedHex = getSeedHex(options.source);
108
- const anchorStep = options.anchorStep ?? 9;
109
- const mode = options.mode ?? "both";
110
- const templateId =
111
- options.template === "auto" || !options.template
112
- ? selectTemplateId(hexToOklch(seedHex))
113
- : options.template;
114
- const gamutStrategy = options.gamut?.strategy ?? "compress";
115
-
116
- const lightResult = buildScaleForMode(
117
- seedHex,
118
- templateId,
119
- anchorStep,
120
- options.curves,
121
- gamutStrategy,
122
- "light",
123
- options.p3 ?? false,
124
- );
125
-
126
- const darkResult = buildScaleForMode(
127
- seedHex,
128
- templateId,
129
- anchorStep,
130
- options.curves,
131
- gamutStrategy,
132
- "dark",
133
- options.p3 ?? false,
134
- );
135
-
136
- const scale: Scale = {
137
- light: lightResult.scale,
138
- dark: darkResult.scale,
139
- p3:
140
- lightResult.p3 && darkResult.p3 ? { light: lightResult.p3, dark: darkResult.p3 } : undefined,
141
- meta: {
142
- outOfGamutCount: lightResult.outOfGamutCount + darkResult.outOfGamutCount,
143
- outOfP3GamutCount: lightResult.outOfP3GamutCount + darkResult.outOfP3GamutCount,
144
- },
145
- };
146
-
147
- if (mode === "light") {
148
- return {
149
- ...scale,
150
- dark: scale.light,
151
- p3: scale.p3 ? { light: scale.p3.light, dark: scale.p3.light } : undefined,
152
- };
153
- }
154
- if (mode === "dark") {
155
- return {
156
- ...scale,
157
- light: scale.dark,
158
- p3: scale.p3 ? { light: scale.p3.dark, dark: scale.p3.dark } : undefined,
159
- };
160
- }
161
-
162
- return scale;
163
- }