@synnaxlabs/x 0.53.0 → 0.54.0

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 (119) hide show
  1. package/.turbo/turbo-build.log +14 -8
  2. package/dist/src/array/nullable.d.ts +10 -1
  3. package/dist/src/array/nullable.d.ts.map +1 -1
  4. package/dist/src/binary/codec.d.ts +5 -5
  5. package/dist/src/binary/codec.d.ts.map +1 -1
  6. package/dist/src/caseconv/caseconv.bench.d.ts +2 -0
  7. package/dist/src/caseconv/caseconv.bench.d.ts.map +1 -0
  8. package/dist/src/caseconv/caseconv.d.ts +19 -1
  9. package/dist/src/caseconv/caseconv.d.ts.map +1 -1
  10. package/dist/src/color/color.d.ts +22 -24
  11. package/dist/src/color/color.d.ts.map +1 -1
  12. package/dist/src/color/gradient.d.ts +10 -10
  13. package/dist/src/color/palette.d.ts +16 -16
  14. package/dist/src/control/control.d.ts +21 -37
  15. package/dist/src/control/control.d.ts.map +1 -1
  16. package/dist/src/control/external.d.ts +3 -0
  17. package/dist/src/control/external.d.ts.map +1 -0
  18. package/dist/src/control/index.d.ts +1 -1
  19. package/dist/src/control/index.d.ts.map +1 -1
  20. package/dist/src/control/types.gen.d.ts +34 -0
  21. package/dist/src/control/types.gen.d.ts.map +1 -0
  22. package/dist/src/deep/merge.d.ts.map +1 -1
  23. package/dist/src/label/index.d.ts +1 -1
  24. package/dist/src/label/index.d.ts.map +1 -1
  25. package/dist/src/label/types.gen.d.ts +51 -0
  26. package/dist/src/label/types.gen.d.ts.map +1 -0
  27. package/dist/src/math/constants.d.ts +1 -0
  28. package/dist/src/math/constants.d.ts.map +1 -1
  29. package/dist/src/record/record.d.ts +12 -12
  30. package/dist/src/record/record.d.ts.map +1 -1
  31. package/dist/src/spatial/base.d.ts +40 -43
  32. package/dist/src/spatial/base.d.ts.map +1 -1
  33. package/dist/src/spatial/bounds/bounds.d.ts +2 -2
  34. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  35. package/dist/src/spatial/box/box.d.ts +4 -16
  36. package/dist/src/spatial/box/box.d.ts.map +1 -1
  37. package/dist/src/spatial/dimensions/dimensions.d.ts +3 -7
  38. package/dist/src/spatial/dimensions/dimensions.d.ts.map +1 -1
  39. package/dist/src/spatial/direction/direction.d.ts +3 -3
  40. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  41. package/dist/src/spatial/external.d.ts +1 -0
  42. package/dist/src/spatial/external.d.ts.map +1 -1
  43. package/dist/src/spatial/location/location.d.ts +9 -3
  44. package/dist/src/spatial/location/location.d.ts.map +1 -1
  45. package/dist/src/spatial/spatial.d.ts +2 -1
  46. package/dist/src/spatial/spatial.d.ts.map +1 -1
  47. package/dist/src/spatial/types.gen.d.ts +20 -0
  48. package/dist/src/spatial/types.gen.d.ts.map +1 -0
  49. package/dist/src/spatial/xy/xy.d.ts +3 -2
  50. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  51. package/dist/src/status/external.d.ts +3 -0
  52. package/dist/src/status/external.d.ts.map +1 -0
  53. package/dist/src/status/index.d.ts +1 -1
  54. package/dist/src/status/index.d.ts.map +1 -1
  55. package/dist/src/status/status.d.ts +5 -35
  56. package/dist/src/status/status.d.ts.map +1 -1
  57. package/dist/src/status/types.gen.d.ts +74 -0
  58. package/dist/src/status/types.gen.d.ts.map +1 -0
  59. package/dist/src/telem/external.d.ts +4 -0
  60. package/dist/src/telem/external.d.ts.map +1 -0
  61. package/dist/src/telem/index.d.ts +2 -3
  62. package/dist/src/telem/index.d.ts.map +1 -1
  63. package/dist/src/telem/telem.d.ts +62 -0
  64. package/dist/src/telem/telem.d.ts.map +1 -1
  65. package/dist/src/zod/nullToUndefined.d.ts +1 -1
  66. package/dist/src/zod/nullToUndefined.d.ts.map +1 -1
  67. package/dist/src/zod/schemas.d.ts +6 -0
  68. package/dist/src/zod/schemas.d.ts.map +1 -1
  69. package/dist/x.cjs +8 -13
  70. package/dist/x.js +4393 -5591
  71. package/package.json +8 -8
  72. package/src/array/nullable.ts +10 -1
  73. package/src/binary/codec.spec.ts +31 -0
  74. package/src/binary/codec.ts +9 -8
  75. package/src/caseconv/caseconv.bench.ts +270 -0
  76. package/src/caseconv/caseconv.spec.ts +534 -0
  77. package/src/caseconv/caseconv.ts +186 -41
  78. package/src/color/color.spec.ts +51 -36
  79. package/src/color/color.ts +7 -8
  80. package/src/control/control.ts +7 -32
  81. package/src/{label/label.ts → control/external.ts} +2 -13
  82. package/src/control/index.ts +1 -1
  83. package/src/control/types.gen.ts +52 -0
  84. package/src/deep/merge.ts +2 -1
  85. package/src/deep/path.ts +1 -1
  86. package/src/deep/remove.ts +1 -1
  87. package/src/label/index.ts +1 -1
  88. package/src/label/types.gen.ts +35 -0
  89. package/src/math/constants.ts +1 -0
  90. package/src/migrate/migrate.ts +2 -2
  91. package/src/record/record.spec.ts +31 -7
  92. package/src/record/record.ts +17 -17
  93. package/src/spatial/base.ts +63 -39
  94. package/src/spatial/bounds/bounds.ts +2 -2
  95. package/src/spatial/box/box.ts +2 -2
  96. package/src/spatial/dimensions/dimensions.ts +11 -5
  97. package/src/spatial/direction/direction.ts +2 -2
  98. package/src/spatial/external.ts +1 -0
  99. package/src/spatial/location/location.ts +15 -13
  100. package/src/spatial/spatial.ts +29 -2
  101. package/src/spatial/sticky/sticky.ts +1 -1
  102. package/src/spatial/types.gen.ts +28 -0
  103. package/src/spatial/xy/xy.ts +9 -10
  104. package/src/status/external.ts +11 -0
  105. package/src/status/index.ts +1 -1
  106. package/src/status/status.ts +11 -59
  107. package/src/status/types.gen.ts +118 -0
  108. package/src/strings/deduplicateFileName.ts +3 -3
  109. package/src/telem/external.ts +12 -0
  110. package/src/telem/index.ts +2 -3
  111. package/src/telem/telem.ts +30 -5
  112. package/src/zod/nullToUndefined.ts +4 -4
  113. package/src/zod/schemas.ts +9 -2
  114. package/src/zod/util.ts +2 -2
  115. package/tsconfig.json +3 -2
  116. package/tsconfig.tsbuildinfo +1 -1
  117. package/vite.config.ts +8 -1
  118. package/dist/src/label/label.d.ts +0 -25
  119. package/dist/src/label/label.d.ts.map +0 -1
