@synnaxlabs/x 0.48.0 → 0.49.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 (247) hide show
  1. package/.turbo/turbo-build.log +6 -104
  2. package/dist/src/csv/csv.d.ts +5 -7
  3. package/dist/src/csv/csv.d.ts.map +1 -1
  4. package/dist/src/deep/join.d.ts.map +1 -0
  5. package/dist/src/deep/path.d.ts +1 -1
  6. package/dist/src/deep/path.d.ts.map +1 -1
  7. package/dist/src/destructor/destructor.d.ts +7 -0
  8. package/dist/src/destructor/destructor.d.ts.map +1 -0
  9. package/dist/src/destructor/index.d.ts +2 -0
  10. package/dist/src/destructor/index.d.ts.map +1 -0
  11. package/dist/src/index.d.ts +5 -10
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/math/constants.d.ts +1 -0
  14. package/dist/src/math/constants.d.ts.map +1 -1
  15. package/dist/src/migrate/migrate.d.ts +3 -3
  16. package/dist/src/migrate/migrate.d.ts.map +1 -1
  17. package/dist/src/narrow/index.d.ts +2 -0
  18. package/dist/src/narrow/index.d.ts.map +1 -0
  19. package/dist/src/narrow/narrow.d.ts +4 -0
  20. package/dist/src/narrow/narrow.d.ts.map +1 -0
  21. package/dist/src/narrow/narrow.spec.d.ts +2 -0
  22. package/dist/src/narrow/narrow.spec.d.ts.map +1 -0
  23. package/dist/src/numeric/numeric.d.ts +0 -1
  24. package/dist/src/numeric/numeric.d.ts.map +1 -1
  25. package/dist/src/observe/observe.d.ts +4 -4
  26. package/dist/src/observe/observe.d.ts.map +1 -1
  27. package/dist/src/optional/index.d.ts +2 -0
  28. package/dist/src/optional/index.d.ts.map +1 -0
  29. package/dist/src/optional/optional.d.ts.map +1 -0
  30. package/dist/src/scheduler/index.d.ts +2 -0
  31. package/dist/src/scheduler/index.d.ts.map +1 -0
  32. package/dist/src/{flush.d.ts → scheduler/scheduler.d.ts} +1 -1
  33. package/dist/src/scheduler/scheduler.d.ts.map +1 -0
  34. package/dist/src/scheduler/scheduler.spec.d.ts +2 -0
  35. package/dist/src/scheduler/scheduler.spec.d.ts.map +1 -0
  36. package/dist/src/shallow/copy.d.ts +2 -0
  37. package/dist/src/shallow/copy.d.ts.map +1 -0
  38. package/dist/src/shallow/copy.spec.d.ts +2 -0
  39. package/dist/src/shallow/copy.spec.d.ts.map +1 -0
  40. package/dist/src/shallow/index.d.ts +2 -0
  41. package/dist/src/shallow/index.d.ts.map +1 -0
  42. package/dist/src/spatial/base.d.ts +5 -38
  43. package/dist/src/spatial/base.d.ts.map +1 -1
  44. package/dist/src/spatial/direction/direction.d.ts +2 -1
  45. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  46. package/dist/src/spatial/external.d.ts +2 -1
  47. package/dist/src/spatial/external.d.ts.map +1 -1
  48. package/dist/src/spatial/location/location.d.ts +2 -2
  49. package/dist/src/spatial/location/location.d.ts.map +1 -1
  50. package/dist/src/spatial/sticky/index.d.ts +2 -0
  51. package/dist/src/spatial/sticky/index.d.ts.map +1 -0
  52. package/dist/src/spatial/sticky/sticky.d.ts +74 -0
  53. package/dist/src/spatial/sticky/sticky.d.ts.map +1 -0
  54. package/dist/src/spatial/sticky/sticky.spec.d.ts +2 -0
  55. package/dist/src/spatial/sticky/sticky.spec.d.ts.map +1 -0
  56. package/dist/src/spatial/xy/xy.d.ts +10 -2
  57. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  58. package/dist/src/status/status.d.ts +2 -2
  59. package/dist/src/status/status.d.ts.map +1 -1
  60. package/dist/src/telem/series.d.ts +4 -4
  61. package/dist/src/telem/series.d.ts.map +1 -1
  62. package/dist/src/telem/telem.d.ts +0 -1
  63. package/dist/src/telem/telem.d.ts.map +1 -1
  64. package/dist/src/types/index.d.ts +2 -0
  65. package/dist/src/types/index.d.ts.map +1 -0
  66. package/dist/src/zod/external.d.ts +1 -0
  67. package/dist/src/zod/external.d.ts.map +1 -1
  68. package/dist/src/zod/schemas.d.ts +3 -0
  69. package/dist/src/zod/schemas.d.ts.map +1 -0
  70. package/dist/src/zod/schemas.spec.d.ts +2 -0
  71. package/dist/src/zod/schemas.spec.d.ts.map +1 -0
  72. package/dist/x.cjs +12 -0
  73. package/dist/x.js +5537 -0
  74. package/package.json +4 -142
  75. package/src/binary/codec.ts +2 -2
  76. package/src/csv/csv.spec.ts +59 -11
  77. package/src/csv/csv.ts +27 -11
  78. package/src/deep/merge.ts +3 -3
  79. package/src/deep/path.ts +1 -1
  80. package/src/{destructor.ts → destructor/destructor.ts} +1 -1
  81. package/src/{invert.ts → destructor/index.ts} +1 -1
  82. package/src/index.ts +5 -10
  83. package/src/math/constants.ts +1 -0
  84. package/src/migrate/migrate.ts +5 -3
  85. package/src/{mock → narrow}/index.ts +1 -1
  86. package/src/narrow/narrow.spec.ts +70 -0
  87. package/src/{identity.ts → narrow/narrow.ts} +6 -0
  88. package/src/numeric/numeric.ts +0 -5
  89. package/src/observe/observe.ts +4 -4
  90. package/src/{replace.ts → optional/index.ts} +1 -1
  91. package/src/scheduler/index.ts +10 -0
  92. package/src/scheduler/scheduler.spec.ts +82 -0
  93. package/src/shallow/copy.spec.ts +141 -0
  94. package/src/{shallowCopy.ts → shallow/copy.ts} +1 -1
  95. package/src/shallow/index.ts +10 -0
  96. package/src/spatial/base.ts +6 -15
  97. package/src/spatial/direction/direction.ts +2 -0
  98. package/src/spatial/external.ts +2 -1
  99. package/src/spatial/location/location.spec.ts +16 -0
  100. package/src/spatial/location/location.ts +7 -7
  101. package/src/spatial/sticky/index.ts +10 -0
  102. package/src/spatial/sticky/sticky.spec.ts +584 -0
  103. package/src/spatial/sticky/sticky.ts +98 -0
  104. package/src/spatial/xy/xy.spec.ts +55 -0
  105. package/src/spatial/xy/xy.ts +27 -2
  106. package/src/status/status.ts +5 -5
  107. package/src/telem/series.ts +5 -6
  108. package/src/telem/telem.spec.ts +0 -28
  109. package/src/telem/telem.ts +1 -9
  110. package/src/{clamp → types}/index.ts +1 -1
  111. package/src/zod/external.ts +1 -0
  112. package/src/zod/schemas.spec.ts +51 -0
  113. package/src/zod/schemas.ts +14 -0
  114. package/tsconfig.tsbuildinfo +1 -1
  115. package/vite.config.ts +1 -36
  116. package/dist/array.cjs +0 -1
  117. package/dist/array.js +0 -4
  118. package/dist/base-DRybODwJ.js +0 -42
  119. package/dist/base-KIBsp6TI.cjs +0 -1
  120. package/dist/binary.cjs +0 -1
  121. package/dist/binary.js +0 -4
  122. package/dist/bounds-4BWKPqaP.js +0 -183
  123. package/dist/bounds-C2TKFgVk.cjs +0 -1
  124. package/dist/bounds.cjs +0 -1
  125. package/dist/bounds.js +0 -4
  126. package/dist/box-BXWXSkKu.js +0 -203
  127. package/dist/box-rH3ggwXk.cjs +0 -1
  128. package/dist/box.cjs +0 -1
  129. package/dist/box.js +0 -4
  130. package/dist/caseconv.cjs +0 -1
  131. package/dist/caseconv.js +0 -4
  132. package/dist/change-C-YELKx6.cjs +0 -1
  133. package/dist/change-DLl6DccR.js +0 -12
  134. package/dist/change.cjs +0 -1
  135. package/dist/change.js +0 -4
  136. package/dist/compare-Bnx9CdjS.js +0 -119
  137. package/dist/compare-GPoFaKRW.cjs +0 -1
  138. package/dist/compare.cjs +0 -1
  139. package/dist/compare.js +0 -36
  140. package/dist/debounce.cjs +0 -1
  141. package/dist/debounce.js +0 -17
  142. package/dist/deep.cjs +0 -1
  143. package/dist/deep.js +0 -247
  144. package/dist/destructor.cjs +0 -1
  145. package/dist/destructor.js +0 -1
  146. package/dist/dimensions-Cg5Owbwn.cjs +0 -1
  147. package/dist/dimensions-DC0uLPwn.js +0 -43
  148. package/dist/dimensions.cjs +0 -1
  149. package/dist/dimensions.js +0 -4
  150. package/dist/direction-C_b4tfRN.js +0 -19
  151. package/dist/direction-DqQB9M37.cjs +0 -1
  152. package/dist/direction.cjs +0 -1
  153. package/dist/direction.js +0 -4
  154. package/dist/external-2YWy569j.js +0 -23
  155. package/dist/external-B6edOwoQ.cjs +0 -1
  156. package/dist/external-B80i4ymZ.js +0 -29
  157. package/dist/external-B9AAGv50.cjs +0 -1
  158. package/dist/external-BYuXBYJh.js +0 -40
  159. package/dist/external-BxmTQZ6m.cjs +0 -1
  160. package/dist/external-DLiGrXn7.cjs +0 -1
  161. package/dist/external-Du5qzfYv.js +0 -35
  162. package/dist/get-CtJEJIC_.js +0 -82
  163. package/dist/get-D2VRwUw4.cjs +0 -1
  164. package/dist/identity.cjs +0 -1
  165. package/dist/identity.js +0 -4
  166. package/dist/index-Bfvg0v-N.cjs +0 -3
  167. package/dist/index-Bv029kh3.js +0 -19
  168. package/dist/index-CqisIWWC.cjs +0 -1
  169. package/dist/index-CyNZHQFw.cjs +0 -1
  170. package/dist/index-qmkoZBNO.js +0 -57
  171. package/dist/index-yz34Wc2p.js +0 -92
  172. package/dist/index.cjs +0 -5
  173. package/dist/index.js +0 -1004
  174. package/dist/kv.cjs +0 -1
  175. package/dist/kv.js +0 -4
  176. package/dist/link.cjs +0 -1
  177. package/dist/link.js +0 -10
  178. package/dist/location-0qDBiCqP.cjs +0 -1
  179. package/dist/location-BIzpxczO.js +0 -95
  180. package/dist/location.cjs +0 -1
  181. package/dist/location.js +0 -4
  182. package/dist/observe.cjs +0 -1
  183. package/dist/observe.js +0 -48
  184. package/dist/record-BwjIgrpU.cjs +0 -1
  185. package/dist/record-tSFQKmdG.js +0 -19
  186. package/dist/record.cjs +0 -1
  187. package/dist/record.js +0 -4
  188. package/dist/runtime.cjs +0 -1
  189. package/dist/runtime.js +0 -4
  190. package/dist/scale-BXy1w8R_.cjs +0 -1
  191. package/dist/scale-DJCMZbfU.js +0 -228
  192. package/dist/scale.cjs +0 -1
  193. package/dist/scale.js +0 -4
  194. package/dist/series-B7l2au4y.cjs +0 -6
  195. package/dist/series-TpAaBlEg.js +0 -2837
  196. package/dist/spatial-DnsaOypA.js +0 -11
  197. package/dist/spatial-DrxzaD5U.cjs +0 -1
  198. package/dist/spatial.cjs +0 -1
  199. package/dist/spatial.js +0 -18
  200. package/dist/src/clamp/index.d.ts +0 -2
  201. package/dist/src/clamp/index.d.ts.map +0 -1
  202. package/dist/src/destructor.d.ts +0 -7
  203. package/dist/src/destructor.d.ts.map +0 -1
  204. package/dist/src/flush.d.ts.map +0 -1
  205. package/dist/src/identity.d.ts +0 -3
  206. package/dist/src/identity.d.ts.map +0 -1
  207. package/dist/src/invert.d.ts +0 -2
  208. package/dist/src/invert.d.ts.map +0 -1
  209. package/dist/src/join.d.ts.map +0 -1
  210. package/dist/src/mock/index.d.ts +0 -2
  211. package/dist/src/mock/index.d.ts.map +0 -1
  212. package/dist/src/optional.d.ts.map +0 -1
  213. package/dist/src/renderable.d.ts +0 -5
  214. package/dist/src/renderable.d.ts.map +0 -1
  215. package/dist/src/replace.d.ts +0 -2
  216. package/dist/src/replace.d.ts.map +0 -1
  217. package/dist/src/shallowCopy.d.ts +0 -2
  218. package/dist/src/shallowCopy.d.ts.map +0 -1
  219. package/dist/src/telem/generate.d.ts +0 -3
  220. package/dist/src/telem/generate.d.ts.map +0 -1
  221. package/dist/src/transform.d.ts +0 -6
  222. package/dist/src/transform.d.ts.map +0 -1
  223. package/dist/src/undefined.d.ts +0 -2
  224. package/dist/src/undefined.d.ts.map +0 -1
  225. package/dist/telem.cjs +0 -1
  226. package/dist/telem.js +0 -18
  227. package/dist/unique.cjs +0 -1
  228. package/dist/unique.js +0 -4
  229. package/dist/url.cjs +0 -1
  230. package/dist/url.js +0 -51
  231. package/dist/worker.cjs +0 -1
  232. package/dist/worker.js +0 -43
  233. package/dist/xy-C-MUIjVs.cjs +0 -1
  234. package/dist/xy-DnrCAZaw.js +0 -154
  235. package/dist/xy.cjs +0 -1
  236. package/dist/xy.js +0 -4
  237. package/dist/zod.cjs +0 -1
  238. package/dist/zod.js +0 -4
  239. package/src/renderable.ts +0 -20
  240. package/src/telem/generate.ts +0 -17
  241. package/src/transform.ts +0 -17
  242. package/src/undefined.ts +0 -14
  243. /package/dist/src/{join.d.ts → deep/join.d.ts} +0 -0
  244. /package/dist/src/{optional.d.ts → optional/optional.d.ts} +0 -0
  245. /package/src/{join.ts → deep/join.ts} +0 -0
  246. /package/src/{optional.ts → optional/optional.ts} +0 -0
  247. /package/src/{flush.ts → scheduler/scheduler.ts} +0 -0
