@vibecuting/video-project-core 0.1.26 → 0.1.27

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 (213) hide show
  1. package/package.json +4 -4
  2. package/src/core/intro/_shared/intro-layout.test.ts +18 -0
  3. package/src/core/intro/_shared/intro-layout.ts +50 -0
  4. package/src/core/intro/cyberpunk-intro-holo/default-cyberpunk-intro-bumper-Horizontal.tsx +25 -0
  5. package/src/core/intro/cyberpunk-intro-holo/default-cyberpunk-intro-bumper-Landscape.tsx +25 -0
  6. package/src/core/intro/cyberpunk-intro-holo/default-cyberpunk-intro-bumper.stories.tsx +31 -0
  7. package/src/core/intro/cyberpunk-intro-holo/default-cyberpunk-intro-bumper.tsx +118 -0
  8. package/src/core/intro/cyberpunk-intro-holo/index.ts +11 -0
  9. package/src/core/intro/cyberpunk-intro-neon/default-cyberpunk-intro-bumper-Horizontal.tsx +24 -0
  10. package/src/core/intro/cyberpunk-intro-neon/default-cyberpunk-intro-bumper-Landscape.tsx +24 -0
  11. package/src/core/intro/cyberpunk-intro-neon/default-cyberpunk-intro-bumper.stories.tsx +30 -0
  12. package/src/core/intro/cyberpunk-intro-neon/default-cyberpunk-intro-bumper.tsx +108 -0
  13. package/src/core/intro/cyberpunk-intro-neon/index.ts +10 -0
  14. package/src/core/intro/default-cinematic-intro-bumper/default-cinematic-intro-bumper-Horizontal.tsx +24 -0
  15. package/src/core/intro/default-cinematic-intro-bumper/default-cinematic-intro-bumper-Landscape.tsx +24 -0
  16. package/src/core/intro/default-cinematic-intro-bumper/default-cinematic-intro-bumper.stories.tsx +29 -0
  17. package/src/core/intro/default-cinematic-intro-bumper/default-cinematic-intro-bumper.tsx +234 -0
  18. package/src/core/intro/default-cinematic-intro-bumper/index.ts +11 -0
  19. package/src/core/intro/default-festival-intro-bumper/default-festival-intro-bumper-Horizontal.tsx +24 -0
  20. package/src/core/intro/default-festival-intro-bumper/default-festival-intro-bumper-Landscape.tsx +24 -0
  21. package/src/core/intro/default-festival-intro-bumper/default-festival-intro-bumper.stories.tsx +29 -0
  22. package/src/core/intro/default-festival-intro-bumper/default-festival-intro-bumper.tsx +187 -0
  23. package/src/core/intro/default-festival-intro-bumper/index.ts +11 -0
  24. package/src/core/intro/default-intro-bumper/default-intro-bumper-Horizontal.tsx +24 -0
  25. package/src/core/intro/default-intro-bumper/default-intro-bumper-Landscape.tsx +24 -0
  26. package/src/core/intro/default-intro-bumper/default-intro-bumper.stories.tsx +41 -0
  27. package/src/core/intro/default-intro-bumper/default-intro-bumper.tsx +256 -0
  28. package/src/core/intro/default-intro-bumper/index.ts +11 -0
  29. package/src/core/intro/default-noir-intro-bumper/default-noir-intro-bumper-Horizontal.tsx +24 -0
  30. package/src/core/intro/default-noir-intro-bumper/default-noir-intro-bumper-Landscape.tsx +24 -0
  31. package/src/core/intro/default-noir-intro-bumper/default-noir-intro-bumper.stories.tsx +29 -0
  32. package/src/core/intro/default-noir-intro-bumper/default-noir-intro-bumper.tsx +122 -0
  33. package/src/core/intro/default-noir-intro-bumper/index.ts +11 -0
  34. package/src/core/intro/default-trailer-intro-bumper/default-trailer-intro-bumper-Horizontal.tsx +24 -0
  35. package/src/core/intro/default-trailer-intro-bumper/default-trailer-intro-bumper-Landscape.tsx +24 -0
  36. package/src/core/intro/default-trailer-intro-bumper/default-trailer-intro-bumper.stories.tsx +29 -0
  37. package/src/core/intro/default-trailer-intro-bumper/default-trailer-intro-bumper.tsx +181 -0
  38. package/src/core/intro/default-trailer-intro-bumper/index.ts +11 -0
  39. package/src/core/intro/documentary-intro-observation/default-documentary-intro-bumper-Horizontal.tsx +18 -0
  40. package/src/core/intro/documentary-intro-observation/default-documentary-intro-bumper-Landscape.tsx +18 -0
  41. package/src/core/intro/documentary-intro-observation/default-documentary-intro-bumper.stories.tsx +31 -0
  42. package/src/core/intro/documentary-intro-observation/default-documentary-intro-bumper.tsx +158 -0
  43. package/src/core/intro/documentary-intro-observation/index.ts +10 -0
  44. package/src/core/intro/family-intro-reunion/default-family-intro-bumper-Horizontal.tsx +18 -0
  45. package/src/core/intro/family-intro-reunion/default-family-intro-bumper-Landscape.tsx +18 -0
  46. package/src/core/intro/family-intro-reunion/default-family-intro-bumper.stories.tsx +43 -0
  47. package/src/core/intro/family-intro-reunion/default-family-intro-bumper.tsx +165 -0
  48. package/src/core/intro/family-intro-reunion/index.ts +10 -0
  49. package/src/core/intro/festival-intro-spotlight/default-festival-intro-bumper-Horizontal.tsx +1 -0
  50. package/src/core/intro/festival-intro-spotlight/default-festival-intro-bumper-Landscape.tsx +1 -0
  51. package/src/core/intro/festival-intro-spotlight/default-festival-intro-bumper.stories.tsx +30 -0
  52. package/src/core/intro/festival-intro-spotlight/default-festival-intro-bumper.tsx +105 -0
  53. package/src/core/intro/festival-intro-spotlight/index.ts +10 -0
  54. package/src/core/intro/index.test.ts +45 -0
  55. package/src/core/intro/index.ts +17 -0
  56. package/src/core/intro/noir-intro-city/default-noir-intro-bumper-Horizontal.tsx +24 -0
  57. package/src/core/intro/noir-intro-city/default-noir-intro-bumper-Landscape.tsx +24 -0
  58. package/src/core/intro/noir-intro-city/default-noir-intro-bumper.stories.tsx +31 -0
  59. package/src/core/intro/noir-intro-city/default-noir-intro-bumper.tsx +96 -0
  60. package/src/core/intro/noir-intro-city/index.ts +10 -0
  61. package/src/core/intro/noir-intro-deluxe/default-noir-intro-bumper-Horizontal.tsx +1 -0
  62. package/src/core/intro/noir-intro-deluxe/default-noir-intro-bumper-Landscape.tsx +1 -0
  63. package/src/core/intro/noir-intro-deluxe/default-noir-intro-bumper.stories.tsx +31 -0
  64. package/src/core/intro/noir-intro-deluxe/default-noir-intro-bumper.tsx +117 -0
  65. package/src/core/intro/noir-intro-deluxe/index.ts +10 -0
  66. package/src/core/intro/sci-fi-intro/default-sci-fi-intro-bumper-Horizontal.tsx +1 -0
  67. package/src/core/intro/sci-fi-intro/default-sci-fi-intro-bumper-Landscape.tsx +1 -0
  68. package/src/core/intro/sci-fi-intro/default-sci-fi-intro-bumper.stories.tsx +48 -0
  69. package/src/core/intro/sci-fi-intro/default-sci-fi-intro-bumper.tsx +105 -0
  70. package/src/core/intro/sci-fi-intro/index.ts +10 -0
  71. package/src/core/intro/thriller-intro-suspense/default-thriller-intro-bumper-Horizontal.tsx +18 -0
  72. package/src/core/intro/thriller-intro-suspense/default-thriller-intro-bumper-Landscape.tsx +18 -0
  73. package/src/core/intro/thriller-intro-suspense/default-thriller-intro-bumper.stories.tsx +31 -0
  74. package/src/core/intro/thriller-intro-suspense/default-thriller-intro-bumper.tsx +155 -0
  75. package/src/core/intro/thriller-intro-suspense/index.ts +10 -0
  76. package/src/core/intro/trailer-intro-premiere/default-trailer-intro-bumper-Horizontal.tsx +1 -0
  77. package/src/core/intro/trailer-intro-premiere/default-trailer-intro-bumper-Landscape.tsx +1 -0
  78. package/src/core/intro/trailer-intro-premiere/default-trailer-intro-bumper.stories.tsx +30 -0
  79. package/src/core/intro/trailer-intro-premiere/default-trailer-intro-bumper.tsx +104 -0
  80. package/src/core/intro/trailer-intro-premiere/index.ts +10 -0
  81. package/src/core/intro/travel-intro-journey/default-travel-intro-bumper-Horizontal.tsx +18 -0
  82. package/src/core/intro/travel-intro-journey/default-travel-intro-bumper-Landscape.tsx +18 -0
  83. package/src/core/intro/travel-intro-journey/default-travel-intro-bumper.stories.tsx +43 -0
  84. package/src/core/intro/travel-intro-journey/default-travel-intro-bumper.tsx +158 -0
  85. package/src/core/intro/travel-intro-journey/index.ts +10 -0
  86. package/src/core/intro/western-intro/default-western-intro-bumper-Horizontal.tsx +24 -0
  87. package/src/core/intro/western-intro/default-western-intro-bumper-Landscape.tsx +24 -0
  88. package/src/core/intro/western-intro/default-western-intro-bumper.stories.tsx +29 -0
  89. package/src/core/intro/western-intro/default-western-intro-bumper.tsx +146 -0
  90. package/src/core/intro/western-intro/index.ts +4 -0
  91. package/src/core/intro/western-intro-gangster/default-western-intro-bumper-Horizontal.tsx +25 -0
  92. package/src/core/intro/western-intro-gangster/default-western-intro-bumper-Landscape.tsx +25 -0
  93. package/src/core/intro/western-intro-gangster/default-western-intro-bumper.stories.tsx +31 -0
  94. package/src/core/intro/western-intro-gangster/default-western-intro-bumper.tsx +105 -0
  95. package/src/core/intro/western-intro-gangster/index.ts +11 -0
  96. package/src/core/outro/_shared/outro-layout.test.ts +32 -0
  97. package/src/core/outro/_shared/outro-layout.ts +72 -0
  98. package/src/core/outro/award-outro-celebration/default-award-outro-bumper-Horizontal.tsx +18 -0
  99. package/src/core/outro/award-outro-celebration/default-award-outro-bumper-Landscape.tsx +18 -0
  100. package/src/core/outro/award-outro-celebration/default-award-outro-bumper.stories.tsx +42 -0
  101. package/src/core/outro/award-outro-celebration/default-award-outro-bumper.tsx +152 -0
  102. package/src/core/outro/award-outro-celebration/index.ts +10 -0
  103. package/src/core/outro/blackout-outro-finale/default-blackout-outro-bumper-Horizontal.tsx +1 -0
  104. package/src/core/outro/blackout-outro-finale/default-blackout-outro-bumper-Landscape.tsx +1 -0
  105. package/src/core/outro/blackout-outro-finale/default-blackout-outro-bumper.stories.tsx +28 -0
  106. package/src/core/outro/blackout-outro-finale/default-blackout-outro-bumper.tsx +71 -0
  107. package/src/core/outro/blackout-outro-finale/index.ts +10 -0
  108. package/src/core/outro/credits-roll-outro-epic/default-credits-roll-outro-Horizontal.tsx +24 -0
  109. package/src/core/outro/credits-roll-outro-epic/default-credits-roll-outro-Landscape.tsx +24 -0
  110. package/src/core/outro/credits-roll-outro-epic/default-credits-roll-outro.stories.tsx +31 -0
  111. package/src/core/outro/credits-roll-outro-epic/default-credits-roll-outro.tsx +106 -0
  112. package/src/core/outro/credits-roll-outro-epic/index.ts +10 -0
  113. package/src/core/outro/credits-roll-outro-premium/default-credits-roll-outro-Horizontal.tsx +1 -0
  114. package/src/core/outro/credits-roll-outro-premium/default-credits-roll-outro-Landscape.tsx +1 -0
  115. package/src/core/outro/credits-roll-outro-premium/default-credits-roll-outro.stories.tsx +30 -0
  116. package/src/core/outro/credits-roll-outro-premium/default-credits-roll-outro.tsx +88 -0
  117. package/src/core/outro/credits-roll-outro-premium/index.ts +10 -0
  118. package/src/core/outro/credits-roll-outro-royal/default-credits-roll-outro-Horizontal.tsx +25 -0
  119. package/src/core/outro/credits-roll-outro-royal/default-credits-roll-outro-Landscape.tsx +25 -0
  120. package/src/core/outro/credits-roll-outro-royal/default-credits-roll-outro.stories.tsx +32 -0
  121. package/src/core/outro/credits-roll-outro-royal/default-credits-roll-outro.tsx +110 -0
  122. package/src/core/outro/credits-roll-outro-royal/index.ts +11 -0
  123. package/src/core/outro/default-blackout-outro-bumper/default-blackout-outro-bumper-Horizontal.tsx +22 -0
  124. package/src/core/outro/default-blackout-outro-bumper/default-blackout-outro-bumper-Landscape.tsx +22 -0
  125. package/src/core/outro/default-blackout-outro-bumper/default-blackout-outro-bumper.stories.tsx +48 -0
  126. package/src/core/outro/default-blackout-outro-bumper/default-blackout-outro-bumper.tsx +124 -0
  127. package/src/core/outro/default-blackout-outro-bumper/index.ts +11 -0
  128. package/src/core/outro/default-credits-roll-outro/default-credits-roll-outro-Horizontal.tsx +22 -0
  129. package/src/core/outro/default-credits-roll-outro/default-credits-roll-outro-Landscape.tsx +22 -0
  130. package/src/core/outro/default-credits-roll-outro/default-credits-roll-outro.stories.tsx +48 -0
  131. package/src/core/outro/default-credits-roll-outro/default-credits-roll-outro.tsx +208 -0
  132. package/src/core/outro/default-credits-roll-outro/index.ts +11 -0
  133. package/src/core/outro/default-epilogue-outro/default-epilogue-outro-Horizontal.tsx +22 -0
  134. package/src/core/outro/default-epilogue-outro/default-epilogue-outro-Landscape.tsx +22 -0
  135. package/src/core/outro/default-epilogue-outro/default-epilogue-outro.stories.tsx +48 -0
  136. package/src/core/outro/default-epilogue-outro/default-epilogue-outro.tsx +115 -0
  137. package/src/core/outro/default-epilogue-outro/index.ts +11 -0
  138. package/src/core/outro/default-outro-bumper/default-outro-bumper-Horizontal.tsx +22 -0
  139. package/src/core/outro/default-outro-bumper/default-outro-bumper-Landscape.tsx +22 -0
  140. package/src/core/outro/default-outro-bumper/default-outro-bumper.stories.tsx +48 -0
  141. package/src/core/outro/default-outro-bumper/default-outro-bumper.tsx +159 -0
  142. package/src/core/outro/default-outro-bumper/index.ts +11 -0
  143. package/src/core/outro/default-sunset-outro-bumper/default-sunset-outro-bumper-Horizontal.tsx +22 -0
  144. package/src/core/outro/default-sunset-outro-bumper/default-sunset-outro-bumper-Landscape.tsx +22 -0
  145. package/src/core/outro/default-sunset-outro-bumper/default-sunset-outro-bumper.stories.tsx +48 -0
  146. package/src/core/outro/default-sunset-outro-bumper/default-sunset-outro-bumper.tsx +183 -0
  147. package/src/core/outro/default-sunset-outro-bumper/index.ts +11 -0
  148. package/src/core/outro/festival-outro-encore/default-festival-outro-bumper-Horizontal.tsx +18 -0
  149. package/src/core/outro/festival-outro-encore/default-festival-outro-bumper-Landscape.tsx +18 -0
  150. package/src/core/outro/festival-outro-encore/default-festival-outro-bumper.stories.tsx +31 -0
  151. package/src/core/outro/festival-outro-encore/default-festival-outro-bumper.tsx +130 -0
  152. package/src/core/outro/festival-outro-encore/index.ts +10 -0
  153. package/src/core/outro/heist-outro/default-heist-outro-bumper-Horizontal.tsx +24 -0
  154. package/src/core/outro/heist-outro/default-heist-outro-bumper-Landscape.tsx +24 -0
  155. package/src/core/outro/heist-outro/default-heist-outro-bumper.stories.tsx +29 -0
  156. package/src/core/outro/heist-outro/default-heist-outro-bumper.tsx +160 -0
  157. package/src/core/outro/heist-outro/index.ts +4 -0
  158. package/src/core/outro/index.test.ts +88 -0
  159. package/src/core/outro/index.ts +73 -0
  160. package/src/core/outro/memoir-outro-reflection/default-memoir-outro-bumper-Horizontal.tsx +18 -0
  161. package/src/core/outro/memoir-outro-reflection/default-memoir-outro-bumper-Landscape.tsx +18 -0
  162. package/src/core/outro/memoir-outro-reflection/default-memoir-outro-bumper.stories.tsx +42 -0
  163. package/src/core/outro/memoir-outro-reflection/default-memoir-outro-bumper.tsx +158 -0
  164. package/src/core/outro/memoir-outro-reflection/index.ts +10 -0
  165. package/src/core/outro/romance-outro/default-romance-outro-bumper-Horizontal.tsx +1 -0
  166. package/src/core/outro/romance-outro/default-romance-outro-bumper-Landscape.tsx +1 -0
  167. package/src/core/outro/romance-outro/default-romance-outro-bumper.stories.tsx +48 -0
  168. package/src/core/outro/romance-outro/default-romance-outro-bumper.tsx +123 -0
  169. package/src/core/outro/romance-outro/index.ts +10 -0
  170. package/src/core/outro/sunset-outro-coda/default-sunset-outro-bumper-Horizontal.tsx +1 -0
  171. package/src/core/outro/sunset-outro-coda/default-sunset-outro-bumper-Landscape.tsx +1 -0
  172. package/src/core/outro/sunset-outro-coda/default-sunset-outro-bumper.stories.tsx +30 -0
  173. package/src/core/outro/sunset-outro-coda/default-sunset-outro-bumper.tsx +75 -0
  174. package/src/core/outro/sunset-outro-coda/index.ts +10 -0
  175. package/src/core/scene/slide/_shared/index.ts +27 -0
  176. package/src/core/scene/slide/_shared/media-templates.test.ts +47 -0
  177. package/src/core/scene/slide/_shared/media-templates.tsx +324 -0
  178. package/src/core/scene/slide/index.test.ts +28 -0
  179. package/src/core/scene/slide/index.ts +14 -0
  180. package/src/core/scene/slide/person-intro/default-person-intro-duo-slide.tsx +19 -0
  181. package/src/core/scene/slide/person-intro/default-person-intro-group-slide.tsx +168 -0
  182. package/src/core/scene/slide/person-intro/default-person-intro-quad-slide.tsx +19 -0
  183. package/src/core/scene/slide/person-intro/default-person-intro-slide-Horizontal.animated.stories.tsx +47 -0
  184. package/src/core/scene/slide/person-intro/default-person-intro-slide-Horizontal.stories.tsx +46 -0
  185. package/src/core/scene/slide/person-intro/default-person-intro-slide-Horizontal.tsx +302 -0
  186. package/src/core/scene/slide/person-intro/default-person-intro-slide-Landscape.tsx +20 -0
  187. package/src/core/scene/slide/person-intro/default-person-intro-slide.animated.stories.tsx +126 -0
  188. package/src/core/scene/slide/person-intro/default-person-intro-slide.stories.tsx +119 -0
  189. package/src/core/scene/slide/person-intro/default-person-intro-slide.tsx +363 -0
  190. package/src/core/scene/slide/person-intro/default-person-intro-trio-slide.tsx +19 -0
  191. package/src/core/scene/slide/person-intro/index.ts +8 -0
  192. package/src/core/scene/slide/photo-album/default-photo-album-multi-slide.tsx +19 -0
  193. package/src/core/scene/slide/photo-album/default-photo-album-pair-slide.tsx +19 -0
  194. package/src/core/scene/slide/photo-album/default-photo-album-single-slide.tsx +19 -0
  195. package/src/core/scene/slide/photo-album/default-photo-album-slide-Horizontal.animated.stories.tsx +26 -0
  196. package/src/core/scene/slide/photo-album/default-photo-album-slide-Horizontal.stories.tsx +25 -0
  197. package/src/core/scene/slide/photo-album/default-photo-album-slide-Horizontal.tsx +20 -0
  198. package/src/core/scene/slide/photo-album/default-photo-album-slide-Landscape.tsx +20 -0
  199. package/src/core/scene/slide/photo-album/default-photo-album-slide.animated.stories.tsx +94 -0
  200. package/src/core/scene/slide/photo-album/default-photo-album-slide.stories.tsx +87 -0
  201. package/src/core/scene/slide/photo-album/default-photo-album-slide.tsx +414 -0
  202. package/src/core/scene/slide/photo-album/index.ts +7 -0
  203. package/src/core/scene/slide/video-showcase/default-video-showcase-multi-slide.tsx +195 -0
  204. package/src/core/scene/slide/video-showcase/default-video-showcase-slide-Horizontal.tsx +212 -0
  205. package/src/core/scene/slide/video-showcase/default-video-showcase-slide-Landscape.tsx +20 -0
  206. package/src/core/scene/slide/video-showcase/default-video-showcase-slide.animated.stories.tsx +78 -0
  207. package/src/core/scene/slide/video-showcase/default-video-showcase-slide.stories.tsx +74 -0
  208. package/src/core/scene/slide/video-showcase/default-video-showcase-slide.tsx +365 -0
  209. package/src/core/scene/slide/video-showcase/index.ts +5 -0
  210. package/src/core/intro/default-intro-bumper.stories.tsx +0 -19
  211. package/src/core/intro/default-intro-bumper.tsx +0 -218
  212. package/src/core/outro/default-outro-bumper.stories.tsx +0 -19
  213. package/src/core/outro/default-outro-bumper.tsx +0 -218