@@ -7,56 +7,212 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
+ import { type z } from "zod";
11
+
10
12
  import { type record } from "@/record";
11
13
 
14
+ /**
15
+ * Global symbol used to mark Zod schemas that should not have their keys converted.
16
+ * Uses Symbol.for() to ensure the same symbol is used across different module instances.
17
+ */
18
+ const PRESERVE_CASE_SYMBOL = "synnax.caseconv.preserveCase";
19
+
20
+ interface ZodDef extends z.core.$ZodTypeDef {
21
+ innerType?: z.core.SomeType;
22
+ options?: readonly z.core.SomeType[];
23
+ in?: z.core.SomeType;
24
+ out?: z.core.SomeType;
25
+ element?: z.core.SomeType;
26
+ shape?: Record<string, z.ZodType>;
27
+ }
28
+
29
+ interface ZodInternals extends z.core.$ZodTypeInternals {
30
+ def: ZodDef;
31
+ }
32
+
33
+ interface ZodSchema extends z.core.$ZodType {
34
+ [PRESERVE_CASE_SYMBOL]?: boolean;
35
+ _zod: ZodInternals;
36
+ shape?: Record<string, z.ZodType>;
37
+ sourceType?: () => { shape?: Record<string, z.ZodType> } | undefined;
38
+ }
39
+
40
+ /**
41
+ * Marks a Zod schema to prevent case conversion of its keys and nested content.
42
+ * Use this for schemas where keys are semantic values (like OPC UA NodeIds or Modbus channel keys)
43
+ * rather than property names.
44
+ *
45
+ * @param schema - The Zod schema to mark
46
+ * @returns The same schema with a preserve case marker
47
+ *
48
+ * @example
49
+ * const propertiesZ = z.object({
50
+ * read: z.object({
51
+ * channels: preserveCase(z.record(z.string(), z.number()))
52
+ * })
53
+ * });
54
+ */
55
+ export const preserveCase = <T extends z.ZodType>(schema: T): T => {
56
+ (schema as ZodSchema)[PRESERVE_CASE_SYMBOL] = true;
57
+ return schema;
58
+ };
59
+
60
+ /**
61
+ * Checks if a Zod schema has the preserve case marker.
62
+ * Traverses through wrapper schemas (optional, nullable, union, transform, etc.)
63
+ * to find markers on inner schemas.
64
+ */
65
+ const hasPreserveCaseMarker = (schema: unknown): boolean => {
66
+ if (schema == null || typeof schema !== "object") return false;
67
+
68
+ // Direct marker check
69
+ if (PRESERVE_CASE_SYMBOL in schema) return true;
70
+
71
+ const s = schema as ZodSchema;
72
+ const def: ZodDef | undefined = s._zod?.def;
73
+ if (def == null) return false;
74
+
75
+ // Traverse through wrappers with innerType (optional, nullable, default, catch)
76
+ if (def.innerType && hasPreserveCaseMarker(def.innerType)) return true;
77
+
78
+ // Traverse through unions - check all options
79
+ if (def.type === "union" && Array.isArray(def.options))
80
+ return def.options.some(hasPreserveCaseMarker);
81
+
82
+ // Traverse through pipes/transforms - check both ends
83
+ if (def.type === "pipe")
84
+ return hasPreserveCaseMarker(def.in) || hasPreserveCaseMarker(def.out);
85
+
86
+ return false;
87
+ };
88
+
89
+ /**
90
+ * Unwraps an array schema to get its element schema.
91
+ * Handles direct arrays and unions containing arrays (e.g., from nullishToEmpty).
92
+ * Returns undefined if the schema is not an array or is undefined.
93
+ */
94
+ const getArrayElementSchema = (
95
+ schema: z.ZodType | z.core.SomeType | undefined,
96
+ ): z.ZodType | undefined => {
97
+ if (schema == null) return undefined;
98
+ const def = (schema as ZodSchema)._zod?.def;
99
+ if (def?.type === "array" && def.element != null) return def.element as z.ZodType;
100
+ // Handle union types that may contain arrays (e.g., nullishToEmpty)
101
+ if (def?.type === "union" && Array.isArray(def.options))
102
+ for (const option of def.options) {
103
+ const result = getArrayElementSchema(option);
104
+ if (result != null) return result;
105
+ }
106
+
107
+ return undefined;
108
+ };
109
+
110
+ /**
111
+ * Extracts the shape (field name → ZodType map) from a Zod schema.
112
+ * Traverses through wrappers (optional, nullable, default, catch, union, pipe)
113
+ * to find the inner object schema's shape. Returns null for non-object schemas.
114
+ */
115
+ const getSchemaShape = (
116
+ schema: z.ZodType | z.core.SomeType | undefined,
117
+ ): Record<string, z.ZodType> | null => {
118
+ if (schema == null) return null;
119
+ const s = schema as ZodSchema;
120
+ if (s.shape != null) return s.shape;
121
+ if (typeof s.sourceType === "function") {
122
+ const st = s.sourceType();
123
+ if (st?.shape != null) return st.shape;
124
+ }
125
+ const def = s._zod?.def;
126
+ if (def == null) return null;
127
+ if (def.innerType != null) return getSchemaShape(def.innerType);
128
+ if (def.type === "union" && Array.isArray(def.options))
129
+ for (const option of def.options) {
130
+ const result = getSchemaShape(option);
131
+ if (result != null) return result;
132
+ }
133
+
134
+ if (def.type === "pipe") return getSchemaShape(def.in) ?? getSchemaShape(def.out);
135
+ return null;
136
+ };
137
+
12
138
  const snakeToCamelStr = (str: string): string => {
13
- const c = str.replace(/_[a-z]/g, (m) => m[1].toUpperCase());
14
- // if both first and second characters are upper case, leave as is
15
- // if only first character is upper case, convert to lower case
16
- if (c.length > 1 && c[0] === c[0].toUpperCase() && c[1] === c[1].toUpperCase())
17
- return c;
18
- if (c.length === 0) return c;
19
- return c[0].toLowerCase() + c.slice(1);
139
+ if (str.length === 0) return str;
140
+ const hasUnderscore = str.indexOf("_") !== -1;
141
+ const c = hasUnderscore ? str.replace(/_[a-z]/g, (m) => m[1].toUpperCase()) : str;
142
+ const first = c.charCodeAt(0);
143
+ if (first < 65 || first > 90) return c; // not uppercase A-Z
144
+ if (c.length > 1 && c.charCodeAt(1) >= 65 && c.charCodeAt(1) <= 90) return c;
145
+ return String.fromCharCode(first + 32) + c.slice(1);
20
146
  };
