@getuserfeedback/protocol 3.0.8 → 3.0.10
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/dist/host/index.d.ts +1 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -0
- package/dist/host/widget-layout-size-update.d.ts +31 -2
- package/dist/host/widget-layout-size-update.d.ts.map +1 -1
- package/dist/host/widget-layout-size-update.js +40 -0
- package/dist/host/widget-layout-transition-plan.d.ts +23 -0
- package/dist/host/widget-layout-transition-plan.d.ts.map +1 -0
- package/dist/host/widget-layout-transition-plan.js +52 -0
- package/package.json +1 -1
- package/src/host/index.ts +1 -0
- package/src/host/widget-layout-size-update.test.ts +65 -0
- package/src/host/widget-layout-size-update.ts +85 -2
- package/src/host/widget-layout-transition-plan.test.ts +122 -0
- package/src/host/widget-layout-transition-plan.ts +97 -0
package/dist/host/index.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export { createCommandRequestId } from "./request-id.js";
|
|
|
12
12
|
export * from "./sdk-types.js";
|
|
13
13
|
export { createUniqueId } from "./unique-id.js";
|
|
14
14
|
export * from "./widget-layout-size-update.js";
|
|
15
|
+
export * from "./widget-layout-transition-plan.js";
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/host/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,cAAc,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC"}
|
package/dist/host/index.js
CHANGED
|
@@ -2,14 +2,43 @@ export type WidgetLayoutSize = {
|
|
|
2
2
|
width: number;
|
|
3
3
|
height: number;
|
|
4
4
|
};
|
|
5
|
-
export type
|
|
5
|
+
export type WidgetLayoutCubicBezier = readonly [
|
|
6
|
+
x1: number,
|
|
7
|
+
y1: number,
|
|
8
|
+
x2: number,
|
|
9
|
+
y2: number
|
|
10
|
+
];
|
|
11
|
+
export type WidgetLayoutInstantSizeTransition = {
|
|
6
12
|
kind: "instant";
|
|
7
13
|
durationMs: 0;
|
|
8
14
|
};
|
|
15
|
+
export type WidgetLayoutTimedSizeTransition = {
|
|
16
|
+
kind: "timed";
|
|
17
|
+
durationMs: number;
|
|
18
|
+
easing: {
|
|
19
|
+
kind: "cubic-bezier";
|
|
20
|
+
points: WidgetLayoutCubicBezier;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type WidgetLayoutSizeTransition = WidgetLayoutInstantSizeTransition;
|
|
9
24
|
export type WidgetLayoutSizeUpdate = {
|
|
10
25
|
size: WidgetLayoutSize;
|
|
11
26
|
transition: WidgetLayoutSizeTransition;
|
|
12
27
|
};
|
|
13
|
-
export
|
|
28
|
+
export type WidgetLayoutTimedSizeUpdate = {
|
|
29
|
+
size: WidgetLayoutSize;
|
|
30
|
+
transition: WidgetLayoutTimedSizeTransition;
|
|
31
|
+
};
|
|
32
|
+
export declare const INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION: WidgetLayoutInstantSizeTransition;
|
|
33
|
+
export declare function isWidgetLayoutCubicBezier(points: unknown): points is WidgetLayoutCubicBezier;
|
|
14
34
|
export declare function toInstantWidgetLayoutSizeUpdate(size: WidgetLayoutSize): WidgetLayoutSizeUpdate;
|
|
35
|
+
export declare function toTimedWidgetLayoutSizeTransition(input: {
|
|
36
|
+
durationMs: number;
|
|
37
|
+
easing: WidgetLayoutCubicBezier;
|
|
38
|
+
}): WidgetLayoutTimedSizeTransition;
|
|
39
|
+
export declare function toTimedWidgetLayoutSizeUpdate(input: {
|
|
40
|
+
size: WidgetLayoutSize;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
easing: WidgetLayoutCubicBezier;
|
|
43
|
+
}): WidgetLayoutTimedSizeUpdate;
|
|
15
44
|
//# sourceMappingURL=widget-layout-size-update.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widget-layout-size-update.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-size-update.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"widget-layout-size-update.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-size-update.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,SAAS;IAC9C,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;CACV,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,CAAC,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QACP,IAAI,EAAE,cAAc,CAAC;QACrB,MAAM,EAAE,uBAAuB,CAAC;KAChC,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,iCAAiC,CAAC;AAE3E,MAAM,MAAM,sBAAsB,GAAG;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,0BAA0B,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACzC,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,+BAA+B,CAAC;CAC5C,CAAC;AAEF,eAAO,MAAM,qCAAqC,EAAE,iCAIlD,CAAC;AAKH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,OAAO,GACb,MAAM,IAAI,uBAAuB,CAenC;AAED,wBAAgB,+BAA+B,CAC9C,IAAI,EAAE,gBAAgB,GACpB,sBAAsB,CAKxB;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,uBAAuB,CAAC;CAChC,GAAG,+BAA+B,CAmBlC;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACpD,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,uBAAuB,CAAC;CAChC,GAAG,2BAA2B,CAQ9B"}
|
|
@@ -2,9 +2,49 @@ export const INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION = {
|
|
|
2
2
|
kind: "instant",
|
|
3
3
|
durationMs: 0,
|
|
4
4
|
};
|
|
5
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
6
|
+
export function isWidgetLayoutCubicBezier(points) {
|
|
7
|
+
if (!Array.isArray(points) || points.length !== 4) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const [x1, y1, x2, y2] = points;
|
|
11
|
+
return (isFiniteNumber(x1) &&
|
|
12
|
+
x1 >= 0 &&
|
|
13
|
+
x1 <= 1 &&
|
|
14
|
+
isFiniteNumber(y1) &&
|
|
15
|
+
isFiniteNumber(x2) &&
|
|
16
|
+
x2 >= 0 &&
|
|
17
|
+
x2 <= 1 &&
|
|
18
|
+
isFiniteNumber(y2));
|
|
19
|
+
}
|
|
5
20
|
export function toInstantWidgetLayoutSizeUpdate(size) {
|
|
6
21
|
return {
|
|
7
22
|
size: { ...size },
|
|
8
23
|
transition: INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION,
|
|
9
24
|
};
|
|
10
25
|
}
|
|
26
|
+
export function toTimedWidgetLayoutSizeTransition(input) {
|
|
27
|
+
if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) {
|
|
28
|
+
throw new RangeError("Timed widget layout transitions require durationMs > 0");
|
|
29
|
+
}
|
|
30
|
+
if (!isWidgetLayoutCubicBezier(input.easing)) {
|
|
31
|
+
throw new RangeError("Timed widget layout transitions require cubic-bezier x control points in [0, 1] and finite y control points");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: "timed",
|
|
35
|
+
durationMs: input.durationMs,
|
|
36
|
+
easing: {
|
|
37
|
+
kind: "cubic-bezier",
|
|
38
|
+
points: [...input.easing],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function toTimedWidgetLayoutSizeUpdate(input) {
|
|
43
|
+
return {
|
|
44
|
+
size: { ...input.size },
|
|
45
|
+
transition: toTimedWidgetLayoutSizeTransition({
|
|
46
|
+
durationMs: input.durationMs,
|
|
47
|
+
easing: input.easing,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WidgetLayoutSize, WidgetLayoutSizeUpdate, WidgetLayoutTimedSizeUpdate } from "./widget-layout-size-update.js";
|
|
2
|
+
export type WidgetLayoutComparableSizeUpdate = WidgetLayoutSizeUpdate | WidgetLayoutTimedSizeUpdate;
|
|
3
|
+
export type WidgetLayoutUnchangedTransitionPlan = {
|
|
4
|
+
kind: "unchanged";
|
|
5
|
+
size: WidgetLayoutSize;
|
|
6
|
+
update: WidgetLayoutSizeUpdate;
|
|
7
|
+
observedAtMs: number;
|
|
8
|
+
};
|
|
9
|
+
export type WidgetLayoutInstantTransitionPlan = {
|
|
10
|
+
kind: "instant";
|
|
11
|
+
size: WidgetLayoutSize;
|
|
12
|
+
update: WidgetLayoutSizeUpdate;
|
|
13
|
+
startedAtMs: number;
|
|
14
|
+
settledAtMs: number;
|
|
15
|
+
};
|
|
16
|
+
export type WidgetLayoutTransitionPlan = WidgetLayoutUnchangedTransitionPlan | WidgetLayoutInstantTransitionPlan;
|
|
17
|
+
export declare function areWidgetLayoutSizeUpdatesEqual(left: WidgetLayoutComparableSizeUpdate | undefined, right: WidgetLayoutComparableSizeUpdate): boolean;
|
|
18
|
+
export declare function planWidgetLayoutTransition(input: {
|
|
19
|
+
previous?: WidgetLayoutSizeUpdate;
|
|
20
|
+
next: WidgetLayoutSizeUpdate;
|
|
21
|
+
nowMs: number;
|
|
22
|
+
}): WidgetLayoutTransitionPlan;
|
|
23
|
+
//# sourceMappingURL=widget-layout-transition-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-layout-transition-plan.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-transition-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,gBAAgB,EAChB,sBAAsB,EACtB,2BAA2B,EAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,MAAM,gCAAgC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAE/B,MAAM,MAAM,mCAAmC,GAAG;IACjD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACnC,mCAAmC,GACnC,iCAAiC,CAAC;AAErC,wBAAgB,+BAA+B,CAC9C,IAAI,EAAE,gCAAgC,GAAG,SAAS,EAClD,KAAK,EAAE,gCAAgC,GACrC,OAAO,CAiCT;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,0BAA0B,CAyB7B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function areWidgetLayoutSizeUpdatesEqual(left, right) {
|
|
2
|
+
if (left?.size.width !== right.size.width ||
|
|
3
|
+
left.size.height !== right.size.height ||
|
|
4
|
+
left.transition.kind !== right.transition.kind ||
|
|
5
|
+
left.transition.durationMs !== right.transition.durationMs) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const leftTransition = left.transition;
|
|
9
|
+
const rightTransition = right.transition;
|
|
10
|
+
switch (rightTransition.kind) {
|
|
11
|
+
case "instant":
|
|
12
|
+
return true;
|
|
13
|
+
case "timed":
|
|
14
|
+
if (leftTransition.kind !== "timed") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return (leftTransition.easing.kind === rightTransition.easing.kind &&
|
|
18
|
+
leftTransition.easing.points.length ===
|
|
19
|
+
rightTransition.easing.points.length &&
|
|
20
|
+
leftTransition.easing.points.every((point, index) => point === rightTransition.easing.points[index]));
|
|
21
|
+
default: {
|
|
22
|
+
const unreachable = rightTransition;
|
|
23
|
+
throw new Error(`Unsupported widget layout transition: ${JSON.stringify(unreachable)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function planWidgetLayoutTransition(input) {
|
|
28
|
+
if (!Number.isFinite(input.nowMs)) {
|
|
29
|
+
throw new RangeError("Widget layout transition plans require finite nowMs");
|
|
30
|
+
}
|
|
31
|
+
if (areWidgetLayoutSizeUpdatesEqual(input.previous, input.next)) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "unchanged",
|
|
34
|
+
size: { ...input.next.size },
|
|
35
|
+
update: {
|
|
36
|
+
size: { ...input.next.size },
|
|
37
|
+
transition: { ...input.next.transition },
|
|
38
|
+
},
|
|
39
|
+
observedAtMs: input.nowMs,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: "instant",
|
|
44
|
+
size: { ...input.next.size },
|
|
45
|
+
update: {
|
|
46
|
+
size: { ...input.next.size },
|
|
47
|
+
transition: { ...input.next.transition },
|
|
48
|
+
},
|
|
49
|
+
startedAtMs: input.nowMs,
|
|
50
|
+
settledAtMs: input.nowMs,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/package.json
CHANGED
package/src/host/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION,
|
|
4
|
+
isWidgetLayoutCubicBezier,
|
|
4
5
|
toInstantWidgetLayoutSizeUpdate,
|
|
6
|
+
toTimedWidgetLayoutSizeTransition,
|
|
7
|
+
toTimedWidgetLayoutSizeUpdate,
|
|
5
8
|
} from "./widget-layout-size-update.js";
|
|
6
9
|
|
|
7
10
|
describe("widget layout size update", () => {
|
|
@@ -22,4 +25,66 @@ describe("widget layout size update", () => {
|
|
|
22
25
|
toInstantWidgetLayoutSizeUpdate({ width: 1, height: 1 }).transition,
|
|
23
26
|
).toBe(INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION);
|
|
24
27
|
});
|
|
28
|
+
|
|
29
|
+
test("keeps the existing size update contract instant-only", () => {
|
|
30
|
+
const update = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 });
|
|
31
|
+
|
|
32
|
+
expect(update.transition.kind).toBe("instant");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("wraps a host size in the timed transition contract", () => {
|
|
36
|
+
expect(
|
|
37
|
+
toTimedWidgetLayoutSizeUpdate({
|
|
38
|
+
size: { width: 320, height: 240 },
|
|
39
|
+
durationMs: 180,
|
|
40
|
+
easing: [0.2, 0, 0, 1],
|
|
41
|
+
}),
|
|
42
|
+
).toEqual({
|
|
43
|
+
size: { width: 320, height: 240 },
|
|
44
|
+
transition: {
|
|
45
|
+
kind: "timed",
|
|
46
|
+
durationMs: 180,
|
|
47
|
+
easing: {
|
|
48
|
+
kind: "cubic-bezier",
|
|
49
|
+
points: [0.2, 0, 0, 1],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("accepts cubic-bezier y control point overshoot", () => {
|
|
56
|
+
expect(isWidgetLayoutCubicBezier([0.34, -0.2, 0.64, 1.35])).toBe(true);
|
|
57
|
+
expect(
|
|
58
|
+
toTimedWidgetLayoutSizeTransition({
|
|
59
|
+
durationMs: 220,
|
|
60
|
+
easing: [0.34, -0.2, 0.64, 1.35],
|
|
61
|
+
}).easing.points,
|
|
62
|
+
).toEqual([0.34, -0.2, 0.64, 1.35]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("fails closed for non-array cubic-bezier input", () => {
|
|
66
|
+
expect(isWidgetLayoutCubicBezier(null)).toBe(false);
|
|
67
|
+
expect(isWidgetLayoutCubicBezier({ 0: 0.2, 1: 0, 2: 0, 3: 1 })).toBe(false);
|
|
68
|
+
expect(isWidgetLayoutCubicBezier("0.2,0,0,1")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("rejects cubic-bezier x control points outside normalized time", () => {
|
|
72
|
+
expect(isWidgetLayoutCubicBezier([-0.01, 0, 0.5, 1])).toBe(false);
|
|
73
|
+
expect(isWidgetLayoutCubicBezier([0.5, 0, 1.01, 1])).toBe(false);
|
|
74
|
+
expect(() =>
|
|
75
|
+
toTimedWidgetLayoutSizeTransition({
|
|
76
|
+
durationMs: 220,
|
|
77
|
+
easing: [-0.01, 0, 0.5, 1],
|
|
78
|
+
}),
|
|
79
|
+
).toThrow(RangeError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("rejects invalid timed transition duration", () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
toTimedWidgetLayoutSizeTransition({
|
|
85
|
+
durationMs: 0,
|
|
86
|
+
easing: [0.2, 0, 0, 1],
|
|
87
|
+
}),
|
|
88
|
+
).toThrow(RangeError);
|
|
89
|
+
});
|
|
25
90
|
});
|
|
@@ -3,22 +3,67 @@ export type WidgetLayoutSize = {
|
|
|
3
3
|
height: number;
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type WidgetLayoutCubicBezier = readonly [
|
|
7
|
+
x1: number,
|
|
8
|
+
y1: number,
|
|
9
|
+
x2: number,
|
|
10
|
+
y2: number,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export type WidgetLayoutInstantSizeTransition = {
|
|
7
14
|
kind: "instant";
|
|
8
15
|
durationMs: 0;
|
|
9
16
|
};
|
|
10
17
|
|
|
18
|
+
export type WidgetLayoutTimedSizeTransition = {
|
|
19
|
+
kind: "timed";
|
|
20
|
+
durationMs: number;
|
|
21
|
+
easing: {
|
|
22
|
+
kind: "cubic-bezier";
|
|
23
|
+
points: WidgetLayoutCubicBezier;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type WidgetLayoutSizeTransition = WidgetLayoutInstantSizeTransition;
|
|
28
|
+
|
|
11
29
|
export type WidgetLayoutSizeUpdate = {
|
|
12
30
|
size: WidgetLayoutSize;
|
|
13
31
|
transition: WidgetLayoutSizeTransition;
|
|
14
32
|
};
|
|
15
33
|
|
|
16
|
-
export
|
|
34
|
+
export type WidgetLayoutTimedSizeUpdate = {
|
|
35
|
+
size: WidgetLayoutSize;
|
|
36
|
+
transition: WidgetLayoutTimedSizeTransition;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION: WidgetLayoutInstantSizeTransition =
|
|
17
40
|
{
|
|
18
41
|
kind: "instant",
|
|
19
42
|
durationMs: 0,
|
|
20
43
|
};
|
|
21
44
|
|
|
45
|
+
const isFiniteNumber = (value: unknown): value is number =>
|
|
46
|
+
typeof value === "number" && Number.isFinite(value);
|
|
47
|
+
|
|
48
|
+
export function isWidgetLayoutCubicBezier(
|
|
49
|
+
points: unknown,
|
|
50
|
+
): points is WidgetLayoutCubicBezier {
|
|
51
|
+
if (!Array.isArray(points) || points.length !== 4) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const [x1, y1, x2, y2] = points;
|
|
55
|
+
return (
|
|
56
|
+
isFiniteNumber(x1) &&
|
|
57
|
+
x1 >= 0 &&
|
|
58
|
+
x1 <= 1 &&
|
|
59
|
+
isFiniteNumber(y1) &&
|
|
60
|
+
isFiniteNumber(x2) &&
|
|
61
|
+
x2 >= 0 &&
|
|
62
|
+
x2 <= 1 &&
|
|
63
|
+
isFiniteNumber(y2)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
export function toInstantWidgetLayoutSizeUpdate(
|
|
23
68
|
size: WidgetLayoutSize,
|
|
24
69
|
): WidgetLayoutSizeUpdate {
|
|
@@ -27,3 +72,41 @@ export function toInstantWidgetLayoutSizeUpdate(
|
|
|
27
72
|
transition: INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION,
|
|
28
73
|
};
|
|
29
74
|
}
|
|
75
|
+
|
|
76
|
+
export function toTimedWidgetLayoutSizeTransition(input: {
|
|
77
|
+
durationMs: number;
|
|
78
|
+
easing: WidgetLayoutCubicBezier;
|
|
79
|
+
}): WidgetLayoutTimedSizeTransition {
|
|
80
|
+
if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) {
|
|
81
|
+
throw new RangeError(
|
|
82
|
+
"Timed widget layout transitions require durationMs > 0",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!isWidgetLayoutCubicBezier(input.easing)) {
|
|
86
|
+
throw new RangeError(
|
|
87
|
+
"Timed widget layout transitions require cubic-bezier x control points in [0, 1] and finite y control points",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "timed",
|
|
92
|
+
durationMs: input.durationMs,
|
|
93
|
+
easing: {
|
|
94
|
+
kind: "cubic-bezier",
|
|
95
|
+
points: [...input.easing],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function toTimedWidgetLayoutSizeUpdate(input: {
|
|
101
|
+
size: WidgetLayoutSize;
|
|
102
|
+
durationMs: number;
|
|
103
|
+
easing: WidgetLayoutCubicBezier;
|
|
104
|
+
}): WidgetLayoutTimedSizeUpdate {
|
|
105
|
+
return {
|
|
106
|
+
size: { ...input.size },
|
|
107
|
+
transition: toTimedWidgetLayoutSizeTransition({
|
|
108
|
+
durationMs: input.durationMs,
|
|
109
|
+
easing: input.easing,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
toInstantWidgetLayoutSizeUpdate,
|
|
4
|
+
toTimedWidgetLayoutSizeUpdate,
|
|
5
|
+
} from "./widget-layout-size-update.js";
|
|
6
|
+
import {
|
|
7
|
+
areWidgetLayoutSizeUpdatesEqual,
|
|
8
|
+
planWidgetLayoutTransition,
|
|
9
|
+
} from "./widget-layout-transition-plan.js";
|
|
10
|
+
|
|
11
|
+
describe("widget layout transition plan", () => {
|
|
12
|
+
test("plans the first instant layout update at the requested time", () => {
|
|
13
|
+
const next = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 });
|
|
14
|
+
|
|
15
|
+
expect(
|
|
16
|
+
planWidgetLayoutTransition({
|
|
17
|
+
next,
|
|
18
|
+
nowMs: 1_000,
|
|
19
|
+
}),
|
|
20
|
+
).toEqual({
|
|
21
|
+
kind: "instant",
|
|
22
|
+
size: { width: 320, height: 240 },
|
|
23
|
+
update: {
|
|
24
|
+
size: { width: 320, height: 240 },
|
|
25
|
+
transition: {
|
|
26
|
+
kind: "instant",
|
|
27
|
+
durationMs: 0,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
startedAtMs: 1_000,
|
|
31
|
+
settledAtMs: 1_000,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("keeps repeated identical instant updates explicit as unchanged", () => {
|
|
36
|
+
const previous = toInstantWidgetLayoutSizeUpdate({
|
|
37
|
+
width: 320,
|
|
38
|
+
height: 240,
|
|
39
|
+
});
|
|
40
|
+
const next = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 });
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
planWidgetLayoutTransition({
|
|
44
|
+
previous,
|
|
45
|
+
next,
|
|
46
|
+
nowMs: 1_250,
|
|
47
|
+
}),
|
|
48
|
+
).toMatchObject({
|
|
49
|
+
kind: "unchanged",
|
|
50
|
+
observedAtMs: 1_250,
|
|
51
|
+
});
|
|
52
|
+
expect(areWidgetLayoutSizeUpdatesEqual(previous, next)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("compares the full timed transition shape before it is live", () => {
|
|
56
|
+
const base = {
|
|
57
|
+
size: { width: 320, height: 240 },
|
|
58
|
+
durationMs: 180,
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
areWidgetLayoutSizeUpdatesEqual(
|
|
63
|
+
toTimedWidgetLayoutSizeUpdate({ ...base, easing: [0.2, 0, 0, 1] }),
|
|
64
|
+
toTimedWidgetLayoutSizeUpdate({ ...base, easing: [0.2, 0, 0, 1] }),
|
|
65
|
+
),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(
|
|
68
|
+
areWidgetLayoutSizeUpdatesEqual(
|
|
69
|
+
toTimedWidgetLayoutSizeUpdate({ ...base, easing: [0.2, 0, 0, 1] }),
|
|
70
|
+
toTimedWidgetLayoutSizeUpdate({ ...base, easing: [0.3, 0, 0, 1] }),
|
|
71
|
+
),
|
|
72
|
+
).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("treats size changes as instant while the live protocol is instant-only", () => {
|
|
76
|
+
const previous = toInstantWidgetLayoutSizeUpdate({
|
|
77
|
+
width: 320,
|
|
78
|
+
height: 240,
|
|
79
|
+
});
|
|
80
|
+
const next = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 360 });
|
|
81
|
+
|
|
82
|
+
expect(
|
|
83
|
+
planWidgetLayoutTransition({
|
|
84
|
+
previous,
|
|
85
|
+
next,
|
|
86
|
+
nowMs: 1_500,
|
|
87
|
+
}),
|
|
88
|
+
).toMatchObject({
|
|
89
|
+
kind: "instant",
|
|
90
|
+
size: { width: 320, height: 360 },
|
|
91
|
+
startedAtMs: 1_500,
|
|
92
|
+
settledAtMs: 1_500,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("does not leak mutable references from the next update", () => {
|
|
97
|
+
const next = {
|
|
98
|
+
size: { width: 320, height: 240 },
|
|
99
|
+
transition: { kind: "instant", durationMs: 0 },
|
|
100
|
+
} as const;
|
|
101
|
+
const plan = planWidgetLayoutTransition({
|
|
102
|
+
next,
|
|
103
|
+
nowMs: 1_000,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
(next.size as { height: number }).height = 360;
|
|
107
|
+
(next.transition as { durationMs: number }).durationMs = 1;
|
|
108
|
+
|
|
109
|
+
expect(plan.size).toEqual({ width: 320, height: 240 });
|
|
110
|
+
expect(plan.update.size).toEqual({ width: 320, height: 240 });
|
|
111
|
+
expect(plan.update.transition.durationMs).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("rejects non-finite plan time", () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
planWidgetLayoutTransition({
|
|
117
|
+
next: toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 }),
|
|
118
|
+
nowMs: Number.NaN,
|
|
119
|
+
}),
|
|
120
|
+
).toThrow(RangeError);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WidgetLayoutSize,
|
|
3
|
+
WidgetLayoutSizeUpdate,
|
|
4
|
+
WidgetLayoutTimedSizeUpdate,
|
|
5
|
+
} from "./widget-layout-size-update.js";
|
|
6
|
+
|
|
7
|
+
export type WidgetLayoutComparableSizeUpdate =
|
|
8
|
+
| WidgetLayoutSizeUpdate
|
|
9
|
+
| WidgetLayoutTimedSizeUpdate;
|
|
10
|
+
|
|
11
|
+
export type WidgetLayoutUnchangedTransitionPlan = {
|
|
12
|
+
kind: "unchanged";
|
|
13
|
+
size: WidgetLayoutSize;
|
|
14
|
+
update: WidgetLayoutSizeUpdate;
|
|
15
|
+
observedAtMs: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WidgetLayoutInstantTransitionPlan = {
|
|
19
|
+
kind: "instant";
|
|
20
|
+
size: WidgetLayoutSize;
|
|
21
|
+
update: WidgetLayoutSizeUpdate;
|
|
22
|
+
startedAtMs: number;
|
|
23
|
+
settledAtMs: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type WidgetLayoutTransitionPlan =
|
|
27
|
+
| WidgetLayoutUnchangedTransitionPlan
|
|
28
|
+
| WidgetLayoutInstantTransitionPlan;
|
|
29
|
+
|
|
30
|
+
export function areWidgetLayoutSizeUpdatesEqual(
|
|
31
|
+
left: WidgetLayoutComparableSizeUpdate | undefined,
|
|
32
|
+
right: WidgetLayoutComparableSizeUpdate,
|
|
33
|
+
): boolean {
|
|
34
|
+
if (
|
|
35
|
+
left?.size.width !== right.size.width ||
|
|
36
|
+
left.size.height !== right.size.height ||
|
|
37
|
+
left.transition.kind !== right.transition.kind ||
|
|
38
|
+
left.transition.durationMs !== right.transition.durationMs
|
|
39
|
+
) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const leftTransition = left.transition;
|
|
43
|
+
const rightTransition = right.transition;
|
|
44
|
+
switch (rightTransition.kind) {
|
|
45
|
+
case "instant":
|
|
46
|
+
return true;
|
|
47
|
+
case "timed":
|
|
48
|
+
if (leftTransition.kind !== "timed") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
leftTransition.easing.kind === rightTransition.easing.kind &&
|
|
53
|
+
leftTransition.easing.points.length ===
|
|
54
|
+
rightTransition.easing.points.length &&
|
|
55
|
+
leftTransition.easing.points.every(
|
|
56
|
+
(point, index) => point === rightTransition.easing.points[index],
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
default: {
|
|
60
|
+
const unreachable: never = rightTransition;
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Unsupported widget layout transition: ${JSON.stringify(unreachable)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function planWidgetLayoutTransition(input: {
|
|
69
|
+
previous?: WidgetLayoutSizeUpdate;
|
|
70
|
+
next: WidgetLayoutSizeUpdate;
|
|
71
|
+
nowMs: number;
|
|
72
|
+
}): WidgetLayoutTransitionPlan {
|
|
73
|
+
if (!Number.isFinite(input.nowMs)) {
|
|
74
|
+
throw new RangeError("Widget layout transition plans require finite nowMs");
|
|
75
|
+
}
|
|
76
|
+
if (areWidgetLayoutSizeUpdatesEqual(input.previous, input.next)) {
|
|
77
|
+
return {
|
|
78
|
+
kind: "unchanged",
|
|
79
|
+
size: { ...input.next.size },
|
|
80
|
+
update: {
|
|
81
|
+
size: { ...input.next.size },
|
|
82
|
+
transition: { ...input.next.transition },
|
|
83
|
+
},
|
|
84
|
+
observedAtMs: input.nowMs,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
kind: "instant",
|
|
89
|
+
size: { ...input.next.size },
|
|
90
|
+
update: {
|
|
91
|
+
size: { ...input.next.size },
|
|
92
|
+
transition: { ...input.next.transition },
|
|
93
|
+
},
|
|
94
|
+
startedAtMs: input.nowMs,
|
|
95
|
+
settledAtMs: input.nowMs,
|
|
96
|
+
};
|
|
97
|
+
}
|