vargai 0.4.0-alpha49 → 0.4.0-alpha50

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 (42) hide show
  1. package/launch-videos/07-ugc-weight-loss.tsx +2 -6
  2. package/package.json +1 -1
  3. package/src/ai-sdk/file.ts +30 -0
  4. package/src/ai-sdk/providers/editly/editly.test.ts +240 -59
  5. package/src/ai-sdk/providers/editly/index.ts +74 -17
  6. package/src/ai-sdk/providers/editly/layers.ts +39 -7
  7. package/src/ai-sdk/providers/editly/rendi/index.ts +7 -0
  8. package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +26 -0
  9. package/src/ai-sdk/providers/editly/types.ts +2 -0
  10. package/src/cli/commands/frame.tsx +1 -1
  11. package/src/cli/commands/render.tsx +1 -1
  12. package/src/cli/commands/storyboard.tsx +1 -1
  13. package/src/react/elements.ts +0 -9
  14. package/src/react/examples/character-video.tsx +1 -1
  15. package/src/react/examples/grid.tsx +1 -1
  16. package/src/react/examples/quickstart-test.tsx +1 -1
  17. package/src/react/examples/split-layout-demo.tsx +6 -6
  18. package/src/react/examples/split.tsx +3 -3
  19. package/src/react/examples/video-grid.tsx +1 -1
  20. package/src/react/index.ts +5 -2
  21. package/src/react/layouts/grid.tsx +4 -0
  22. package/src/react/layouts/index.ts +1 -1
  23. package/src/react/layouts/layouts.test.ts +345 -0
  24. package/src/react/layouts/split.tsx +1 -18
  25. package/src/react/react.test.ts +46 -6
  26. package/src/react/render.ts +4 -4
  27. package/src/react/renderers/cache.test.ts +1 -0
  28. package/src/react/renderers/clip.ts +11 -15
  29. package/src/react/renderers/context.ts +1 -0
  30. package/src/react/renderers/image.ts +12 -1
  31. package/src/react/renderers/index.ts +0 -1
  32. package/src/react/renderers/music.ts +12 -2
  33. package/src/react/renderers/render.ts +9 -4
  34. package/src/react/renderers/speech.ts +12 -2
  35. package/src/react/renderers/video.ts +14 -2
  36. package/src/react/types.ts +13 -0
  37. package/src/react/warnings.test.ts +95 -0
  38. package/src/studio/step-renderer.ts +1 -0
  39. package/src/react/renderers/split.ts +0 -118
  40. package/test-slot-grid.tsx +0 -19
  41. package/test-slot-userland.tsx +0 -30
  42. package/test-sync-v2.tsx +0 -29
@@ -10,6 +10,32 @@ const mockStorage: StorageProvider = {
10
10
  },
11
11
  };
12
12
 