@@ -0,0 +1,324 @@
1
+ import type { CSSProperties, ReactNode } from 'react'
2
+
3
+ export type MediaTemplateRuntime = {
4
+ width: number
5
+ height: number
6
+ }
7
+
8
+ export type MediaTileStyle = {
9
+ borderRadius: number
10
+ border: string
11
+ background: string
12
+ boxShadow?: string
13
+ }
14
+
15
+ export type PhotoGridLayout = {
16
+ columns: number
17
+ rows: number
18
+ gap: number
19
+ cardPaddingX: number
20
+ cardPaddingY: number
21
+ cardMinHeight: number
22
+ mediaAspectRatio: string
23
+ titleLines: number
24
+ captionLines: number
25
+ visibleItems: number
26
+ }
27
+
28
+ export type VideoPosterRailLayout = {
29
+ railDirection: 'row' | 'column'
30
+ columns: number
31
+ rows: number
32
+ gap: number
33
+ cardPaddingX: number
34
+ cardPaddingY: number
35
+ posterAspectRatio: string
36
+ posterMinHeight: number
37
+ posterMinWidth: number
38
+ visibleItems: number
39
+ }
40
+
41
+ export type ProfileHeroLayout = {
42
+ columns: number
43
+ gap: number
44
+ cardPaddingX: number
45
+ cardPaddingY: number
46
+ avatarSize: number
47
+ avatarColumnSpan: number
48
+ bodyMaxWidth: number
49
+ titleLines: number
50
+ bioLines: number
51
+ statColumns: number
52
+ }
53
+
54
+ export type PhotoGridTemplateItem = {
55
+ title: string
56
+ caption?: string
57
+ badge?: string
58
+ imageLabel?: string
59
+ accent?: string
60
+ }
61
+
62
+ export type VideoPosterRailItem = {
63
+ title: string
64
+ caption?: string
65
+ duration?: string
66
+ badge?: string
67
+ posterLabel?: string
68
+ accent?: string
69
+ }
70
+
71
+ export type ProfileHeroStat = {
72
+ label: string
73
+ value: string
74
+ }
75
+
76
+ export type ProfileHeroTemplateItem = {
77
+ name: string
78
+ role: string
79
+ bio?: string
80
+ avatarLabel?: string
81
+ accent?: string
82
+ highlights?: string[]
83
+ stats?: ProfileHeroStat[]
84
+ }
85
+
86
+ function clamp(value: number, min: number, max: number) {
87
+ return Math.min(max, Math.max(min, value))
88
+ }
89
+
90
+ function deriveDensity(runtime: MediaTemplateRuntime, itemCount: number) {
91
+ const landscape = runtime.width >= runtime.height
92
+ const compact = itemCount >= 4
93
+ const dense = itemCount >= 6
94
+ const vertical = !landscape
95
+ return { landscape, compact, dense, vertical }
96
+ }
97
+
98
+ export function getPhotoGridLayout(itemCount: number, runtime: MediaTemplateRuntime): PhotoGridLayout {
99
+ const count = Math.max(1, Math.min(8, itemCount))
100
+ const density = deriveDensity(runtime, count)
101
+ const maxColumns = density.vertical ? 2 : 3
102
+ const columns = count <= 1 ? 1 : count === 2 ? 2 : count <= 4 ? Math.min(2, maxColumns) : Math.min(maxColumns, 3)
103
+ const rows = Math.ceil(count / columns)
104
+ const gap = density.dense ? 12 : density.compact ? 14 : 18
105
+
106
+ return {
107
+ columns,
108
+ rows,
109
+ gap,
110
+ cardPaddingX: density.dense ? 14 : density.compact ? 18 : 24,
111
+ cardPaddingY: density.dense ? 12 : density.compact ? 16 : 22,
112
+ cardMinHeight: clamp(Math.floor((runtime.height - 220 - gap * (rows - 1)) / rows), 120, density.vertical ? 340 : 420),
113
+ mediaAspectRatio: density.vertical ? '4 / 3' : count <= 3 ? '16 / 10' : '3 / 2',
114
+ titleLines: density.dense ? 2 : 3,
115
+ captionLines: density.dense ? 2 : 3,
116
+ visibleItems: count,
117
+ }
118
+ }
119
+
120
+ export function getVideoPosterRailLayout(
121
+ itemCount: number,
122
+ runtime: MediaTemplateRuntime,
123
+ ): VideoPosterRailLayout {
124
+ const count = Math.max(1, Math.min(6, itemCount))
125
+ const density = deriveDensity(runtime, count)
126
+ const railDirection: 'row' | 'column' = density.vertical || count >= 4 ? 'column' : 'row'
127
+ const columns = railDirection === 'column' ? 1 : clamp(count, 2, 3)
128
+ const rows = railDirection === 'column' ? count : Math.ceil(count / columns)
129
+ const gap = density.dense ? 12 : density.compact ? 14 : 18
130
+ const posterMinHeight = clamp(Math.floor((runtime.height - 240 - gap * (rows - 1)) / Math.max(1, rows)), 128, density.vertical ? 220 : 300)
131
+ const posterMinWidth = clamp(Math.floor((runtime.width - 220 - gap * (columns - 1)) / Math.max(1, columns)), 220, density.vertical ? 520 : 680)
132
+
133
+ return {
134
+ railDirection,
135
+ columns,
136
+ rows,
137
+ gap,
138
+ cardPaddingX: density.dense ? 14 : density.compact ? 18 : 24,
139
+ cardPaddingY: density.dense ? 12 : density.compact ? 16 : 22,
140
+ posterAspectRatio: density.vertical ? '9 / 16' : '16 / 9',
141
+ posterMinHeight,
142
+ posterMinWidth,
143
+ visibleItems: count,
144
+ }
145
+ }
146
+
147
+ export function getProfileHeroLayout(
148
+ runtime: MediaTemplateRuntime,
149
+ itemCount = 1,
150
+ ): ProfileHeroLayout {
151
+ const density = deriveDensity(runtime, Math.max(1, itemCount))
152
+ const columns = density.vertical ? 1 : 2
153
+ const gap = density.dense ? 14 : density.compact ? 18 : 24
154
+ const avatarSize = clamp(Math.round(Math.min(runtime.width, runtime.height) * (density.vertical ? 0.24 : 0.28)), 120, density.vertical ? 280 : 360)
155
+
156
+ return {
157
+ columns,
158
+ gap,
159
+ cardPaddingX: density.dense ? 16 : density.compact ? 20 : 28,
160
+ cardPaddingY: density.dense ? 14 : density.compact ? 18 : 28,
161
+ avatarSize,
162
+ avatarColumnSpan: density.vertical ? 1 : 1,
163
+ bodyMaxWidth: density.vertical ? runtime.width - 64 : Math.round(runtime.width * 0.52),
164
+ titleLines: density.dense ? 2 : 3,
165
+ bioLines: density.dense ? 3 : 4,
166
+ statColumns: density.vertical ? 2 : 3,
167
+ }
168
+ }
169
+
170
+ export function clampTextLines(lines: number): CSSProperties {
171
+ return {
172
+ display: '-webkit-box',
173
+ WebkitBoxOrient: 'vertical',
174
+ WebkitLineClamp: lines,
175
+ overflow: 'hidden',
176
+ }
177
+ }
178
+
179
+ export function createTileStyle(theme: MediaTileStyle): CSSProperties {
180
+ return {
181
+ borderRadius: theme.borderRadius,
182
+ border: theme.border,
183
+ background: theme.background,
184
+ boxShadow: theme.boxShadow,
185
+ overflow: 'hidden',
186
+ minWidth: 0,
187
+ minHeight: 0,
188
+ }
189
+ }
190
+
191
+ export function fitMediaStyle(aspectRatio: string): CSSProperties {
192
+ return {
193
+ width: '100%',
194
+ aspectRatio,
195
+ borderRadius: 0,
196
+ objectFit: 'cover',
197
+ display: 'block',
198
+ minWidth: 0,
199
+ minHeight: 0,
200
+ }
201
+ }
202
+
203
+ export function stackContentStyle(gap: number): CSSProperties {
204
+ return {
205
+ display: 'grid',
206
+ gap,
207
+ minWidth: 0,
208
+ minHeight: 0,
209
+ }
210
+ }
211
+
212
+ export function gridStyle(columns: number, gap: number): CSSProperties {
213
+ return {
214
+ display: 'grid',
215
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
216
+ gap,
217
+ minWidth: 0,
218
+ minHeight: 0,
219
+ }
220
+ }
221
+
222
+ export function rowRailStyle(columns: number, gap: number): CSSProperties {
223
+ return {
224
+ display: 'grid',
225
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
226
+ gap,
227
+ minWidth: 0,
228
+ minHeight: 0,
229
+ }
230
+ }
231
+
232
+ export function columnRailStyle(rows: number, gap: number): CSSProperties {
233
+ return {
234
+ display: 'grid',
235
+ gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
236
+ gap,
237
+ minWidth: 0,
238
+ minHeight: 0,
239
+ }
240
+ }
241
+
242
+ export function safeFullSizeStyle(extra: CSSProperties = {}): CSSProperties {
243
+ return {
244
+ minWidth: 0,
245
+ minHeight: 0,
246
+ maxWidth: '100%',
247
+ maxHeight: '100%',
248
+ overflow: 'hidden',
249
+ boxSizing: 'border-box',
250
+ ...extra,
251
+ }
252
+ }
253
+
254
+ export function renderMediaBadge(label?: string) {
255
+ if (!label) {
256
+ return null
257
+ }
258
+
259
+ return (
260
+ <span
261
+ style={{
262
+ display: 'inline-flex',
263
+ alignItems: 'center',
264
+ justifyContent: 'center',
265
+ minHeight: 22,
266
+ padding: '0 8px',
267
+ borderRadius: 0,
268
+ border: '1px solid rgba(148,163,184,0.28)',
269
+ background: 'rgba(255,255,255,0.72)',
270
+ color: 'inherit',
271
+ fontSize: 11,
272
+ fontWeight: 700,
273
+ letterSpacing: '0.06em',
274
+ textTransform: 'uppercase',
275
+ }}
276
+ >
277
+ {label}
278
+ </span>
279
+ )
280
+ }
281
+
282
+ export function renderMediaPlaceholder(label: string, aspectRatio: string, background: string) {
283
+ return (
284
+ <div
285
+ style={{
286
+ ...fitMediaStyle(aspectRatio),
287
+ display: 'grid',
288
+ placeItems: 'center',
289
+ color: '#e2e8f0',
290
+ background,
291
+ border: '1px solid rgba(148,163,184,0.18)',
292
+ fontSize: 12,
293
+ fontWeight: 700,
294
+ letterSpacing: '0.04em',
295
+ textTransform: 'uppercase',
296
+ }}
297
+ >
298
+ {label}
299
+ </div>
300
+ )
301
+ }
302
+
303
+ export type MediaTemplateSectionProps = {
304
+ children: ReactNode
305
+ style?: CSSProperties
306
+ }
307
+
308
+ export function MediaTemplateSection({ children, style }: MediaTemplateSectionProps) {
309
+ return (
310
+ <section
311
+ style={{
312
+ width: '100%',
313
+ height: '100%',
314
+ minWidth: 0,
315
+ minHeight: 0,
316
+ overflow: 'hidden',
317
+ boxSizing: 'border-box',
318
+ ...style,
319
+ }}
320
+ >
321
+ {children}
322
+ </section>
323
+ )
324
+ }
@@ -52,6 +52,11 @@ import {
52
52
  DefaultFeatureListSlide,
53
53
  componentMetadata as galleryMetadata,
54
54
  DefaultGallerySlide,
55
+ componentMetadata as photoAlbumMetadata,
56
+ DefaultPhotoAlbumSlide,
57
+ DefaultPhotoAlbumSingleSlide,
58
+ DefaultPhotoAlbumPairSlide,
59
+ DefaultPhotoAlbumMultiSlide,
55
60
  componentMetadata as faqAccordionMetadata,
56
61
  DefaultFAQAccordionSlide,
57
62
  componentMetadata as faqMetadata,
@@ -64,6 +69,12 @@ import {
64
69
  DefaultHeroVariantSlide,
65
70
  componentMetadata as imageHeroMetadata,
66
71
  DefaultImageHeroSlide,
72
+ componentMetadata as personIntroMetadata,
73
+ DefaultPersonIntroSlide,
74
+ DefaultPersonIntroGroupSlide,
75
+ DefaultPersonIntroDuoSlide,
76
+ DefaultPersonIntroTrioSlide,
77
+ DefaultPersonIntroQuadSlide,
67
78
  componentMetadata as interactiveDemoMetadata,
68
79
  DefaultInteractiveDemoSlide,
69
80
  componentMetadata as kpiDashboardMetadata,
@@ -90,6 +101,9 @@ import {
90
101
  DefaultMapLocationSlide,
91
102
  componentMetadata as orgChartMetadata,
92
103
  DefaultOrgChartSlide,
104
+ componentMetadata as videoShowcaseMetadata,
105
+ DefaultVideoShowcaseSlide,
106
+ DefaultVideoShowcaseMultiSlide,
93
107
  componentMetadata as stackTimelineMetadata,
94
108
  DefaultStackTimelineSlide,
95
109
  componentMetadata as processMetadata,
@@ -185,6 +199,7 @@ describe('video-project-core slide exports', () => {
185
199
  expect(geoMapMetadata.sceneFamily).toBe('slide')
186
200
  expect(diagramMatrixMetadata.sceneFamily).toBe('slide')
187
201
  expect(galleryMetadata.sceneFamily).toBe('slide')
202
+ expect(photoAlbumMetadata.sceneFamily).toBe('slide')
188
203
  expect(kpiDashboardMetadata.sceneFamily).toBe('slide')
189
204
  expect(heroVariantMetadata.sceneFamily).toBe('slide')
190
205
  expect(interactiveDemoMetadata.sceneFamily).toBe('slide')
@@ -244,6 +259,10 @@ describe('video-project-core slide exports', () => {
244
259
  expect(DefaultFeatureGridSlide).toBeTypeOf('function')
245
260
  expect(DefaultFeatureListSlide).toBeTypeOf('function')
246
261
  expect(DefaultGallerySlide).toBeTypeOf('function')
262
+ expect(DefaultPhotoAlbumSlide).toBeTypeOf('function')
263
+ expect(DefaultPhotoAlbumSingleSlide).toBeTypeOf('function')
264
+ expect(DefaultPhotoAlbumPairSlide).toBeTypeOf('function')
265
+ expect(DefaultPhotoAlbumMultiSlide).toBeTypeOf('function')
247
266
  expect(DefaultFAQAccordionSlide).toBeTypeOf('function')
248
267
  expect(DefaultFAQSlide).toBeTypeOf('function')
249
268
  expect(DefaultFAQSidecarSlide).toBeTypeOf('function')
@@ -261,6 +280,7 @@ describe('video-project-core slide exports', () => {
261
280
  expect(DefaultFlowDiagramSlide).toBeTypeOf('function')
262
281
  expect(DefaultMapLocationSlide).toBeTypeOf('function')
263
282
  expect(DefaultOrgChartSlide).toBeTypeOf('function')
283
+ expect(videoShowcaseMetadata.sceneFamily).toBe('slide')
264
284
  expect(DefaultStackTimelineSlide).toBeTypeOf('function')
265
285
  expect(DefaultPricingSlide).toBeTypeOf('function')
266
286
  expect(DefaultProsConsSlide).toBeTypeOf('function')
@@ -294,8 +314,16 @@ describe('video-project-core slide exports', () => {
294
314
  expect(threeColumnMetadata.sceneFamily).toBe('slide')
295
315
  expect(DefaultHeroSlide).toBeTypeOf('function')
296
316
  expect(DefaultImageHeroSlide).toBeTypeOf('function')
317
+ expect(personIntroMetadata.sceneFamily).toBe('slide')
318
+ expect(DefaultPersonIntroSlide).toBeTypeOf('function')
319
+ expect(DefaultPersonIntroGroupSlide).toBeTypeOf('function')
320
+ expect(DefaultPersonIntroDuoSlide).toBeTypeOf('function')
321
+ expect(DefaultPersonIntroTrioSlide).toBeTypeOf('function')
322
+ expect(DefaultPersonIntroQuadSlide).toBeTypeOf('function')
297
323
  expect(DefaultSplitSlide).toBeTypeOf('function')
298
324
  expect(DefaultChapterSlide).toBeTypeOf('function')
299
325
  expect(DefaultThreeColumnSlide).toBeTypeOf('function')
326
+ expect(DefaultVideoShowcaseSlide).toBeTypeOf('function')
327
+ expect(DefaultVideoShowcaseMultiSlide).toBeTypeOf('function')
300
328
  })
301
329
  })
@@ -32,10 +32,19 @@ export { DefaultFeatureGridSlide } from './feature-grid'
32
32
  export { DefaultFeatureListSlide } from './feature-list'
33
33
  export { DefaultFlowDiagramSlide } from './flow-diagram'
34
34
  export { DefaultGallerySlide } from './gallery'
35
+ export { DefaultPhotoAlbumSlide } from './photo-album'
36
+ export { DefaultPhotoAlbumSingleSlide } from './photo-album'
37
+ export { DefaultPhotoAlbumPairSlide } from './photo-album'
38
+ export { DefaultPhotoAlbumMultiSlide } from './photo-album'
35
39
  export { DefaultGeoMapSlide } from './geo-map'
36
40
  export { DefaultHeroSlide } from './hero'
37
41
  export { DefaultHeroVariantSlide } from './hero-variant'
38
42
  export { DefaultImageHeroSlide } from './image-hero'
43
+ export { DefaultPersonIntroSlide } from './person-intro'
44
+ export { DefaultPersonIntroGroupSlide } from './person-intro'
45
+ export { DefaultPersonIntroDuoSlide } from './person-intro'
46
+ export { DefaultPersonIntroTrioSlide } from './person-intro'
47
+ export { DefaultPersonIntroQuadSlide } from './person-intro'
39
48
  export { DefaultInteractiveDemoSlide } from './interactive-demo'
40
49
  export { DefaultKPIDashboardSlide } from './kpi-dashboard'
41
50
  export { DefaultListHeroSlide } from './list-hero'
@@ -49,6 +58,8 @@ export { DefaultMetricShowcaseSlide } from './metric-showcase'
49
58
  export { DefaultMetricsStripSlide } from './metrics-strip'
50
59
  export { DefaultMermaidDiagramSlide } from './mermaid-diagram'
51
60
  export { DefaultOrgChartSlide } from './org-chart'
61
+ export { DefaultVideoShowcaseSlide } from './video-showcase'
62
+ export { DefaultVideoShowcaseMultiSlide } from './video-showcase'
52
63
  export { DefaultPricingSlide } from './pricing'
53
64
  export { DefaultProcessCardsSlide } from './process-cards'
54
65
  export { DefaultProcessSlide } from './process'
@@ -111,10 +122,12 @@ export const featureGridMetadata = slideSceneMetadata
111
122
  export const featureListMetadata = slideSceneMetadata
112
123
  export const flowDiagramMetadata = slideSceneMetadata
113
124
  export const galleryMetadata = slideSceneMetadata
125
+ export const photoAlbumMetadata = slideSceneMetadata
114
126
  export const geoMapMetadata = slideSceneMetadata
115
127
  export const heroMetadata = slideSceneMetadata
116
128
  export const heroVariantMetadata = slideSceneMetadata
117
129
  export const imageHeroMetadata = slideSceneMetadata
130
+ export const personIntroMetadata = slideSceneMetadata
118
131
  export const interactiveDemoMetadata = slideSceneMetadata
119
132
  export const kpiDashboardMetadata = slideSceneMetadata
120
133
  export const listHeroMetadata = slideSceneMetadata
@@ -128,6 +141,7 @@ export const metricShowcaseMetadata = slideSceneMetadata
128
141
  export const metricsStripMetadata = slideSceneMetadata
129
142
  export const mermaidDiagramMetadata = slideSceneMetadata
130
143
  export const orgChartMetadata = slideSceneMetadata
144
+ export const videoShowcaseMetadata = slideSceneMetadata
131
145
  export const pricingMetadata = slideSceneMetadata
132
146
  export const processCardsMetadata = slideSceneMetadata
133
147
  export const processMetadata = slideSceneMetadata
@@ -0,0 +1,19 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPersonIntroGroupSlide, componentMetadata as baseComponentMetadata } from './default-person-intro-group-slide'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPersonIntroDuoSlide',
10
+ description: 'Two person intro slide',
11
+ sourceFile: 'src/core/scene/slide/person-intro/default-person-intro-duo-slide.tsx',
12
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'duo', 'two-person'])),
13
+ }
14
+
15
+ export const DefaultPersonIntroDuoSlide = VideoComponent(componentMetadata)(function DefaultPersonIntroDuoSlide(
16
+ props: ComponentProps<typeof DefaultPersonIntroGroupSlide>,
17
+ ) {
18
+ return <DefaultPersonIntroGroupSlide {...props} />
19
+ })
@@ -0,0 +1,168 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { z } from 'zod'
4
+
5
+ import { VideoComponent, defineScenePluginMetadata } from '@vibecuting/component-project-helper'
6
+
7
+ import { SceneMotionLayer, useRemotionSceneRuntime } from '../../../../base'
8
+ import { defineSceneComponent } from '../../../../plugins/scene-component'
9
+ import type { DefaultSceneProps } from '../../../../scenes/default-scene-props'
10
+ import { SlideFrame, getOverflowSafeSlideGridLayout, getSlideTypographyScale } from '../_shared'
11
+
12
+ export interface PersonIntroGroupMember {
13
+ name: string
14
+ role?: string
15
+ bio?: string
16
+ avatar?: ReactNode
17
+ }
18
+
19
+ export interface DefaultPersonIntroGroupSlideProps extends DefaultSceneProps {
20
+ title: string
21
+ subtitle?: string
22
+ people: PersonIntroGroupMember[]
23
+ }
24
+
25
+ function PersonGroupAvatarFallback({ name }: { name: string }) {
26
+ const initials = name
27
+ .split(/\s+/)
28
+ .filter(Boolean)
29
+ .slice(0, 2)
30
+ .map((part) => part[0] ?? '')
31
+ .join('')
32
+ .toUpperCase()
33
+
34
+ return (
35
+ <div style={{ width: '100%', height: '100%', display: 'grid', placeItems: 'center', background: 'linear-gradient(135deg, rgba(59,130,246,0.34), rgba(14,165,233,0.18))', color: '#f8fafc', fontSize: 32, fontWeight: 800 }}>
36
+ {initials || 'P'}
37
+ </div>
38
+ )
39
+ }
40
+
41
+ function PersonGroupCard({ member, scale }: { member: PersonIntroGroupMember; scale: ReturnType<typeof getSlideTypographyScale> }) {
42
+ return (
43
+ <article
44
+ style={{
45
+ minWidth: 0,
46
+ minHeight: 0,
47
+ padding: 14,
48
+ display: 'grid',
49
+ gap: 12,
50
+ background: 'rgba(15,23,42,0.58)',
51
+ border: '1px solid rgba(148,163,184,0.18)',
52
+ color: '#e2e8f0',
53
+ overflow: 'hidden',
54
+ }}
55
+ >
56
+ <div style={{ display: 'grid', gridTemplateColumns: '92px 1fr', gap: 14, alignItems: 'center', minWidth: 0 }}>
57
+ <div style={{ width: 92, height: 92, overflow: 'hidden', borderRadius: 0, border: '1px solid rgba(148,163,184,0.22)', background: 'rgba(2,6,23,0.88)' }}>
58
+ {member.avatar ?? <PersonGroupAvatarFallback name={member.name} />}
59
+ </div>
60
+ <div style={{ minWidth: 0, display: 'grid', gap: 8 }}>
61
+ <div style={{ fontSize: scale.eyebrow, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(148,163,184,0.86)' }}>Profile</div>
62
+ <h3 style={{ margin: 0, fontSize: Math.max(scale.body * 1.02, 22), lineHeight: 1.02, letterSpacing: '-0.05em' }}>{member.name}</h3>
63
+ {member.role ? <div style={{ fontSize: scale.caption, lineHeight: 1.4, opacity: 0.82 }}>{member.role}</div> : null}
64
+ </div>
65
+ </div>
66
+ {member.bio ? <p style={{ margin: 0, fontSize: scale.caption, lineHeight: 1.5, color: 'rgba(226,232,240,0.8)' }}>{member.bio}</p> : null}
67
+ </article>
68
+ )
69
+ }
70
+
71
+ export const componentMetadata = defineScenePluginMetadata({
72
+ resourceKind: 'scene',
73
+ name: 'DefaultPersonIntroGroupSlide',
74
+ description: 'Multi person intro slide',
75
+ sourceFile: 'src/core/scene/slide/person-intro/default-person-intro-group-slide.tsx',
76
+ pluginKey: 'core.slide.person-intro.group',
77
+ tags: ['scene', 'slide', 'person', 'profile', 'group'],
78
+ aspectRatio: '16:9',
79
+ sceneType: 'scene',
80
+ sceneFamily: 'slide',
81
+ rootLayout: 'absolute-fill',
82
+ propsTypeName: 'DefaultPersonIntroGroupSlideProps',
83
+ })
84
+
85
+ export const DefaultPersonIntroGroupSlide = VideoComponent(componentMetadata)(
86
+ defineSceneComponent({
87
+ family: 'slide',
88
+ propsSchema: z.object({
89
+ sceneId: z.string().optional(),
90
+ sceneName: z.string().optional(),
91
+ title: z.string().min(1),
92
+ subtitle: z.string().optional(),
93
+ people: z
94
+ .array(
95
+ z.object({
96
+ name: z.string().min(1),
97
+ role: z.string().optional(),
98
+ bio: z.string().optional(),
99
+ avatar: z.any().optional(),
100
+ }),
101
+ )
102
+ .min(2)
103
+ .max(4),
104
+ }),
105
+ component: function DefaultPersonIntroGroupSlide({ sceneId, sceneName, title, subtitle, people }: DefaultPersonIntroGroupSlideProps) {
106
+ const runtime = useRemotionSceneRuntime()
107
+ const isPortrait = runtime.height > runtime.width
108
+ const scale = getSlideTypographyScale(runtime.height)
109
+ const grid = getOverflowSafeSlideGridLayout(people.length, isPortrait ? 1 : 2, {
110
+ runtimeWidth: runtime.width,
111
+ reservedWidth: isPortrait ? 40 : 120,
112
+ runtimeHeight: runtime.height,
113
+ reservedHeight: isPortrait ? 420 : 240,
114
+ minCardWidth: isPortrait ? 220 : 240,
115
+ minCardHeight: isPortrait ? 168 : 180,
116
+ })
117
+
118
+ return (
119
+ <SlideFrame
120
+ pluginKey={componentMetadata.pluginKey}
121
+ sceneId={sceneId}
122
+ sceneName={sceneName ?? title}
123
+ background="linear-gradient(135deg, #020617 0%, #0f172a 48%, #1e293b 100%)"
124
+ >
125
+ <section
126
+ style={{
127
+ width: '100%',
128
+ height: '100%',
129
+ display: 'grid',
130
+ gridTemplateRows: 'auto minmax(0, 1fr)',
131
+ gap: isPortrait ? 18 : 24,
132
+ padding: isPortrait ? '40px 24px' : '60px 72px',
133
+ color: '#e2e8f0',
134
+ minWidth: 0,
135
+ minHeight: 0,
136
+ }}
137
+ >
138
+ <SceneMotionLayer variant="hero-title">
139
+ <div style={{ display: 'grid', gap: 12, maxWidth: isPortrait ? 620 : 760, minWidth: 0 }}>
140
+ <div style={{ fontSize: scale.eyebrow, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(148,163,184,0.9)' }}>Team intro</div>
141
+ <h1 style={{ margin: 0, fontSize: isPortrait ? scale.title * 0.76 : scale.title, lineHeight: 0.94, letterSpacing: '-0.06em' }}>{title}</h1>
142
+ {subtitle ? <p style={{ margin: 0, fontSize: isPortrait ? scale.body * 0.92 : scale.body, lineHeight: 1.5, opacity: 0.82 }}>{subtitle}</p> : null}
143
+ </div>
144
+ </SceneMotionLayer>
145
+
146
+ <SceneMotionLayer variant="panel" delayIndex={1}>
147
+ <div
148
+ style={{
149
+ display: 'grid',
150
+ gridTemplateColumns: `repeat(${Math.max(1, grid.columns)}, minmax(0, 1fr))`,
151
+ gap: grid.gap,
152
+ minWidth: 0,
153
+ minHeight: 0,
154
+ }}
155
+ >
156
+ {people.slice(0, 4).map((member, index) => (
157
+ <SceneMotionLayer key={`${member.name}-${index}`} variant="card" delayIndex={index + 1}>
158
+ <PersonGroupCard member={member} scale={scale} />
159
+ </SceneMotionLayer>
160
+ ))}
161
+ </div>
162
+ </SceneMotionLayer>
163
+ </section>
164
+ </SlideFrame>
165
+ )
166
+ },
167
+ }),
168
+ )
@@ -0,0 +1,19 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPersonIntroGroupSlide, componentMetadata as baseComponentMetadata } from './default-person-intro-group-slide'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPersonIntroQuadSlide',
10
+ description: 'Four person intro slide',
11
+ sourceFile: 'src/core/scene/slide/person-intro/default-person-intro-quad-slide.tsx',
12
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'quad', 'four-person'])),
13
+ }
14
+
15
+ export const DefaultPersonIntroQuadSlide = VideoComponent(componentMetadata)(function DefaultPersonIntroQuadSlide(
16
+ props: ComponentProps<typeof DefaultPersonIntroGroupSlide>,
17
+ ) {
18
+ return <DefaultPersonIntroGroupSlide {...props} />
19
+ })