147
+
21
148
  /**
22
149
  * Convert string keys in an object to snake_case format.
23
150
  * @param obj: object to convert keys. If `obj` isn't a json object, `null` is returned.
24
151
  * @param opt: (optional) Options parameter, default is non-recursive.
152
+ * @param schema: (optional) Zod schema to check for preserve case markers
25
153
  */
26
154
  const createConverter = (
27
155
  f: (v: string) => string,
28
156
  ): (<V>(obj: V, opt?: Options) => V) => {
29
157
  const converter = <V>(obj: V, opt: Options = defaultOptions): V => {
30
- if (typeof obj === "string") return f(obj) as any;
31
- if (Array.isArray(obj)) return obj.map((v) => converter(v, opt)) as V;
158
+ if (typeof obj === "string") return f(obj) as V;
159
+ if (Array.isArray(obj)) {
160
+ const elementSchema = getArrayElementSchema(opt.schema);
161
+ const elemOpt: Options = {
162
+ recursive: opt.recursive,
163
+ recursiveInArray: opt.recursiveInArray,
164
+ schema: elementSchema,
165
+ };
166
+ return obj.map((v) => converter(v, elemOpt)) as V;
167
+ }
32
168
  if (!isValidObject(obj)) return obj;
33
- opt = validateOptions(opt);
169
+
170
+ if (opt.schema != null && hasPreserveCaseMarker(opt.schema)) return obj;
171
+
172
+ const recursive = opt.recursive ?? true;
173
+ const recursiveInArray = opt.recursiveInArray ?? recursive;
174
+ const schema = opt.schema;
34
175
  const res: record.Unknown = {};
35
176
  const anyObj = obj as record.Unknown;
36
177
  if ("toJSON" in anyObj && typeof anyObj.toJSON === "function")
37
178
  return converter(anyObj.toJSON(), opt);
38
- Object.keys(anyObj).forEach((key) => {
179
+
180
+ const shape = getSchemaShape(schema);
181
+ const childOpt: Options = { recursive, recursiveInArray, schema: undefined };
182
+ const keys = Object.keys(anyObj);
183
+ for (let i = 0; i < keys.length; i++) {
184
+ const key = keys[i];
39
185
  let value = anyObj[key];
40
186
  const nkey = f(key);
41
- if (opt.recursive)
187
+
188
+ // Look up schema using BOTH original key and converted key since:
189
+ // - For snakeToCamel: schema has camelCase keys, input has snake_case, nkey is camelCase (matches)
190
+ // - For camelToSnake: schema has camelCase keys, input has camelCase, nkey is snake_case (key matches)
191
+ const propSchema: z.ZodType | undefined =
192
+ shape != null ? (shape[key] ?? shape[nkey] ?? undefined) : undefined;
193
+
194
+ if (recursive)
42
195
  if (isValidObject(value)) {
43
- if (!belongToTypes(value)) value = converter(value, opt);
44
- } else if (opt.recursiveInArray && isArrayObject(value))
45
- value = [...(value as unknown[])].map((v) => {
46
- let ret = v;
196
+ if (!isPreservedType(value)) {
197
+ childOpt.schema = propSchema;
198
+ value = converter(value, childOpt);
199
+ }
200
+ } else if (recursiveInArray && Array.isArray(value)) {
201
+ const elementSchema = getArrayElementSchema(propSchema);
202
+ childOpt.schema = elementSchema;
203
+ value = (value as unknown[]).map((v) => {
47
204
  if (isValidObject(v)) {
48
- // object in array
49
- if (!belongToTypes(ret)) ret = converter(v, opt);
50
- } else if (isArrayObject(v)) {
51
- // array in array
52
- // workaround by using an object holding array value
53
- const temp: record.Unknown = converter({ key: v }, opt);
54
- ret = temp.key;
205
+ if (!isPreservedType(v)) return converter(v, childOpt);
206
+ } else if (Array.isArray(v)) {
207
+ const temp: record.Unknown = converter({ key: v }, childOpt);
208
+ return temp.key;
55
209
  }
56
- return ret;
210
+ return v;
57
211
  });
212
+ }
213
+
58
214
  res[nkey] = value;
59
- });
215
+ }
60
216
 
61
217
  return res as V;
62
218
  };
@@ -113,33 +269,22 @@ export const capitalize = (str: string): string => {
113
269
  * Example Date, RegExp. These types will be right-hand side of 'instanceof' operator.
114
270
  */
115
271
  export interface Options {
116
- recursive: boolean;
272
+ recursive?: boolean;
117
273
  recursiveInArray?: boolean;
274
+ schema?: z.ZodType;
118
275
  }
119
276
 
120
- const keepTypesOnRecursion = [Number, String, Uint8Array];
121
-
122
- /**
123
- * Default options for convert function. This option is not recursive.
124
- */
125
277
  const defaultOptions: Options = {
126
278
  recursive: true,
127
279
  recursiveInArray: true,
280
+ schema: undefined,
128
281
  };
129
282
 
130
- const validateOptions = (opt: Options = defaultOptions): Options => {
131
- if (opt.recursive == null) opt = defaultOptions;
132
- else opt.recursiveInArray ??= false;
133
- return opt;
134
- };
135
-
136
- const isArrayObject = (obj: unknown): boolean => obj != null && Array.isArray(obj);
137
-
138
283
  const isValidObject = (obj: unknown): boolean =>
139
284
  obj != null && typeof obj === "object" && !Array.isArray(obj);
140
285
 
141
- const belongToTypes = (obj: unknown): boolean =>
142
- keepTypesOnRecursion.some((type) => obj instanceof type);
286
+ const isPreservedType = (obj: unknown): boolean =>
287
+ obj instanceof Uint8Array || obj instanceof Number || obj instanceof String;
143
288
 
144
289
  /**
145
290
  * Converts a string to kebab-case.
@@ -24,7 +24,7 @@ describe("color.Color", () => {
24
24
  expect(color.rValue(c)).toEqual(122);
25
25
  expect(color.gValue(c)).toEqual(44);
26
26
  expect(color.bValue(c)).toEqual(38);
27
- expect(color.aValue(c)).toEqual(0.5);
27
+ expect(color.aValue(c)).toBeCloseTo(0.5);
28
28
  });
29
29
 
30
30
  describe("from eight digit hex", () => {
@@ -33,14 +33,14 @@ describe("color.Color", () => {
33
33
  expect(color.rValue(c)).toEqual(122);
34
34
  expect(color.gValue(c)).toEqual(44);
35
35
  expect(color.bValue(c)).toEqual(38);
36
- expect(color.aValue(c)).toEqual(1);
36
+ expect(color.aValue(c)).toBeCloseTo(1);
37
37
  });
38
38
  test("case 2", () => {
39
39
  const c = color.construct("#7a2c2605");
40
40
  expect(color.rValue(c)).toEqual(122);
41
41
  expect(color.gValue(c)).toEqual(44);
42
42
  expect(color.bValue(c)).toEqual(38);
43
- expect(color.aValue(c)).toEqual(5 / 255);
43
+ expect(color.aValue(c)).toBeCloseTo(5 / 255);
44
44
  });
45
45
  });
46
46
 
@@ -55,7 +55,7 @@ describe("color.Color", () => {
55
55
  expect(color.rValue(c)).toEqual(122);
56
56
  expect(color.gValue(c)).toEqual(44);
57
57
  expect(color.bValue(c)).toEqual(38);
58
- expect(color.aValue(c)).toEqual(0.5);
58
+ expect(color.aValue(c)).toBeCloseTo(0.5);
59
59
  });
60
60
  test("from c", () => {
61
61
  const c = color.construct(color.construct("#7a2c26"));
@@ -69,7 +69,7 @@ describe("color.Color", () => {
69
69
  expect(color.rValue(c)).toEqual(122);
70
70
  expect(color.gValue(c)).toEqual(44);
71
71
  expect(color.bValue(c)).toEqual(38);
72
- expect(color.aValue(c)).toEqual(0.5);
72
+ expect(color.aValue(c)).toBeCloseTo(0.5);
73
73
  });
74
74
 
75
75
  test("from rgba struct", () => {
@@ -105,16 +105,20 @@ describe("color.Color", () => {
105
105
  });
106
106
  });
107
107
 
108
- describe("to RGBA255", () => {
108
+ describe("to RGBA", () => {
109
109
  test("with alpha", () => {
110
110
  const c = color.construct("#7a2c26", 0.5);
111
- const expected = [122, 44, 38, 0.5];
112
- expect(color.construct(c)).toEqual(expected);
111
+ expect(c[0]).toEqual(122);
112
+ expect(c[1]).toEqual(44);
113
+ expect(c[2]).toEqual(38);
114
+ expect(c[3]).toBeCloseTo(0.5);
113
115
  });
114
116
  test("without alpha", () => {
115
117
  const c = color.construct("#7a2c26");
116
- const expected = [122, 44, 38, 1];
117
- expect(color.construct(c)).toEqual(expected);
118
+ expect(c[0]).toEqual(122);
119
+ expect(c[1]).toEqual(44);
120
+ expect(c[2]).toEqual(38);
121
+ expect(c[3]).toBeCloseTo(1);
118
122
  });
119
123
  });
120
124
 
@@ -319,7 +323,7 @@ describe("color.Color", () => {
319
323
  const semiTransparent: color.HSLA = [0, 100, 50, 0.5]; // Semi-transparent red
320
324
 
321
325
  expect(color.fromHSLA(transparent)[3]).toEqual(0);
322
- expect(color.fromHSLA(semiTransparent)[3]).toEqual(0.5);
326
+ expect(color.fromHSLA(semiTransparent)[3]).toBeCloseTo(0.5);
323
327
  });
324
328
  });
325
329
 
@@ -362,8 +366,8 @@ describe("color.Color", () => {
362
366
  // we use toBeCloseTo for HSL values with precision 0
363
367
  for (let i = 0; i < 3; i++) expect(result[i]).toBeCloseTo(expected[i], 0);
364
368
 
365
- // Alpha should match exactly
366
- expect(result[3]).toEqual(expected[3]);
369
+ // Alpha should match
370
+ expect(result[3]).toBeCloseTo(expected[3]);
367
371
  });
368
372
  });
369
373
 
@@ -375,7 +379,7 @@ describe("color.Color", () => {
375
379
 
376
380
  for (let i = 0; i < 3; i++) expect(result[i]).toBeCloseTo(expected[i], 0);
377
381
 
378
- expect(result[3]).toEqual(expected[3]);
382
+ expect(result[3]).toBeCloseTo(expected[3]);
379
383
  });
380
384
 
381
385
  test("handles RGB array input", () => {
@@ -386,7 +390,7 @@ describe("color.Color", () => {
386
390
 
387
391
  for (let i = 0; i < 3; i++) expect(result[i]).toBeCloseTo(expected[i], 0);
388
392
 
389
- expect(result[3]).toEqual(1); // Default alpha
393
+ expect(result[3]).toBeCloseTo(1); // Default alpha
390
394
  });
391
395
 
392
396
  test("preserves original color after round-trip conversion", () => {
@@ -408,8 +412,8 @@ describe("color.Color", () => {
408
412
  // Compare RGB values with some tolerance for rounding
409
413
  for (let i = 0; i < 3; i++) expect(converted[i]).toBeCloseTo(original[i], 0);
410
414
 
411
- // Alpha should match exactly
412
- expect(converted[3]).toEqual(original[3]);
415
+ // Alpha should match
416
+ expect(converted[3]).toBeCloseTo(original[3]);
413
417
  }
414
418
  });
415
419
 
@@ -422,7 +426,7 @@ describe("color.Color", () => {
422
426
  expect(result[0]).toEqual(0); // Hue
423
427
  expect(result[1]).toEqual(0); // Saturation
424
428
  expect(result[2]).toBeCloseTo(39, 0); // Lightness ~39%
425
- expect(result[3]).toEqual(1); // Alpha
429
+ expect(result[3]).toBeCloseTo(1); // Alpha
426
430
  });
427
431
  });
428
432
 
@@ -431,28 +435,40 @@ describe("color.Color", () => {
431
435
  const rgb: color.RGB = [255, 0, 0];
432
436
  const result = color.setAlpha(rgb, 0.5);
433
437
 
434
- expect(result).toEqual([255, 0, 0, 0.5]);
438
+ expect(result[0]).toEqual(255);
439
+ expect(result[1]).toEqual(0);
440
+ expect(result[2]).toEqual(0);
441
+ expect(result[3]).toBeCloseTo(0.5);
435
442
  });
436
443
 
437
444
  test("sets alpha on RGBA color", () => {
438
445
  const rgba: color.RGBA = [0, 255, 0, 1];
439
446
  const result = color.setAlpha(rgba, 0.3);
440
447
 
441
- expect(result).toEqual([0, 255, 0, 0.3]);
448
+ expect(result[0]).toEqual(0);
449
+ expect(result[1]).toEqual(255);
450
+ expect(result[2]).toEqual(0);
451
+ expect(result[3]).toBeCloseTo(0.3);
442
452
  });
443
453
 
444
454
  test("sets alpha on hex color", () => {
445
455
  const hex = "#0000ff";
446
456
  const result = color.setAlpha(hex, 0.7);
447
457
 
448
- expect(result).toEqual([0, 0, 255, 0.7]);
458
+ expect(result[0]).toEqual(0);
459
+ expect(result[1]).toEqual(0);
460
+ expect(result[2]).toEqual(255);
461
+ expect(result[3]).toBeCloseTo(0.7);
449
462
  });
450
463
 
451
464
  test("overrides existing alpha in RGBA color", () => {
452
465
  const rgba: color.RGBA = [128, 128, 128, 0.2];
453
466
  const result = color.setAlpha(rgba, 0.8);
454
467
 
455
- expect(result).toEqual([128, 128, 128, 0.8]);
468
+ expect(result[0]).toEqual(128);
469
+ expect(result[1]).toEqual(128);
470
+ expect(result[2]).toEqual(128);
471
+ expect(result[3]).toBeCloseTo(0.8);
456
472
  });
457
473
 
458
474
  test("handles alpha value of 0", () => {
@@ -466,19 +482,14 @@ describe("color.Color", () => {
466
482
  const color1 = color.construct("#ff0000", 0.5);
467
483
  const result = color.setAlpha(color1, 1);
468
484
 
469
- expect(result[3]).toEqual(1);
485
+ expect(result[3]).toBeCloseTo(1);
470
486
  });
471
487
 
472
- test("converts percentage (>1) alpha values to 0-1 range", () => {
488
+ test("accepts alpha values in 0-1 range", () => {
473
489
  const color1 = color.construct("#ff0000");
474
- const result = color.setAlpha(color1, 50);
490
+ const result = color.setAlpha(color1, 0.5);
475
491
 
476
- expect(result[3]).toEqual(0.5);
477
- });
478
-
479
- test("throws error for alpha values > 100", () => {
480
- const color1 = color.construct("#ff0000");
481
- expect(() => color.setAlpha(color1, 101)).toThrow();
492
+ expect(result[3]).toBeCloseTo(0.5);
482
493
  });
483
494
 
484
495
  test("preserves RGB values when setting alpha", () => {
@@ -518,7 +529,9 @@ describe("color.Color", () => {
518
529
 
519
530
  test("converts hex with alpha to CSS rgba string", () => {
520
531
  const hex = "#ff000080";
521
- expect(color.rgbaCSS(hex)).toEqual("rgba(255, 0, 0, 0.5019607843137255)");
532
+ const result = color.rgbaCSS(hex);
533
+ // Alpha 0x80 = 128/255 ≈ 0.502
534
+ expect(result).toMatch(/rgba\(255, 0, 0, 0\.5\d*\)/);
522
535
  });
523
536
 
524
537
  test("handles Color object", () => {
@@ -604,7 +617,7 @@ describe("color.Color", () => {
604
617
 
605
618
  test("eight-digit hex and RGBA with same values are equal", () => {
606
619
  const c1 = "#ff000080";
607
- const c2: color.RGBA = [255, 0, 0, 0.5019607843137255];
620
+ const c2: color.RGBA = [255, 0, 0, 128 / 255];
608
621
  expect(color.equals(c1, c2)).toBe(true);
609
622
  });
610
623
 
@@ -652,7 +665,9 @@ describe("color.Color", () => {
652
665
 
653
666
  test("handles eight-digit hex with alpha", () => {
654
667
  const hex = "#00ff0080";
655
- expect(color.cssString(hex)).toEqual("rgba(0, 255, 0, 0.5019607843137255)");
668
+ const result = color.cssString(hex);
669
+ // Alpha 0x80 = 128/255 ≈ 0.502
670
+ expect(result).toMatch(/rgba\(0, 255, 0, 0\.5\d*\)/);
656
671
  });
657
672
 
658
673
  test("handles Color object", () => {
@@ -711,7 +726,7 @@ describe("color.Color", () => {
711
726
  });
712
727
 
713
728
  test("rejects invalid RGBA arrays", () => {
714
- expect(color.isCrude([255, 0, 0, 1.1])).toBe(false);
729
+ expect(color.isCrude([255, 0, 0, 1.5])).toBe(false);
715
730
  expect(color.isCrude([255, 0, 0, -0.1])).toBe(false);
716
731
  });
717
732
 
@@ -754,7 +769,7 @@ describe("color.Color", () => {
754
769
  expect(color.isColor([255, 0, 0, 0, 0])).toBe(false); // Too many elements
755
770
  expect(color.isColor([255, 0, -1, 1])).toBe(false); // Negative value
756
771
  expect(color.isColor([255, 0, 256, 1])).toBe(false); // Value > 255
757
- expect(color.isColor([255, 0, 0, 1.1])).toBe(false); // Alpha > 1
772
+ expect(color.isColor([255, 0, 0, 1.5])).toBe(false); // Alpha > 1
758
773
  expect(color.isColor([255, 0, 0, -0.1])).toBe(false); // Alpha < 0
759
774
  });
760
775
 
@@ -9,25 +9,24 @@
9
9
 
10
10
  import { z } from "zod";
11
11
 
12
+ import { zod } from "@/zod";
13
+
12
14
  /** A regex to match hex colors. */
13
15
  const hexRegex = /^#?([0-9a-f]{6}|[0-9a-f]{8})$/i;
14
16
 
15
17
  /** A zod schema for a hex color. */
16
18
  const hexZ = z.string().regex(hexRegex);
17
19
  /** A zod schema for an RGB value. */
18
- const rgbValueZ = z.number().min(0).max(255);
20
+ const rgbValueZ = zod.uint8;
19
21
  /** A zod schema for an alpha value between 0 and 1. */
20
22
  const alphaZ = z.number().min(0).max(1);
21
23
  /** A zod schema for an RGBA color. */
22
24
  const rgbaZ = z.tuple([rgbValueZ, rgbValueZ, rgbValueZ, alphaZ]);
23
25
  /** A zod schema for an RGB color. */
24
26
  const rgbZ = z.tuple([rgbValueZ, rgbValueZ, rgbValueZ]);
25
- /** A zod schema for a legacy color object. */
26
27
  const legacyObjectZ = z.object({ rgba255: rgbaZ });
27
28
  /** A zod schema for an RGBA struct (r, g, b, a fields). */
28
29
  const rgbaStructZ = z.object({ r: rgbValueZ, g: rgbValueZ, b: rgbValueZ, a: alphaZ });
29
- /** An RGBA struct with named fields. */
30
- export type RGBAStruct = z.infer<typeof rgbaStructZ>;
31
30
  /** A zod schema for a hue value between 0 and 360. */
32
31
  const hueZ = z.number().min(0).max(360);
33
32
  /** A zod schema for a saturation value between 0 and 100. */
@@ -48,7 +47,8 @@ export type Hex = z.infer<typeof hexZ>;
48
47
 
49
48
  /** A legacy color object. Used for backwards compatibility. */
50
49
  type LegacyObject = z.infer<typeof legacyObjectZ>;
51
-
50
+ /** A color in RGBA format as a struct. */
51
+ type RGBAStruct = z.infer<typeof rgbaStructZ>;
52
52
  /** A zod schema for a crude color representation. */
53
53
  export const crudeZ = z.union([hexZ, rgbZ, rgbaZ, hslaZ, legacyObjectZ, rgbaStructZ]);
54
54
  /**
@@ -203,8 +203,7 @@ export const hsla = (color: Crude): HSLA => rgbaToHSLA(construct(color));
203
203
  /**
204
204
  * @returns A new color with the given alpha.
205
205
  * @param color - The color to set the alpha value on.
206
- * @param alpha - The alpha value to set. If the value is greater than 1, it will be
207
- * divided by 100.
206
+ * @param alpha - The alpha value to set (0-1).
208
207
  */
209
208
  export const setAlpha = (color: Crude, alpha: number): Color => {
210
209
  const [r, g, b] = construct(color);
@@ -353,7 +352,7 @@ export const fromCSS = (cssColor: string): Color | undefined => {
353
352
  );
354
353
  if (match) {
355
354
  const [, r, g, b, a] = match;
356
- return [parseInt(r), parseInt(g), parseInt(b), a ? parseFloat(a) : 1];
355
+ return [parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), a ? parseFloat(a) : 1];
357
356
  }
358
357
  }
359
358
  if (NAMED[trimmed]) return fromHex(NAMED[trimmed]);
@@ -9,11 +9,9 @@
9
9
 
10
10
  import { z } from "zod";
11
11
 
12
+ import { type Authority, type State, stateZ } from "@/control/types.gen";
12
13
  import { type bounds } from "@/spatial";
13
14
 
14
- export const authorityZ = z.int().min(0).max(255);
15
- export type Authority = z.infer<typeof authorityZ>;
16
-
17
15
  export const ABSOLUTE_AUTHORITY: Authority = 255;
18
16
  export const ZERO_AUTHORITY: Authority = 0;
19
17
 
@@ -22,31 +20,8 @@ export const AUTHORITY_BOUNDS: bounds.Bounds<Authority> = {
22
20
  upper: ABSOLUTE_AUTHORITY + 1,
23
21
  };
24
22
 
25
- export const subjectZ = z.object({
26
- name: z.string(),
27
- key: z.string(),
28
- });
29
-
30
- export interface Subject {
31
- name: string;
32
- key: string;
33
- }
34
-
35
- export const stateZ = <R extends z.ZodType>(resource: R) =>
36
- z.object({
37
- subject: subjectZ,
38
- resource,
39
- authority: authorityZ,
40
- });
41
-
42
- export interface State<R> {
43
- subject: Subject;
44
- resource: R;
45
- authority: Authority;
46
- }
47
-
48
23
  export const filterTransfersByChannelKey =
49
- <R>(...resources: R[]) =>
24
+ <R extends z.ZodType>(...resources: z.infer<R>[]) =>
50
25
  (transfers: Transfer<R>[]): Transfer<R>[] =>
51
26
  transfers.filter((t) => {
52
27
  let ok = false;
@@ -55,7 +30,7 @@ export const filterTransfersByChannelKey =
55
30
  return ok;
56
31
  });
57
32
 
58
- interface Release<R> {
33
+ interface Release<R extends z.ZodType> {
59
34
  from: State<R>;
60
35
  to?: null;
61
36
  }
@@ -63,21 +38,21 @@ interface Release<R> {
63
38
  export const releaseZ = <R extends z.ZodType>(resource: R) =>
64
39
  z.object({
65
40
  from: stateZ(resource),
66
- to: z.null(),
41
+ to: z.null().optional(),
67
42
  });
68
43
 
69
- interface Acquire<R> {
44
+ interface Acquire<R extends z.ZodType> {
70
45
  from?: null;
71
46
  to: State<R>;
72
47
  }
73
48
 
74
49
  export const acquireZ = <R extends z.ZodType>(resource: R) =>
75
50
  z.object({
76
- from: z.null(),
51
+ from: z.null().optional(),
77
52
  to: stateZ(resource),
78
53
  });
79
54
 
80
- export type Transfer<R> =
55
+ export type Transfer<R extends z.ZodType> =
81
56
  | {
82
57
  from: State<R>;
83
58
  to: State<R>;
@@ -7,16 +7,5 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import z from "zod";
11
-
12
- import { color } from "@/color";
13
-
14
- export const keyZ = z.uuid();
15
- export type Key = z.infer<typeof keyZ>;
16
-
17
- export const labelZ = z.object({
18
- key: keyZ,
19
- name: z.string().min(1),
20
- color: color.colorZ,
21
- });
22
- export interface Label extends z.infer<typeof labelZ> {}
10
+ export * from "@/control/control";
11
+ export * from "@/control/types.gen";
@@ -7,4 +7,4 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- export * as control from "@/control/control";
10
+ export * as control from "@/control/external";