@@ -0,0 +1,584 @@
1
+ // Copyright 2025 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { describe, expect, test } from "vitest";
11
+
12
+ import { box } from "@/spatial/box";
13
+ import { type location } from "@/spatial/location";
14
+ import { sticky } from "@/spatial/sticky";
15
+
16
+ describe("sticky", () => {
17
+ describe("toCSS", () => {
18
+ interface Spec {
19
+ pos: sticky.XY;
20
+ expected: Partial<Record<location.Outer, string>>;
21
+ }
22
+ const SPECS: Spec[] = [
23
+ {
24
+ pos: { x: 10, y: 20 },
25
+ expected: { left: "1000%", top: "2000%" },
26
+ },
27
+ {
28
+ pos: { x: 10, y: 20, units: { x: "px", y: "px" } },
29
+ expected: { left: "10px", top: "20px" },
30
+ },
31
+ {
32
+ pos: { x: 10, y: 20, units: { x: "decimal", y: "decimal" } },
33
+ expected: { left: "1000%", top: "2000%" },
34
+ },
35
+ {
36
+ pos: { x: 10, y: 20, units: { x: "px", y: "decimal" } },
37
+ expected: { left: "10px", top: "2000%" },
38
+ },
39
+ {
40
+ pos: { x: 10, y: 20, units: { x: "decimal", y: "px" } },
41
+ expected: { left: "1000%", top: "20px" },
42
+ },
43
+ {
44
+ pos: { x: 10, y: 20, root: { x: "right", y: "bottom" } },
45
+ expected: { right: "1000%", bottom: "2000%" },
46
+ },
47
+ {
48
+ pos: {
49
+ x: 10,
50
+ y: 20,
51
+ root: { x: "right", y: "bottom" },
52
+ units: { x: "px", y: "px" },
53
+ },
54
+ expected: { right: "10px", bottom: "20px" },
55
+ },
56
+ {
57
+ pos: {
58
+ x: 10,
59
+ y: 20,
60
+ root: { x: "left", y: "bottom" },
61
+ units: { x: "decimal", y: "px" },
62
+ },
63
+ expected: { left: "1000%", bottom: "20px" },
64
+ },
65
+ {
66
+ pos: {
67
+ x: 10,
68
+ y: 20,
69
+ root: { x: "right", y: "top" },
70
+ units: { x: "px", y: "decimal" },
71
+ },
72
+ expected: { right: "10px", top: "2000%" },
73
+ },
74
+ {
75
+ pos: { x: 0, y: 0 },
76
+ expected: { left: "0%", top: "0%" },
77
+ },
78
+ {
79
+ pos: { x: 0.5, y: 0.5 },
80
+ expected: { left: "50%", top: "50%" },
81
+ },
82
+ {
83
+ pos: { x: 0.5, y: 0.5, units: { x: "decimal", y: "decimal" } },
84
+ expected: { left: "50%", top: "50%" },
85
+ },
86
+ {
87
+ pos: { x: 0.1, y: 0.9, root: { x: "right", y: "bottom" } },
88
+ expected: { right: "10%", bottom: "90%" },
89
+ },
90
+ ];
91
+ SPECS.forEach(({ pos, expected }, i) => {
92
+ test(`toCSS ${i}`, () => {
93
+ expect(sticky.toCSS(pos)).toEqual(expected);
94
+ });
95
+ });
96
+ });
97
+ describe("xy schema", () => {
98
+ interface Spec {
99
+ value: unknown;
100
+ valid: boolean;
101
+ }
102
+ const SPECS: Spec[] = [
103
+ { value: { x: 10, y: 20 }, valid: true },
104
+ { value: { x: 0, y: 0 }, valid: true },
105
+ { value: { x: -10, y: -20 }, valid: true },
106
+ { value: { x: 10, y: 20, root: { x: "left", y: "top" } }, valid: true },
107
+ { value: { x: 10, y: 20, root: { x: "right", y: "bottom" } }, valid: true },
108
+ {
109
+ value: { x: 10, y: 20, units: { x: "px", y: "px" } },
110
+ valid: true,
111
+ },
112
+ {
113
+ value: { x: 10, y: 20, units: { x: "decimal", y: "decimal" } },
114
+ valid: true,
115
+ },
116
+ {
117
+ value: {
118
+ x: 10,
119
+ y: 20,
120
+ root: { x: "left", y: "top" },
121
+ units: { x: "px", y: "decimal" },
122
+ },
123
+ valid: true,
124
+ },
125
+ { value: { x: 10 }, valid: false },
126
+ { value: { y: 20 }, valid: false },
127
+ { value: {}, valid: false },
128
+ { value: { x: "10", y: 20 }, valid: false },
129
+ { value: { x: 10, y: "20" }, valid: false },
130
+ { value: { x: 10, y: 20, root: { x: "invalid", y: "top" } }, valid: false },
131
+ { value: { x: 10, y: 20, root: { x: "left", y: "invalid" } }, valid: false },
132
+ { value: { x: 10, y: 20, units: { x: "invalid", y: "px" } }, valid: false },
133
+ { value: { x: 10, y: 20, units: { x: "px", y: "invalid" } }, valid: false },
134
+ ];
135
+ SPECS.forEach(({ value, valid }, i) => {
136
+ test(`xy schema ${i}`, () => {
137
+ const result = sticky.xy.safeParse(value);
138
+ expect(result.success).toBe(valid);
139
+ });
140
+ });
141
+ });
142
+ describe("completeXY schema", () => {
143
+ interface Spec {
144
+ value: unknown;
145
+ valid: boolean;
146
+ }
147
+ const SPECS: Spec[] = [
148
+ {
149
+ value: {
150
+ x: 10,
151
+ y: 20,
152
+ root: { x: "left", y: "top" },
153
+ units: { x: "px", y: "px" },
154
+ },
155
+ valid: true,
156
+ },
157
+ {
158
+ value: {
159
+ x: 10,
160
+ y: 20,
161
+ root: { x: "right", y: "bottom" },
162
+ units: { x: "decimal", y: "decimal" },
163
+ },
164
+ valid: true,
165
+ },
166
+ {
167
+ value: {
168
+ x: 0,
169
+ y: 0,
170
+ root: { x: "left", y: "top" },
171
+ units: { x: "px", y: "px" },
172
+ },
173
+ valid: true,
174
+ },
175
+ { value: { x: 10, y: 20 }, valid: false },
176
+ { value: { x: 10, y: 20, root: { x: "left", y: "top" } }, valid: false },
177
+ { value: { x: 10, y: 20, units: { x: "px", y: "px" } }, valid: false },
178
+ {
179
+ value: {
180
+ x: 10,
181
+ y: 20,
182
+ root: { x: "invalid", y: "top" },
183
+ units: { x: "px", y: "px" },
184
+ },
185
+ valid: false,
186
+ },
187
+ {
188
+ value: {
189
+ x: 10,
190
+ y: 20,
191
+ root: { x: "left", y: "top" },
192
+ units: { x: "invalid", y: "px" },
193
+ },
194
+ valid: false,
195
+ },
196
+ ];
197
+ SPECS.forEach(({ value, valid }, i) => {
198
+ test(`completeXY schema ${i}`, () => {
199
+ const result = sticky.completeXY.safeParse(value);
200
+ expect(result.success).toBe(valid);
201
+ });
202
+ });
203
+ });
204
+ describe("toDecimal", () => {
205
+ interface Spec {
206
+ position: sticky.XY;
207
+ element: box.Box;
208
+ container: box.Box;
209
+ expected: { x: number; y: number };
210
+ }
211
+ const SPECS: Spec[] = [
212
+ {
213
+ position: { x: 100, y: 200, units: { x: "px", y: "px" } },
214
+ element: box.construct(0, 0, 50, 50),
215
+ container: box.construct(0, 0, 1000, 1000),
216
+ expected: { x: 0.1, y: 0.2 },
217
+ },
218
+ {
219
+ position: {
220
+ x: 100,
221
+ y: 200,
222
+ root: { x: "right", y: "bottom" },
223
+ units: { x: "px", y: "px" },
224
+ },
225
+ element: box.construct(0, 0, 50, 50),
226
+ container: box.construct(0, 0, 1000, 1000),
227
+ expected: { x: 0.85, y: 0.75 },
228
+ },
229
+ {
230
+ position: { x: 0.5, y: 0.25, units: { x: "decimal", y: "decimal" } },
231
+ element: box.construct(0, 0, 50, 50),
232
+ container: box.construct(0, 0, 1000, 1000),
233
+ expected: { x: 0.5, y: 0.25 },
234
+ },
235
+ {
236
+ position: {
237
+ x: 0.3,
238
+ y: 0.7,
239
+ root: { x: "right", y: "bottom" },
240
+ units: { x: "decimal", y: "decimal" },
241
+ },
242
+ element: box.construct(0, 0, 50, 50),
243
+ container: box.construct(0, 0, 1000, 1000),
244
+ expected: { x: 0.7, y: 0.3 },
245
+ },
246
+ {
247
+ position: {
248
+ x: 50,
249
+ y: 100,
250
+ root: { x: "left", y: "top" },
251
+ units: { x: "px", y: "px" },
252
+ },
253
+ element: box.construct(0, 0, 100, 100),
254
+ container: box.construct(0, 0, 500, 500),
255
+ expected: { x: 0.1, y: 0.2 },
256
+ },
257
+ {
258
+ position: { x: 0, y: 0, units: { x: "px", y: "px" } },
259
+ element: box.construct(0, 0, 50, 50),
260
+ container: box.construct(0, 0, 1000, 1000),
261
+ expected: { x: 0, y: 0 },
262
+ },
263
+ {
264
+ position: {
265
+ x: 0,
266
+ y: 0,
267
+ root: { x: "right", y: "bottom" },
268
+ units: { x: "px", y: "px" },
269
+ },
270
+ element: box.construct(0, 0, 50, 50),
271
+ container: box.construct(0, 0, 1000, 1000),
272
+ expected: { x: 0.95, y: 0.95 },
273
+ },
274
+ {
275
+ position: {
276
+ x: 200,
277
+ y: 150,
278
+ root: { x: "left", y: "bottom" },
279
+ units: { x: "px", y: "px" },
280
+ },
281
+ element: box.construct(0, 0, 60, 40),
282
+ container: box.construct(0, 0, 800, 600),
283
+ expected: { x: 0.25, y: 0.68333 },
284
+ },
285
+ {
286
+ position: {
287
+ x: 200,
288
+ y: 150,
289
+ root: { x: "right", y: "top" },
290
+ units: { x: "px", y: "px" },
291
+ },
292
+ element: box.construct(0, 0, 60, 40),
293
+ container: box.construct(0, 0, 800, 600),
294
+ expected: { x: 0.675, y: 0.25 },
295
+ },
296
+ ];
297
+ SPECS.forEach(({ position, element, container, expected }, i) => {
298
+ test(`toDecimal ${i}`, () => {
299
+ const result = sticky.toDecimal({ position, element, container });
300
+ expect(result.x).toBeCloseTo(expected.x, 4);
301
+ expect(result.y).toBeCloseTo(expected.y, 4);
302
+ });
303
+ });
304
+ });
305
+ describe("calculate", () => {
306
+ interface Spec {
307
+ position: sticky.XY;
308
+ element: box.Box;
309
+ container: box.Box;
310
+ expected: {
311
+ x: number;
312
+ y: number;
313
+ root: { x: "left" | "right"; y: "top" | "bottom" };
314
+ units: { x: "px" | "decimal"; y: "px" | "decimal" };
315
+ };
316
+ }
317
+ const SPECS: Spec[] = [
318
+ {
319
+ position: { x: 0.1, y: 0.1 },
320
+ element: box.construct(0, 0, 50, 50),
321
+ container: box.construct(0, 0, 1000, 1000),
322
+ expected: {
323
+ x: 100,
324
+ y: 100,
325
+ root: { x: "left", y: "top" },
326
+ units: { x: "px", y: "px" },
327
+ },
328
+ },
329
+ {
330
+ position: { x: 0.9, y: 0.9 },
331
+ element: box.construct(0, 0, 50, 50),
332
+ container: box.construct(0, 0, 1000, 1000),
333
+ expected: {
334
+ x: 50,
335
+ y: 50,
336
+ root: { x: "right", y: "bottom" },
337
+ units: { x: "px", y: "px" },
338
+ },
339
+ },
340
+ {
341
+ position: { x: 0.5, y: 0.5 },
342
+ element: box.construct(0, 0, 50, 50),
343
+ container: box.construct(0, 0, 1000, 1000),
344
+ expected: {
345
+ x: 0.5,
346
+ y: 0.5,
347
+ root: { x: "left", y: "top" },
348
+ units: { x: "decimal", y: "decimal" },
349
+ },
350
+ },
351
+ {
352
+ position: { x: 0.05, y: 0.95 },
353
+ element: box.construct(0, 0, 100, 100),
354
+ container: box.construct(0, 0, 500, 500),
355
+ expected: {
356
+ x: 25,
357
+ y: -75,
358
+ root: { x: "left", y: "bottom" },
359
+ units: { x: "px", y: "px" },
360
+ },
361
+ },
362
+ {
363
+ position: { x: 0.95, y: 0.15 },
364
+ element: box.construct(0, 0, 100, 100),
365
+ container: box.construct(0, 0, 500, 500),
366
+ expected: {
367
+ x: -75,
368
+ y: 75,
369
+ root: { x: "right", y: "top" },
370
+ units: { x: "px", y: "px" },
371
+ },
372
+ },
373
+ {
374
+ position: { x: 0.3, y: 0.7 },
375
+ element: box.construct(0, 0, 80, 60),
376
+ container: box.construct(0, 0, 800, 600),
377
+ expected: {
378
+ x: 0.3,
379
+ y: 0.7,
380
+ root: { x: "left", y: "top" },
381
+ units: { x: "decimal", y: "decimal" },
382
+ },
383
+ },
384
+ {
385
+ position: { x: 0, y: 0 },
386
+ element: box.construct(0, 0, 50, 50),
387
+ container: box.construct(0, 0, 1000, 1000),
388
+ expected: {
389
+ x: 0,
390
+ y: 0,
391
+ root: { x: "left", y: "top" },
392
+ units: { x: "px", y: "px" },
393
+ },
394
+ },
395
+ {
396
+ position: { x: 1, y: 1 },
397
+ element: box.construct(0, 0, 50, 50),
398
+ container: box.construct(0, 0, 1000, 1000),
399
+ expected: {
400
+ x: -50,
401
+ y: -50,
402
+ root: { x: "right", y: "bottom" },
403
+ units: { x: "px", y: "px" },
404
+ },
405
+ },
406
+ {
407
+ position: { x: 0.2, y: 0.2 },
408
+ element: box.construct(0, 0, 100, 100),
409
+ container: box.construct(0, 0, 500, 500),
410
+ expected: {
411
+ x: 0.2,
412
+ y: 0.2,
413
+ root: { x: "left", y: "top" },
414
+ units: { x: "decimal", y: "decimal" },
415
+ },
416
+ },
417
+ {
418
+ position: { x: 0.8, y: 0.8 },
419
+ element: box.construct(0, 0, 100, 100),
420
+ container: box.construct(0, 0, 500, 500),
421
+ expected: {
422
+ x: 0.8,
423
+ y: 0.8,
424
+ root: { x: "left", y: "top" },
425
+ units: { x: "decimal", y: "decimal" },
426
+ },
427
+ },
428
+ ];
429
+ SPECS.forEach(({ position, element, container, expected }, i) => {
430
+ test(`calculate ${i}`, () => {
431
+ const result = sticky.calculate({ position, element, container });
432
+ expect(result).not.toBeNull();
433
+ if (result == null) return;
434
+ expect(result.x).toBeCloseTo(expected.x, 2);
435
+ expect(result.y).toBeCloseTo(expected.y, 2);
436
+ expect(result.root).toEqual(expected.root);
437
+ expect(result.units).toEqual(expected.units);
438
+ });
439
+ });
440
+ });
441
+ describe("calculate with custom thresholds", () => {
442
+ interface Spec {
443
+ position: sticky.XY;
444
+ element: box.Box;
445
+ container: box.Box;
446
+ lowerThreshold: number;
447
+ upperThreshold: number;
448
+ expected: {
449
+ x: number;
450
+ y: number;
451
+ root: { x: "left" | "right"; y: "top" | "bottom" };
452
+ units: { x: "px" | "decimal"; y: "px" | "decimal" };
453
+ };
454
+ }
455
+ const SPECS: Spec[] = [
456
+ {
457
+ position: { x: 0.3, y: 0.3 },
458
+ element: box.construct(0, 0, 50, 50),
459
+ container: box.construct(0, 0, 1000, 1000),
460
+ lowerThreshold: 0.4,
461
+ upperThreshold: 0.6,
462
+ expected: {
463
+ x: 300,
464
+ y: 300,
465
+ root: { x: "left", y: "top" },
466
+ units: { x: "px", y: "px" },
467
+ },
468
+ },
469
+ {
470
+ position: { x: 0.3, y: 0.3 },
471
+ element: box.construct(0, 0, 50, 50),
472
+ container: box.construct(0, 0, 1000, 1000),
473
+ lowerThreshold: 0.1,
474
+ upperThreshold: 0.9,
475
+ expected: {
476
+ x: 0.3,
477
+ y: 0.3,
478
+ root: { x: "left", y: "top" },
479
+ units: { x: "decimal", y: "decimal" },
480
+ },
481
+ },
482
+ {
483
+ position: { x: 0.25, y: 0.75 },
484
+ element: box.construct(0, 0, 50, 50),
485
+ container: box.construct(0, 0, 1000, 1000),
486
+ lowerThreshold: 0.3,
487
+ upperThreshold: 0.7,
488
+ expected: {
489
+ x: 250,
490
+ y: 200,
491
+ root: { x: "left", y: "bottom" },
492
+ units: { x: "px", y: "px" },
493
+ },
494
+ },
495
+ {
496
+ position: { x: 0.5, y: 0.5 },
497
+ element: box.construct(0, 0, 100, 100),
498
+ container: box.construct(0, 0, 500, 500),
499
+ lowerThreshold: 0.5,
500
+ upperThreshold: 0.5,
501
+ expected: {
502
+ x: 0.5,
503
+ y: 0.5,
504
+ root: { x: "left", y: "top" },
505
+ units: { x: "decimal", y: "decimal" },
506
+ },
507
+ },
508
+ {
509
+ position: { x: 0.6, y: 0.4 },
510
+ element: box.construct(0, 0, 50, 50),
511
+ container: box.construct(0, 0, 1000, 1000),
512
+ lowerThreshold: 0.3,
513
+ upperThreshold: 0.5,
514
+ expected: {
515
+ x: 350,
516
+ y: 0.4,
517
+ root: { x: "right", y: "top" },
518
+ units: { x: "px", y: "decimal" },
519
+ },
520
+ },
521
+ {
522
+ position: { x: 0.1, y: 0.95 },
523
+ element: box.construct(0, 0, 80, 60),
524
+ container: box.construct(0, 0, 800, 600),
525
+ lowerThreshold: 0.15,
526
+ upperThreshold: 0.9,
527
+ expected: {
528
+ x: 80,
529
+ y: -30,
530
+ root: { x: "left", y: "bottom" },
531
+ units: { x: "px", y: "px" },
532
+ },
533
+ },
534
+ {
535
+ position: { x: 0.4, y: 0.6 },
536
+ element: box.construct(0, 0, 100, 100),
537
+ container: box.construct(0, 0, 1000, 1000),
538
+ lowerThreshold: 0,
539
+ upperThreshold: 1,
540
+ expected: {
541
+ x: 0.4,
542
+ y: 0.6,
543
+ root: { x: "left", y: "top" },
544
+ units: { x: "decimal", y: "decimal" },
545
+ },
546
+ },
547
+ {
548
+ position: { x: 0.4, y: 0.6 },
549
+ element: box.construct(0, 0, 100, 100),
550
+ container: box.construct(0, 0, 1000, 1000),
551
+ lowerThreshold: 0.5,
552
+ upperThreshold: 0.5,
553
+ expected: {
554
+ x: 400,
555
+ y: 300,
556
+ root: { x: "left", y: "bottom" },
557
+ units: { x: "px", y: "px" },
558
+ },
559
+ },
560
+ ];
561
+ SPECS.forEach(
562
+ (
563
+ { position, element, container, lowerThreshold, upperThreshold, expected },
564
+ i,
565
+ ) => {
566
+ test(`calculate with custom thresholds ${i}`, () => {
567
+ const result = sticky.calculate({
568
+ position,
569
+ element,
570
+ container,
571
+ lowerThreshold,
572
+ upperThreshold,
573
+ });
574
+ expect(result).not.toBeNull();
575
+ if (result == null) return;
576
+ expect(result.x).toBeCloseTo(expected.x, 2);
577
+ expect(result.y).toBeCloseTo(expected.y, 2);
578
+ expect(result.root).toEqual(expected.root);
579
+ expect(result.units).toEqual(expected.units);
580
+ });
581
+ },
582
+ );
583
+ });
584
+ });
@@ -0,0 +1,98 @@
1
+ // Copyright 2025 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import z from "zod";
11
+
12
+ import { box } from "@/spatial/box";
13
+ import { location } from "@/spatial/location";
14
+ import { xy as base } from "@/spatial/xy";
15
+
16
+ export const completeXY = base.xy.extend({
17
+ root: location.corner,
18
+ units: z.object({
19
+ x: z.enum(["px", "decimal"]),
20
+ y: z.enum(["px", "decimal"]),
21
+ }),
22
+ });
23
+
24
+ export type CompleteXY = z.infer<typeof completeXY>;
25
+
26
+ export const xy = completeXY.partial({ root: true, units: true });
27
+
28
+ export interface XY extends z.infer<typeof xy> {}
29
+
30
+ interface ToCSSReturn extends Partial<Record<location.Outer, string>> {}
31
+
32
+ export const toCSS = (pos: XY): ToCSSReturn => {
33
+ const ret: ToCSSReturn = {};
34
+ ret[pos.root?.x ?? "left"] =
35
+ pos?.units?.x === "px" ? `${pos.x}px` : `${pos.x * 100}%`;
36
+ ret[pos.root?.y ?? "top"] = pos?.units?.y === "px" ? `${pos.y}px` : `${pos.y * 100}%`;
37
+ return ret;
38
+ };
39
+
40
+ export interface ToDecimalProps {
41
+ position: XY;
42
+ element: box.Box;
43
+ container: box.Box;
44
+ }
45
+
46
+ export const toDecimal = ({
47
+ position,
48
+ element,
49
+ container,
50
+ }: ToDecimalProps): base.XY => {
51
+ const ret = { x: position.x, y: position.y };
52
+ if (position.units?.x === "decimal") {
53
+ if (position.root?.x === "right") ret.x = 1 - position.x;
54
+ } else if (position.root?.x === "right")
55
+ ret.x = 1 - (position.x + box.width(element)) / box.width(container);
56
+ else ret.x /= box.width(container);
57
+ if (position.units?.y === "decimal") {
58
+ if (position.root?.y === "bottom") ret.y = 1 - position.y;
59
+ } else if (position.root?.y === "bottom")
60
+ ret.y = 1 - (position.y + box.height(element)) / box.height(container);
61
+ else ret.y /= box.height(container);
62
+ return ret;
63
+ };
64
+
65
+ export interface CalculateProps {
66
+ position: XY;
67
+ element: box.Box;
68
+ container: box.Box;
69
+ lowerThreshold?: number;
70
+ upperThreshold?: number;
71
+ }
72
+
73
+ export const calculate = ({
74
+ position,
75
+ element,
76
+ container,
77
+ lowerThreshold = 0.2,
78
+ upperThreshold = 0.8,
79
+ }: CalculateProps): XY => {
80
+ const ret: Required<XY> = {
81
+ x: position.x,
82
+ y: position.y,
83
+ root: { ...location.TOP_LEFT },
84
+ units: { x: "px", y: "px" },
85
+ };
86
+ if (position.x > upperThreshold) {
87
+ ret.x = (1 - position.x) * box.width(container) - box.width(element);
88
+ ret.root.x = "right";
89
+ } else if (position.x < lowerThreshold) ret.x = position.x * box.width(container);
90
+ else ret.units.x = "decimal";
91
+ if (position.y > upperThreshold) {
92
+ ret.y = (1 - position.y) * box.height(container) - box.height(element);
93
+ ret.root.y = "bottom";
94
+ } else if (position.y < lowerThreshold) ret.y = position.y * box.height(container);
95
+ else ret.units.y = "decimal";
96
+ ret.x = Math.round(ret.x * 100) / 100;
97
+ return { ...ret, ...base.truncate(ret, 3) };
98
+ };