13
+ describe("rendi backend validation", () => {
14
+ test("throws error when inputs array is empty", async () => {
15
+ const backend = createRendiBackend({ storage: mockStorage });
16
+
17
+ await expect(
18
+ backend.run({
19
+ inputs: [],
20
+ outputArgs: ["-c:v", "libx264"],
21
+ outputPath: "output.mp4",
22
+ }),
23
+ ).rejects.toThrow("Rendi backend requires at least one input file");
24
+ });
25
+
26
+ test("throws error when inputs is undefined", async () => {
27
+ const backend = createRendiBackend({ storage: mockStorage });
28
+
29
+ await expect(
30
+ backend.run({
31
+ inputs: undefined as unknown as string[],
32
+ outputArgs: ["-c:v", "libx264"],
33
+ outputPath: "output.mp4",
34
+ }),
35
+ ).rejects.toThrow("Rendi backend requires at least one input file");
36
+ });
37
+ });
38
+
13
39
  describe.skipIf(!hasRendiKey)("rendi backend", () => {
14
40
  test("ffprobe remote file", async () => {
15
41
  const backend = createRendiBackend({ storage: mockStorage });
@@ -161,6 +161,8 @@ export interface ImageOverlayLayer extends BaseLayer, KenBurns {
161
161
  position?: Position;
162
162
  width?: SizeValue;
163
163
  height?: SizeValue;
164
+ resizeMode?: ResizeMode;
165
+ cropPosition?: CropPosition;
164
166
  }
165
167
 
166
168
  export interface TitleLayer extends TextLayer, KenBurns {
@@ -22,7 +22,7 @@ import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
22
22
  import { renderStatic } from "../ui/render.ts";
23
23
 
24
24
  const AUTO_IMPORTS = `/** @jsxImportSource vargai */
25
- import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid, SplitLayout } from "vargai/react";
25
+ import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid } from "vargai/react";
26
26
  import { fal, elevenlabs, replicate } from "vargai/ai";
27
27
  `;
28
28
 
@@ -10,7 +10,7 @@ import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
10
10
  import { renderStatic } from "../ui/render.ts";
11
11
 
12
12
  const AUTO_IMPORTS = `/** @jsxImportSource vargai */
13
- import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid, SplitLayout } from "vargai/react";
13
+ import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid } from "vargai/react";
14
14
  import { fal, elevenlabs, replicate } from "vargai/ai";
15
15
  `;
16
16
 
@@ -24,7 +24,7 @@ import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
24
24
  import { renderStatic } from "../ui/render.ts";
25
25
 
26
26
  const AUTO_IMPORTS = `/** @jsxImportSource vargai */
27
- import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid, SplitLayout } from "vargai/react";
27
+ import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid } from "vargai/react";
28
28
  import { fal, elevenlabs, replicate } from "vargai/ai";
29
29
  `;
30
30
 
@@ -8,7 +8,6 @@ import type {
8
8
  RenderProps,
9
9
  SliderProps,
10
10
  SpeechProps,
11
- SplitProps,
12
11
  SubtitleProps,
13
12
  SwipeProps,
14
13
  TalkingHeadProps,
@@ -112,14 +111,6 @@ export function Captions(props: CaptionsProps): VargElement<"captions"> {
112
111
  return createElement("captions", props as Record<string, unknown>, undefined);
113
112
  }
114
113
 
115
- export function Split(props: SplitProps): VargElement<"split"> {
116
- return createElement(
117
- "split",
118
- props as Record<string, unknown>,
119
- props.children,
120
- );
121
- }
122
-
123
114
  export function Slider(props: SliderProps): VargElement<"slider"> {
124
115
  return createElement(
125
116
  "slider",
@@ -80,7 +80,7 @@ async function main() {
80
80
  cache: ".cache/ai",
81
81
  });
82
82
 
83
- console.log(`\ndone! ${buffer.byteLength} bytes`);
83
+ console.log(`\ndone! ${buffer.video.byteLength} bytes`);
84
84
  console.log("output: output/react-madi.mp4");
85
85
  }
86
86
 
@@ -46,7 +46,7 @@ async function main() {
46
46
  cache: ".cache/ai",
47
47
  });
48
48
 
49
- console.log(`\ndone! ${buffer.byteLength} bytes`);
49
+ console.log(`\ndone! ${buffer.video.byteLength} bytes`);
50
50
  console.log("output: output/react-grid.mp4");
51
51
  }
52
52
 
@@ -77,7 +77,7 @@ async function main() {
77
77
 
78
78
  console.log("\n=== SUCCESS ===");
79
79
  console.log(
80
- `Output: output/quickstart-test.mp4 (${(buffer.byteLength / 1024 / 1024).toFixed(2)} MB)`,
80
+ `Output: output/quickstart-test.mp4 (${(buffer.video.byteLength / 1024 / 1024).toFixed(2)} MB)`,
81
81
  );
82
82
  console.log("\nYour setup is working! You can now:");
83
83
  console.log("1. Try the templates in .claude/skills/video-generation.md");
@@ -1,7 +1,7 @@
1
1
  /**
2
- * SplitLayout Demo - uses the layout helper (positions children in clip)
2
+ * Split Demo - uses the layout helper (positions children in clip)
3
3
  *
4
- * SplitLayout just adds position props to children, letting the clip handle rendering.
4
+ * Split just adds position props to children, letting the clip handle rendering.
5
5
  * Good for: when you want positioned images/videos within a clip alongside other layers
6
6
  */
7
7
  import {
@@ -11,13 +11,13 @@ import {
11
11
  Render,
12
12
  render,
13
13
  Slider,
14
- SplitLayout,
14
+ Split,
15
15
  Swipe,
16
16
  Title,
17
17
  } from "..";
18
18
 
19
19
  async function main() {
20
- console.log("SplitLayout Demo (uses layout helper)\n");
20
+ console.log("Split Demo (uses layout helper)\n");
21
21
 
22
22
  const img1 = Image({ src: "media/cyberpunk-street.png" });
23
23
  const img2 = Image({ src: "media/fal-coffee-shop.png" });
@@ -26,8 +26,8 @@ async function main() {
26
26
  const video = (
27
27
  <Render width={1280} height={720}>
28
28
  <Clip duration={3}>
29
- <SplitLayout direction="horizontal" left={img1} right={img2} />
30
- <Title position="bottom">SplitLayout (layout helper)</Title>
29
+ <Split direction="horizontal">{[img1, img2]}</Split>
30
+ <Title position="bottom">Split (layout helper)</Title>
31
31
  </Clip>
32
32
 
33
33
  <Clip duration={4} transition={{ name: "fade", duration: 0.5 }}>
@@ -1,5 +1,5 @@
1
1
  import { fal } from "../../ai-sdk/providers/fal";
2
- import { Clip, Image, Render, render, SplitLayout as Split, Title } from "..";
2
+ import { Clip, Image, Render, render, Split, Title } from "..";
3
3
 
4
4
  async function main() {
5
5
  console.log("creating before/after split screen...\n");
@@ -19,7 +19,7 @@ async function main() {
19
19
  const video = (
20
20
  <Render width={1920} height={1080}>
21
21
  <Clip duration={5}>
22
- <Split left={before} right={after} />
22
+ <Split>{[before, after]}</Split>
23
23
  <Title position="bottom" color="#ffffff">
24
24
  30 Day Transformation
25
25
  </Title>
@@ -34,7 +34,7 @@ async function main() {
34
34
  cache: ".cache/ai",
35
35
  });
36
36
 
37
- console.log(`\ndone! ${buffer.byteLength} bytes`);
37
+ console.log(`\ndone! ${buffer.video.byteLength} bytes`);
38
38
  console.log("output: output/react-split.mp4");
39
39
  }
40
40
 
@@ -39,7 +39,7 @@ async function main() {
39
39
  cache: ".cache/ai",
40
40
  });
41
41
 
42
- console.log(`\ndone! ${buffer.byteLength} bytes`);
42
+ console.log(`\ndone! ${buffer.video.byteLength} bytes`);
43
43
  console.log("output: output/react-video-grid.mp4");
44
44
  }
45
45
 
@@ -1,4 +1,5 @@
1
1
  export type { CacheStorage } from "../ai-sdk/cache";
2
+ export { File } from "../ai-sdk/file";
2
3
  export type { SizeValue } from "../ai-sdk/providers/editly/types";
3
4
  export { assets } from "./assets";
4
5
  export {
@@ -11,18 +12,19 @@ export {
11
12
  Render,
12
13
  Slider,
13
14
  Speech,
14
- Split,
15
15
  Subtitle,
16
16
  Swipe,
17
17
  TalkingHead,
18
18
  Title,
19
19
  Video,
20
20
  } from "./elements";
21
- export { Grid, Slot, SplitLayout } from "./layouts";
21
+ export { Grid, Slot, Split } from "./layouts";
22
22
  export { render, renderStream } from "./render";
23
23
  export type {
24
24
  CaptionsProps,
25
25
  ClipProps,
26
+ FileMetadata,
27
+ GeneratedFileType,
26
28
  ImageProps,
27
29
  MusicProps,
28
30
  OverlayProps,
@@ -30,6 +32,7 @@ export type {
30
32
  PositionProps,
31
33
  RenderOptions,
32
34
  RenderProps,
35
+ RenderResult,
33
36
  SliderProps,
34
37
  SpeechProps,
35
38
  SplitProps,
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource vargai */
1
2
  import type { VargElement } from "../types";
2
3
 
3
4
  export const Grid = ({
@@ -11,6 +12,9 @@ export const Grid = ({
11
12
  children: VargElement[];
12
13
  resize?: "cover" | "contain" | "stretch";
13
14
  }) => {
15
+ if (children.length === 0) {
16
+ return null;
17
+ }
14
18
  const cols = columns ?? children.length;
15
19
  const rowCount = rows ?? Math.ceil(children.length / cols);
16
20
  const positioned = children.map((el, i) => ({
@@ -1,3 +1,3 @@
1
1
  export { Grid } from "./grid";
2
2
  export { Slot } from "./slot";
3
- export { Split, SplitLayout } from "./split";
3
+ export { Split } from "./split";
@@ -0,0 +1,345 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Video } from "../index";
3
+ import type { VargElement } from "../types";
4
+ import { Grid } from "./grid";
5
+ import { Slot } from "./slot";
6
+ import { Split } from "./split";
7
+
8
+ // Grid/Split return arrays at runtime (JSX fragments serialize to arrays)
9
+ function asArray(result: unknown): VargElement[] {
10
+ return result as VargElement[];
11
+ }
12
+
13
+ describe("Slot", () => {
14
+ test("parses fit-cover class", () => {
15
+ const child = Video({ src: "test.mp4" });
16
+ const result = Slot({ class: "fit-cover", children: child });
17
+
18
+ expect(result.props.resize).toBe("cover");
19
+ });
20
+
21
+ test("parses fit-contain class", () => {
22
+ const child = Video({ src: "test.mp4" });
23
+ const result = Slot({ class: "fit-contain", children: child });
24
+
25
+ expect(result.props.resize).toBe("contain");
26
+ });
27
+
28
+ test("parses fit-contain-blur class", () => {
29
+ const child = Video({ src: "test.mp4" });
30
+ const result = Slot({ class: "fit-contain-blur", children: child });
31
+
32
+ expect(result.props.resize).toBe("contain-blur");
33
+ });
34
+
35
+ test("parses fit-fill class as stretch", () => {
36
+ const child = Video({ src: "test.mp4" });
37
+ const result = Slot({ class: "fit-fill", children: child });
38
+
39
+ expect(result.props.resize).toBe("stretch");
40
+ });
41
+
42
+ test("parses pos-top class", () => {
43
+ const child = Video({ src: "test.mp4" });
44
+ const result = Slot({ class: "pos-top", children: child });
45
+
46
+ expect(result.props.cropPosition).toBe("top");
47
+ });
48
+
49
+ test("parses pos-bottom class", () => {
50
+ const child = Video({ src: "test.mp4" });
51
+ const result = Slot({ class: "pos-bottom", children: child });
52
+
53
+ expect(result.props.cropPosition).toBe("bottom");
54
+ });
55
+
56
+ test("parses pos-left class", () => {
57
+ const child = Video({ src: "test.mp4" });
58
+ const result = Slot({ class: "pos-left", children: child });
59
+
60
+ expect(result.props.cropPosition).toBe("left");
61
+ });
62
+
63
+ test("parses pos-right class", () => {
64
+ const child = Video({ src: "test.mp4" });
65
+ const result = Slot({ class: "pos-right", children: child });
66
+
67
+ expect(result.props.cropPosition).toBe("right");
68
+ });
69
+
70
+ test("parses compound position classes", () => {
71
+ const positions = [
72
+ "pos-top-left",
73
+ "pos-top-right",
74
+ "pos-bottom-left",
75
+ "pos-bottom-right",
76
+ ] as const;
77
+
78
+ for (const pos of positions) {
79
+ const child = Video({ src: "test.mp4" });
80
+ const result = Slot({ class: pos, children: child });
81
+ expect(result.props.cropPosition).toBe(pos.replace("pos-", ""));
82
+ }
83
+ });
84
+
85
+ test("parses combined fit and position classes", () => {
86
+ const child = Video({ src: "test.mp4" });
87
+ const result = Slot({ class: "fit-cover pos-top", children: child });
88
+
89
+ expect(result.props.resize).toBe("cover");
90
+ expect(result.props.cropPosition).toBe("top");
91
+ });
92
+
93
+ test("explicit props override class", () => {
94
+ const child = Video({ src: "test.mp4" });
95
+ const result = Slot({
96
+ class: "fit-cover pos-top",
97
+ fit: "contain",
98
+ position: "bottom",
99
+ children: child,
100
+ });
101
+
102
+ expect(result.props.resize).toBe("contain");
103
+ expect(result.props.cropPosition).toBe("bottom");
104
+ });
105
+
106
+ test("defaults to cover and center when no class or props", () => {
107
+ const child = Video({ src: "test.mp4" });
108
+ const result = Slot({ children: child });
109
+
110
+ expect(result.props.resize).toBe("cover");
111
+ expect(result.props.cropPosition).toBe("center");
112
+ });
113
+
114
+ test("preserves child props", () => {
115
+ const child = Video({ src: "test.mp4", volume: 0.5 });
116
+ const result = Slot({ class: "fit-cover pos-top", children: child });
117
+
118
+ expect(result.props.src).toBe("test.mp4");
119
+ expect(result.props.volume).toBe(0.5);
120
+ expect(result.type).toBe("video");
121
+ });
122
+ });
123
+
124
+ describe("Split", () => {
125
+ test("horizontal split positions children side by side", () => {
126
+ const children = [
127
+ Video({ src: "a.mp4" }),
128
+ Video({ src: "b.mp4" }),
129
+ ] as VargElement[];
130
+
131
+ const result = asArray(Split({ direction: "horizontal", children }));
132
+
133
+ expect(result.length).toBe(2);
134
+ expect(result[0]?.props.left).toBe("0%");
135
+ expect(result[0]?.props.width).toBe("50%");
136
+ expect(result[1]?.props.left).toBe("50%");
137
+ expect(result[1]?.props.width).toBe("50%");
138
+ });
139
+
140
+ test("vertical split stacks children", () => {
141
+ const children = [
142
+ Video({ src: "a.mp4" }),
143
+ Video({ src: "b.mp4" }),
144
+ ] as VargElement[];
145
+
146
+ const result = asArray(Split({ direction: "vertical", children }));
147
+
148
+ expect(result.length).toBe(2);
149
+ expect(result[0]?.props.top).toBe("0%");
150
+ expect(result[0]?.props.height).toBe("50%");
151
+ expect(result[1]?.props.top).toBe("50%");
152
+ expect(result[1]?.props.height).toBe("50%");
153
+ });
154
+
155
+ test("returns null for empty children", () => {
156
+ const result = Split({ children: [] });
157
+ expect(result).toBeNull();
158
+ });
159
+
160
+ test("defaults to horizontal", () => {
161
+ const children = [
162
+ Video({ src: "a.mp4" }),
163
+ Video({ src: "b.mp4" }),
164
+ ] as VargElement[];
165
+
166
+ const result = asArray(Split({ children }));
167
+
168
+ expect(result[0]?.props.left).toBe("0%");
169
+ expect(result[1]?.props.left).toBe("50%");
170
+ });
171
+ });
172
+
173
+ describe("Grid", () => {
174
+ test("2x1 grid positions children horizontally", () => {
175
+ const children = [
176
+ Video({ src: "a.mp4" }),
177
+ Video({ src: "b.mp4" }),
178
+ ] as VargElement[];
179
+
180
+ const result = asArray(Grid({ columns: 2, rows: 1, children }));
181
+
182
+ expect(result[0]?.props.left).toBe("0%");
183
+ expect(result[0]?.props.top).toBe("0%");
184
+ expect(result[0]?.props.width).toBe("50%");
185
+ expect(result[0]?.props.height).toBe("100%");
186
+
187
+ expect(result[1]?.props.left).toBe("50%");
188
+ expect(result[1]?.props.top).toBe("0%");
189
+ expect(result[1]?.props.width).toBe("50%");
190
+ expect(result[1]?.props.height).toBe("100%");
191
+ });
192
+
193
+ test("1x2 grid positions children vertically", () => {
194
+ const children = [
195
+ Video({ src: "a.mp4" }),
196
+ Video({ src: "b.mp4" }),
197
+ ] as VargElement[];
198
+
199
+ const result = asArray(Grid({ columns: 1, rows: 2, children }));
200
+
201
+ expect(result[0]?.props.left).toBe("0%");
202
+ expect(result[0]?.props.top).toBe("0%");
203
+ expect(result[0]?.props.width).toBe("100%");
204
+ expect(result[0]?.props.height).toBe("50%");
205
+
206
+ expect(result[1]?.props.left).toBe("0%");
207
+ expect(result[1]?.props.top).toBe("50%");
208
+ expect(result[1]?.props.width).toBe("100%");
209
+ expect(result[1]?.props.height).toBe("50%");
210
+ });
211
+
212
+ test("2x2 grid positions children in grid pattern", () => {
213
+ const children = [
214
+ Video({ src: "a.mp4" }),
215
+ Video({ src: "b.mp4" }),
216
+ Video({ src: "c.mp4" }),
217
+ Video({ src: "d.mp4" }),
218
+ ] as VargElement[];
219
+
220
+ const result = asArray(Grid({ columns: 2, rows: 2, children }));
221
+
222
+ expect(result[0]?.props.left).toBe("0%");
223
+ expect(result[0]?.props.top).toBe("0%");
224
+
225
+ expect(result[1]?.props.left).toBe("50%");
226
+ expect(result[1]?.props.top).toBe("0%");
227
+
228
+ expect(result[2]?.props.left).toBe("0%");
229
+ expect(result[2]?.props.top).toBe("50%");
230
+
231
+ expect(result[3]?.props.left).toBe("50%");
232
+ expect(result[3]?.props.top).toBe("50%");
233
+ });
234
+
235
+ test("defaults resize to contain", () => {
236
+ const children = [Video({ src: "a.mp4" })] as VargElement[];
237
+ const result = asArray(Grid({ columns: 1, children }));
238
+
239
+ expect(result[0]?.props.resize).toBe("contain");
240
+ });
241
+
242
+ test("child resize overrides grid default", () => {
243
+ const child = Video({ src: "a.mp4" });
244
+ (child.props as Record<string, unknown>).resize = "cover";
245
+
246
+ const result = asArray(Grid({ columns: 1, children: [child] }));
247
+
248
+ expect(result[0]?.props.resize).toBe("cover");
249
+ });
250
+
251
+ test("infers columns from children length when not specified", () => {
252
+ const children = [
253
+ Video({ src: "a.mp4" }),
254
+ Video({ src: "b.mp4" }),
255
+ Video({ src: "c.mp4" }),
256
+ ] as VargElement[];
257
+
258
+ const result = asArray(Grid({ children }));
259
+
260
+ expect((result[0]?.props.width as string).startsWith("33.33")).toBe(true);
261
+ expect((result[1]?.props.width as string).startsWith("33.33")).toBe(true);
262
+ expect((result[2]?.props.width as string).startsWith("33.33")).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe("Split + Slot integration", () => {
267
+ test("vertical split with Slot applies fit and position", () => {
268
+ const video1 = Video({ src: "a.mp4" });
269
+ const video2 = Video({ src: "b.mp4" });
270
+
271
+ const slot1 = Slot({ class: "fit-cover pos-top", children: video1 });
272
+ const slot2 = Slot({ class: "fit-cover pos-bottom", children: video2 });
273
+
274
+ const result = asArray(
275
+ Split({ direction: "vertical", children: [slot1, slot2] }),
276
+ );
277
+
278
+ expect(result[0]?.props.top).toBe("0%");
279
+ expect(result[0]?.props.height).toBe("50%");
280
+ expect(result[0]?.props.resize).toBe("cover");
281
+ expect(result[0]?.props.cropPosition).toBe("top");
282
+
283
+ expect(result[1]?.props.top).toBe("50%");
284
+ expect(result[1]?.props.height).toBe("50%");
285
+ expect(result[1]?.props.resize).toBe("cover");
286
+ expect(result[1]?.props.cropPosition).toBe("bottom");
287
+ });
288
+
289
+ test("horizontal split with mixed Slot positions", () => {
290
+ const video1 = Video({ src: "a.mp4" });
291
+ const video2 = Video({ src: "b.mp4" });
292
+
293
+ const slot1 = Slot({ class: "fit-cover pos-left", children: video1 });
294
+ const slot2 = Slot({ class: "fit-contain pos-right", children: video2 });
295
+
296
+ const result = asArray(
297
+ Split({ direction: "horizontal", children: [slot1, slot2] }),
298
+ );
299
+
300
+ expect(result[0]?.props.resize).toBe("cover");
301
+ expect(result[0]?.props.cropPosition).toBe("left");
302
+
303
+ expect(result[1]?.props.resize).toBe("contain");
304
+ expect(result[1]?.props.cropPosition).toBe("right");
305
+ });
306
+ });
307
+
308
+ describe("Grid + Slot integration", () => {
309
+ test("2x2 grid with different Slot configurations", () => {
310
+ const videos = [
311
+ Video({ src: "a.mp4" }),
312
+ Video({ src: "b.mp4" }),
313
+ Video({ src: "c.mp4" }),
314
+ Video({ src: "d.mp4" }),
315
+ ];
316
+
317
+ const slots = [
318
+ Slot({
319
+ class: "fit-cover pos-top-left",
320
+ children: videos[0] as VargElement,
321
+ }),
322
+ Slot({
323
+ class: "fit-cover pos-top-right",
324
+ children: videos[1] as VargElement,
325
+ }),
326
+ Slot({
327
+ class: "fit-contain pos-bottom-left",
328
+ children: videos[2] as VargElement,
329
+ }),
330
+ Slot({
331
+ class: "fit-fill pos-bottom-right",
332
+ children: videos[3] as VargElement,
333
+ }),
334
+ ];
335
+
336
+ const result = asArray(Grid({ columns: 2, rows: 2, children: slots }));
337
+
338
+ expect(result[0]?.props.cropPosition).toBe("top-left");
339
+ expect(result[1]?.props.cropPosition).toBe("top-right");
340
+ expect(result[2]?.props.cropPosition).toBe("bottom-left");
341
+ expect(result[2]?.props.resize).toBe("contain");
342
+ expect(result[3]?.props.cropPosition).toBe("bottom-right");
343
+ expect(result[3]?.props.resize).toBe("stretch");
344
+ });
345
+ });
@@ -1,24 +1,7 @@
1
+ /** @jsxImportSource vargai */
1
2
  import type { VargElement } from "../types";
2
3
  import { Grid } from "./grid";
3
4
 
4
- export const SplitLayout = ({
5
- left,
6
- right,
7
- direction = "horizontal",
8
- }: {
9
- left: VargElement;
10
- right: VargElement;
11
- direction?: "horizontal" | "vertical";
12
- }) => (
13
- <Grid
14
- columns={direction === "horizontal" ? 2 : 1}
15
- rows={direction === "vertical" ? 2 : 1}
16
- >
17
- {left}
18
- {right}
19
- </Grid>
20
- );
21
-
22
5
  export const Split = ({
23
6
  direction = "horizontal",
24
7
  children,