@tokens-studio/tokenscript-schemas 0.3.5 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokens-studio/tokenscript-schemas",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Schema registry for TokenScript with bundled schemas and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,15 +5,20 @@ import { bundleSelectiveSchemas } from "./selective-bundler.js";
5
5
  const SCHEMAS_DIR = join(process.cwd(), "src/schemas");
6
6
 
7
7
  describe("Selective Bundler", () => {
8
- it("should bundle a single type schema", async () => {
8
+ it("should bundle a single type schema with its dependencies", async () => {
9
9
  const result = await bundleSelectiveSchemas({
10
10
  schemas: ["hex-color"],
11
11
  schemasDir: SCHEMAS_DIR,
12
12
  });
13
13
 
14
- expect(result.schemas).toHaveLength(1);
15
- expect(result.schemas[0].uri).toContain("hex-color");
16
- expect(result.schemas[0].schema.type).toBe("color");
14
+ // hex-color has srgb-color dependency (for .to.hex() conversion)
15
+ expect(result.schemas.length).toBeGreaterThanOrEqual(1);
16
+
17
+ const uris = result.schemas.map((s) => s.uri);
18
+ expect(uris.some((uri) => uri.includes("hex-color"))).toBe(true);
19
+
20
+ const hexSchema = result.schemas.find((s) => s.uri.includes("hex-color"));
21
+ expect(hexSchema?.schema.type).toBe("color");
17
22
  expect(result.metadata.requestedSchemas).toEqual(["hex-color"]);
18
23
  });
19
24
 
@@ -57,7 +62,10 @@ describe("Selective Bundler", () => {
57
62
  schemasDir: SCHEMAS_DIR,
58
63
  });
59
64
 
60
- const hexSchema = result.schemas[0].schema;
65
+ const hexSchemaEntry = result.schemas.find((s) => s.uri.includes("hex-color"));
66
+ expect(hexSchemaEntry).toBeDefined();
67
+
68
+ const hexSchema = hexSchemaEntry!.schema;
61
69
 
62
70
  if (hexSchema.type === "color") {
63
71
  // Check that initializer script is inlined (not a file reference)
@@ -76,7 +84,9 @@ describe("Selective Bundler", () => {
76
84
  baseUrl: customBaseUrl,
77
85
  });
78
86
 
79
- expect(result.schemas[0].uri).toContain(customBaseUrl);
87
+ const hexSchemaEntry = result.schemas.find((s) => s.uri.includes("hex-color"));
88
+ expect(hexSchemaEntry).toBeDefined();
89
+ expect(hexSchemaEntry!.uri).toContain(customBaseUrl);
80
90
  });
81
91
 
82
92
  it("should include metadata", async () => {
@@ -0,0 +1,124 @@
1
+ // HSL to Hex Conversion
2
+ // Converts HSL to hexadecimal string format
3
+ // Reference: Standard HSL to RGB algorithm
4
+ //
5
+ // Input: Color.HSL with h (0-360), s (0-1), l (0-1)
6
+ // Output: Hex string #rrggbb
7
+
8
+ // Get input HSL values
9
+ variable h: Number = {input}.h;
10
+ variable s: Number = {input}.s;
11
+ variable l: Number = {input}.l;
12
+
13
+ // Normalize hue to 0-1 range
14
+ variable hue: Number = h / 360;
15
+
16
+ // RGB values (default to achromatic)
17
+ variable r: Number = l;
18
+ variable g: Number = l;
19
+ variable b: Number = l;
20
+
21
+ // Only calculate if there's saturation
22
+ if (s > 0) [
23
+ variable q: Number = 0;
24
+ if (l < 0.5) [
25
+ q = l * (1 + s);
26
+ ] else [
27
+ q = l + s - l * s;
28
+ ];
29
+
30
+ variable p: Number = 2 * l - q;
31
+
32
+ // Red (hue + 1/3)
33
+ variable tr: Number = hue + 0.333333333333333;
34
+ if (tr < 0) [ tr = tr + 1; ];
35
+ if (tr > 1) [ tr = tr - 1; ];
36
+
37
+ if (tr < 0.166666666666667) [
38
+ r = p + (q - p) * 6 * tr;
39
+ ] else [
40
+ if (tr < 0.5) [
41
+ r = q;
42
+ ] else [
43
+ if (tr < 0.666666666666667) [
44
+ r = p + (q - p) * (0.666666666666667 - tr) * 6;
45
+ ] else [
46
+ r = p;
47
+ ];
48
+ ];
49
+ ];
50
+
51
+ // Green (hue)
52
+ variable tg: Number = hue;
53
+ if (tg < 0) [ tg = tg + 1; ];
54
+ if (tg > 1) [ tg = tg - 1; ];
55
+
56
+ if (tg < 0.166666666666667) [
57
+ g = p + (q - p) * 6 * tg;
58
+ ] else [
59
+ if (tg < 0.5) [
60
+ g = q;
61
+ ] else [
62
+ if (tg < 0.666666666666667) [
63
+ g = p + (q - p) * (0.666666666666667 - tg) * 6;
64
+ ] else [
65
+ g = p;
66
+ ];
67
+ ];
68
+ ];
69
+
70
+ // Blue (hue - 1/3)
71
+ variable tb: Number = hue - 0.333333333333333;
72
+ if (tb < 0) [ tb = tb + 1; ];
73
+ if (tb > 1) [ tb = tb - 1; ];
74
+
75
+ if (tb < 0.166666666666667) [
76
+ b = p + (q - p) * 6 * tb;
77
+ ] else [
78
+ if (tb < 0.5) [
79
+ b = q;
80
+ ] else [
81
+ if (tb < 0.666666666666667) [
82
+ b = p + (q - p) * (0.666666666666667 - tb) * 6;
83
+ ] else [
84
+ b = p;
85
+ ];
86
+ ];
87
+ ];
88
+ ];
89
+
90
+ // Convert RGB to hex
91
+ variable hex: String = "#";
92
+ variable value: Number = 0;
93
+
94
+ // Red
95
+ value = round(r * 255);
96
+ if (value < 0) [ value = 0; ];
97
+ if (value > 255) [ value = 255; ];
98
+ if (value < 16) [
99
+ hex = hex.concat("0").concat(value.to_string(16));
100
+ ] else [
101
+ hex = hex.concat(value.to_string(16));
102
+ ];
103
+
104
+ // Green
105
+ value = round(g * 255);
106
+ if (value < 0) [ value = 0; ];
107
+ if (value > 255) [ value = 255; ];
108
+ if (value < 16) [
109
+ hex = hex.concat("0").concat(value.to_string(16));
110
+ ] else [
111
+ hex = hex.concat(value.to_string(16));
112
+ ];
113
+
114
+ // Blue
115
+ value = round(b * 255);
116
+ if (value < 0) [ value = 0; ];
117
+ if (value > 255) [ value = 255; ];
118
+ if (value < 16) [
119
+ hex = hex.concat("0").concat(value.to_string(16));
120
+ ] else [
121
+ hex = hex.concat(value.to_string(16));
122
+ ];
123
+
124
+ return hex;
@@ -0,0 +1,119 @@
1
+ // OKLCH to Hex Conversion
2
+ // Converts OKLCH perceptual color to hexadecimal string format
3
+ // Path: OKLCH → OKLab → XYZ-D65 → Linear sRGB → sRGB → Hex
4
+ //
5
+ // Input: Color.OKLCH with l (0-1), c, h (0-360)
6
+ // Output: Hex string #rrggbb
7
+
8
+ // Get input OKLCH values
9
+ variable ok_l: Number = {input}.l;
10
+ variable ok_c: Number = {input}.c;
11
+ variable ok_h: Number = {input}.h;
12
+
13
+ // === Step 1: OKLCH to OKLab (polar to cartesian) ===
14
+ variable pi: Number = pi();
15
+ variable deg_to_rad: Number = pi / 180;
16
+ variable h_rad: Number = ok_h * deg_to_rad;
17
+
18
+ variable lab_a: Number = ok_c * cos(h_rad);
19
+ variable lab_b: Number = ok_c * sin(h_rad);
20
+
21
+ // === Step 2: OKLab to XYZ-D65 ===
22
+ // Inverse Lab-to-LMS matrix
23
+ variable lms_l: Number = 1.0 * ok_l + 0.3963377773761749 * lab_a + 0.2158037573099136 * lab_b;
24
+ variable lms_m: Number = 1.0 * ok_l + -0.1055613458156586 * lab_a + -0.0638541728258133 * lab_b;
25
+ variable lms_s: Number = 1.0 * ok_l + -0.0894841775298119 * lab_a + -1.2914855480194092 * lab_b;
26
+
27
+ // Cube the values (inverse of cube root)
28
+ variable lms_l_cubed: Number = lms_l * lms_l * lms_l;
29
+ variable lms_m_cubed: Number = lms_m * lms_m * lms_m;
30
+ variable lms_s_cubed: Number = lms_s * lms_s * lms_s;
31
+
32
+ // Inverse LMS-to-XYZ matrix
33
+ variable xyz_x: Number = 1.2268798758459243 * lms_l_cubed + -0.5578149944602171 * lms_m_cubed + 0.2813910456659647 * lms_s_cubed;
34
+ variable xyz_y: Number = -0.0405757452148008 * lms_l_cubed + 1.1122868032803170 * lms_m_cubed + -0.0717110580655164 * lms_s_cubed;
35
+ variable xyz_z: Number = -0.0763729366746601 * lms_l_cubed + -0.4214933324022432 * lms_m_cubed + 1.5869240198367816 * lms_s_cubed;
36
+
37
+ // === Step 3: XYZ-D65 to Linear sRGB ===
38
+ variable linear_r: Number = 3.2409699419045226 * xyz_x + -1.537383177570094 * xyz_y + -0.4986107602930034 * xyz_z;
39
+ variable linear_g: Number = -0.9692436362808796 * xyz_x + 1.8759675015077202 * xyz_y + 0.04155505740717559 * xyz_z;
40
+ variable linear_b: Number = 0.05563007969699366 * xyz_x + -0.20397695888897652 * xyz_y + 1.0569715142428786 * xyz_z;
41
+
42
+ // === Step 4: Linear sRGB to sRGB (gamma correction) ===
43
+ variable threshold: Number = 0.0031308;
44
+ variable linear_scale: Number = 12.92;
45
+ variable gamma_offset: Number = 0.055;
46
+ variable gamma_scale: Number = 1.055;
47
+ variable gamma_exp: Number = 0.416666666666667;
48
+
49
+ variable srgb_r: Number = 0;
50
+ if (linear_r <= threshold) [
51
+ srgb_r = linear_r * linear_scale;
52
+ ] else [
53
+ if (linear_r > 0) [
54
+ srgb_r = gamma_scale * pow(linear_r, gamma_exp) - gamma_offset;
55
+ ] else [
56
+ srgb_r = 0;
57
+ ];
58
+ ];
59
+
60
+ variable srgb_g: Number = 0;
61
+ if (linear_g <= threshold) [
62
+ srgb_g = linear_g * linear_scale;
63
+ ] else [
64
+ if (linear_g > 0) [
65
+ srgb_g = gamma_scale * pow(linear_g, gamma_exp) - gamma_offset;
66
+ ] else [
67
+ srgb_g = 0;
68
+ ];
69
+ ];
70
+
71
+ variable srgb_b: Number = 0;
72
+ if (linear_b <= threshold) [
73
+ srgb_b = linear_b * linear_scale;
74
+ ] else [
75
+ if (linear_b > 0) [
76
+ srgb_b = gamma_scale * pow(linear_b, gamma_exp) - gamma_offset;
77
+ ] else [
78
+ srgb_b = 0;
79
+ ];
80
+ ];
81
+
82
+ // === Step 5: sRGB to Hex ===
83
+ variable hex: String = "#";
84
+ variable value: Number = 0;
85
+
86
+ // Red (clamp to 0-1)
87
+ value = srgb_r;
88
+ if (value < 0) [ value = 0; ];
89
+ if (value > 1) [ value = 1; ];
90
+ value = round(value * 255);
91
+ if (value < 16) [
92
+ hex = hex.concat("0").concat(value.to_string(16));
93
+ ] else [
94
+ hex = hex.concat(value.to_string(16));
95
+ ];
96
+
97
+ // Green
98
+ value = srgb_g;
99
+ if (value < 0) [ value = 0; ];
100
+ if (value > 1) [ value = 1; ];
101
+ value = round(value * 255);
102
+ if (value < 16) [
103
+ hex = hex.concat("0").concat(value.to_string(16));
104
+ ] else [
105
+ hex = hex.concat(value.to_string(16));
106
+ ];
107
+
108
+ // Blue
109
+ value = srgb_b;
110
+ if (value < 0) [ value = 0; ];
111
+ if (value > 1) [ value = 1; ];
112
+ value = round(value * 255);
113
+ if (value < 16) [
114
+ hex = hex.concat("0").concat(value.to_string(16));
115
+ ] else [
116
+ hex = hex.concat(value.to_string(16));
117
+ ];
118
+
119
+ return hex;
@@ -0,0 +1,45 @@
1
+ // Display P3 to Hex Conversion
2
+ // Converts P3 (0-1) to hexadecimal string format
3
+ // Note: P3 colors may be out of sRGB gamut, values are clamped to 0-1
4
+ //
5
+ // Examples:
6
+ // P3(1, 0, 0) → #ff0000
7
+ // P3(0, 1, 0.5) → #00ff80
8
+
9
+ variable hex: String = "#";
10
+ variable value: Number = 0;
11
+
12
+ // Red channel (clamp P3 to sRGB range)
13
+ value = {input}.r;
14
+ if (value < 0) [ value = 0; ];
15
+ if (value > 1) [ value = 1; ];
16
+ value = round(value * 255);
17
+ if (value < 16) [
18
+ hex = hex.concat("0").concat(value.to_string(16));
19
+ ] else [
20
+ hex = hex.concat(value.to_string(16));
21
+ ];
22
+
23
+ // Green channel
24
+ value = {input}.g;
25
+ if (value < 0) [ value = 0; ];
26
+ if (value > 1) [ value = 1; ];
27
+ value = round(value * 255);
28
+ if (value < 16) [
29
+ hex = hex.concat("0").concat(value.to_string(16));
30
+ ] else [
31
+ hex = hex.concat(value.to_string(16));
32
+ ];
33
+
34
+ // Blue channel
35
+ value = {input}.b;
36
+ if (value < 0) [ value = 0; ];
37
+ if (value > 1) [ value = 1; ];
38
+ value = round(value * 255);
39
+ if (value < 16) [
40
+ hex = hex.concat("0").concat(value.to_string(16));
41
+ ] else [
42
+ hex = hex.concat(value.to_string(16));
43
+ ];
44
+
45
+ return hex;
@@ -0,0 +1,41 @@
1
+ // sRGB to Hex Conversion
2
+ // Converts sRGB (0-1) to hexadecimal string format
3
+ //
4
+ // Examples:
5
+ // sRGB(1, 0, 0) → #ff0000
6
+ // sRGB(0, 1, 0.5) → #00ff80
7
+
8
+ variable hex: String = "#";
9
+ variable value: Number = 0;
10
+
11
+ // Red channel
12
+ value = round({input}.r * 255);
13
+ if (value < 0) [ value = 0; ];
14
+ if (value > 255) [ value = 255; ];
15
+ if (value < 16) [
16
+ hex = hex.concat("0").concat(value.to_string(16));
17
+ ] else [
18
+ hex = hex.concat(value.to_string(16));
19
+ ];
20
+
21
+ // Green channel
22
+ value = round({input}.g * 255);
23
+ if (value < 0) [ value = 0; ];
24
+ if (value > 255) [ value = 255; ];
25
+ if (value < 16) [
26
+ hex = hex.concat("0").concat(value.to_string(16));
27
+ ] else [
28
+ hex = hex.concat(value.to_string(16));
29
+ ];
30
+
31
+ // Blue channel
32
+ value = round({input}.b * 255);
33
+ if (value < 0) [ value = 0; ];
34
+ if (value > 255) [ value = 255; ];
35
+ if (value < 16) [
36
+ hex = hex.concat("0").concat(value.to_string(16));
37
+ ] else [
38
+ hex = hex.concat(value.to_string(16));
39
+ ];
40
+
41
+ return hex;
@@ -20,5 +20,46 @@
20
20
  }
21
21
  }
22
22
  ],
23
- "conversions": []
23
+ "conversions": [
24
+ {
25
+ "source": "/api/v1/core/srgb-color/0/",
26
+ "target": "$self",
27
+ "description": "Converts sRGB (0-1) to Hex format",
28
+ "lossless": true,
29
+ "script": {
30
+ "type": "/api/v1/core/tokenscript/0/",
31
+ "script": "./from-srgb.tokenscript"
32
+ }
33
+ },
34
+ {
35
+ "source": "/api/v1/core/p3-color/0/",
36
+ "target": "$self",
37
+ "description": "Converts Display P3 to Hex format (clamps to sRGB gamut)",
38
+ "lossless": false,
39
+ "script": {
40
+ "type": "/api/v1/core/tokenscript/0/",
41
+ "script": "./from-p3.tokenscript"
42
+ }
43
+ },
44
+ {
45
+ "source": "/api/v1/core/hsl-color/0/",
46
+ "target": "$self",
47
+ "description": "Converts HSL to Hex format",
48
+ "lossless": true,
49
+ "script": {
50
+ "type": "/api/v1/core/tokenscript/0/",
51
+ "script": "./from-hsl.tokenscript"
52
+ }
53
+ },
54
+ {
55
+ "source": "/api/v1/core/oklch-color/0/",
56
+ "target": "$self",
57
+ "description": "Converts OKLCH to Hex format (clamps to sRGB gamut)",
58
+ "lossless": false,
59
+ "script": {
60
+ "type": "/api/v1/core/tokenscript/0/",
61
+ "script": "./from-oklch.tokenscript"
62
+ }
63
+ }
64
+ ]
24
65
  }
@@ -138,4 +138,249 @@ describe("Hex Color Schema", () => {
138
138
 
139
139
  // Note: Conversion tests are in rgb-color/unit.test.ts since RGB owns the conversions
140
140
  });
141
+
142
+ describe("Conversions", () => {
143
+ it("should convert sRGB to hex (red)", async () => {
144
+ const result = await executeWithSchema(
145
+ "hex-color",
146
+ "type",
147
+ `
148
+ variable color: Color.SRGB;
149
+ color.r = 1; color.g = 0; color.b = 0;
150
+ color.to.hex()
151
+ `,
152
+ );
153
+
154
+ expect((result as any).value).toBe("#ff0000");
155
+ });
156
+
157
+ it("should convert sRGB to hex (green)", async () => {
158
+ const result = await executeWithSchema(
159
+ "hex-color",
160
+ "type",
161
+ `
162
+ variable color: Color.SRGB;
163
+ color.r = 0; color.g = 1; color.b = 0;
164
+ color.to.hex()
165
+ `,
166
+ );
167
+
168
+ expect((result as any).value).toBe("#00ff00");
169
+ });
170
+
171
+ it("should convert sRGB to hex (blue)", async () => {
172
+ const result = await executeWithSchema(
173
+ "hex-color",
174
+ "type",
175
+ `
176
+ variable color: Color.SRGB;
177
+ color.r = 0; color.g = 0; color.b = 1;
178
+ color.to.hex()
179
+ `,
180
+ );
181
+
182
+ expect((result as any).value).toBe("#0000ff");
183
+ });
184
+
185
+ it("should convert sRGB to hex (mid values)", async () => {
186
+ const result = await executeWithSchema(
187
+ "hex-color",
188
+ "type",
189
+ `
190
+ variable color: Color.SRGB;
191
+ color.r = 0.5; color.g = 0.5; color.b = 0.5;
192
+ color.to.hex()
193
+ `,
194
+ );
195
+
196
+ // 0.5 * 255 = 127.5, rounds to 128 = 0x80
197
+ expect((result as any).value).toBe("#808080");
198
+ });
199
+
200
+ it("should convert sRGB to hex (black)", async () => {
201
+ const result = await executeWithSchema(
202
+ "hex-color",
203
+ "type",
204
+ `
205
+ variable color: Color.SRGB;
206
+ color.r = 0; color.g = 0; color.b = 0;
207
+ color.to.hex()
208
+ `,
209
+ );
210
+
211
+ expect((result as any).value).toBe("#000000");
212
+ });
213
+
214
+ it("should convert sRGB to hex (white)", async () => {
215
+ const result = await executeWithSchema(
216
+ "hex-color",
217
+ "type",
218
+ `
219
+ variable color: Color.SRGB;
220
+ color.r = 1; color.g = 1; color.b = 1;
221
+ color.to.hex()
222
+ `,
223
+ );
224
+
225
+ expect((result as any).value).toBe("#ffffff");
226
+ });
227
+
228
+ // P3 to Hex conversions
229
+ it("should convert P3 to hex (red)", async () => {
230
+ const result = await executeWithSchema(
231
+ "hex-color",
232
+ "type",
233
+ `
234
+ variable color: Color.P3;
235
+ color.r = 1; color.g = 0; color.b = 0;
236
+ color.to.hex()
237
+ `,
238
+ );
239
+
240
+ expect((result as any).value).toBe("#ff0000");
241
+ });
242
+
243
+ it("should convert P3 to hex (clamps out-of-gamut)", async () => {
244
+ const result = await executeWithSchema(
245
+ "hex-color",
246
+ "type",
247
+ `
248
+ variable color: Color.P3;
249
+ color.r = 1.2; color.g = -0.1; color.b = 0.5;
250
+ color.to.hex()
251
+ `,
252
+ );
253
+
254
+ // 1.2 clamps to 1 (ff), -0.1 clamps to 0 (00), 0.5 = 80
255
+ expect((result as any).value).toBe("#ff0080");
256
+ });
257
+
258
+ // HSL to Hex conversions
259
+ it("should convert HSL to hex (red)", async () => {
260
+ const result = await executeWithSchema(
261
+ "hex-color",
262
+ "type",
263
+ `
264
+ variable color: Color.HSL;
265
+ color.h = 0; color.s = 1; color.l = 0.5;
266
+ color.to.hex()
267
+ `,
268
+ );
269
+
270
+ expect((result as any).value).toBe("#ff0000");
271
+ });
272
+
273
+ it("should convert HSL to hex (green)", async () => {
274
+ const result = await executeWithSchema(
275
+ "hex-color",
276
+ "type",
277
+ `
278
+ variable color: Color.HSL;
279
+ color.h = 120; color.s = 1; color.l = 0.5;
280
+ color.to.hex()
281
+ `,
282
+ );
283
+
284
+ expect((result as any).value).toBe("#00ff00");
285
+ });
286
+
287
+ it("should convert HSL to hex (blue)", async () => {
288
+ const result = await executeWithSchema(
289
+ "hex-color",
290
+ "type",
291
+ `
292
+ variable color: Color.HSL;
293
+ color.h = 240; color.s = 1; color.l = 0.5;
294
+ color.to.hex()
295
+ `,
296
+ );
297
+
298
+ expect((result as any).value).toBe("#0000ff");
299
+ });
300
+
301
+ it("should convert HSL to hex (gray - no saturation)", async () => {
302
+ const result = await executeWithSchema(
303
+ "hex-color",
304
+ "type",
305
+ `
306
+ variable color: Color.HSL;
307
+ color.h = 0; color.s = 0; color.l = 0.5;
308
+ color.to.hex()
309
+ `,
310
+ );
311
+
312
+ expect((result as any).value).toBe("#808080");
313
+ });
314
+
315
+ // OKLCH to Hex conversions
316
+ it("should convert OKLCH to hex (red-ish)", async () => {
317
+ const result = await executeWithSchema(
318
+ "hex-color",
319
+ "type",
320
+ `
321
+ variable color: Color.OKLCH;
322
+ color.l = 0.628; color.c = 0.258; color.h = 29;
323
+ color.to.hex()
324
+ `,
325
+ );
326
+
327
+ // OKLCH red is approximately l=0.628, c=0.258, h=29
328
+ // Should produce something close to #ff0000
329
+ const hex = (result as any).value as string;
330
+ expect(hex).toMatch(/^#[0-9a-f]{6}$/);
331
+ // Red channel should be high
332
+ const r = parseInt(hex.slice(1, 3), 16);
333
+ expect(r).toBeGreaterThan(200);
334
+ });
335
+
336
+ it("should convert OKLCH to hex (white)", async () => {
337
+ const result = await executeWithSchema(
338
+ "hex-color",
339
+ "type",
340
+ `
341
+ variable color: Color.OKLCH;
342
+ color.l = 1; color.c = 0; color.h = 0;
343
+ color.to.hex()
344
+ `,
345
+ );
346
+
347
+ expect((result as any).value).toBe("#ffffff");
348
+ });
349
+
350
+ it("should convert OKLCH to hex (black)", async () => {
351
+ const result = await executeWithSchema(
352
+ "hex-color",
353
+ "type",
354
+ `
355
+ variable color: Color.OKLCH;
356
+ color.l = 0; color.c = 0; color.h = 0;
357
+ color.to.hex()
358
+ `,
359
+ );
360
+
361
+ expect((result as any).value).toBe("#000000");
362
+ });
363
+
364
+ it("should convert OKLCH to hex (gray)", async () => {
365
+ const result = await executeWithSchema(
366
+ "hex-color",
367
+ "type",
368
+ `
369
+ variable color: Color.OKLCH;
370
+ color.l = 0.6; color.c = 0; color.h = 0;
371
+ color.to.hex()
372
+ `,
373
+ );
374
+
375
+ // Gray with lightness 0.6 should be mid-gray
376
+ const hex = (result as any).value as string;
377
+ expect(hex).toMatch(/^#[0-9a-f]{6}$/);
378
+ const r = parseInt(hex.slice(1, 3), 16);
379
+ const g = parseInt(hex.slice(3, 5), 16);
380
+ const b = parseInt(hex.slice(5, 7), 16);
381
+ // Should be achromatic (r ≈ g ≈ b)
382
+ expect(Math.abs(r - g)).toBeLessThan(2);
383
+ expect(Math.abs(g - b)).toBeLessThan(2);
384
+ });
385
+ });
141
386
  });