@morphika/andami 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +212 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,278 @@
1
+ "use client";
2
+
3
+ import { useBuilderStore } from "../../../lib/builder/store";
4
+ import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
5
+ import type { VideoBlock, ContentBlock } from "../../../lib/sanity/types";
6
+ import {
7
+ SettingsField,
8
+ SettingsSection,
9
+ StyledCheckbox,
10
+ AssetPathInput,
11
+ ViewportBadge,
12
+ ResponsiveField,
13
+ useActiveViewport,
14
+ INPUT_CLASS,
15
+ SELECT_CLASS,
16
+ } from "./shared";
17
+
18
+ interface Props {
19
+ block: VideoBlock;
20
+ }
21
+
22
+ /**
23
+ * Extract embed ID from Vimeo or YouTube URLs.
24
+ */
25
+ function extractEmbedId(
26
+ url: string,
27
+ type: string
28
+ ): { id: string; valid: boolean } {
29
+ if (!url) return { id: "", valid: false };
30
+
31
+ if (type === "vimeo") {
32
+ const match = url.match(
33
+ /(?:vimeo\.com\/(?:video\/)?|player\.vimeo\.com\/video\/)(\d+)/
34
+ );
35
+ return match ? { id: match[1], valid: true } : { id: url, valid: false };
36
+ }
37
+
38
+ if (type === "youtube") {
39
+ const match = url.match(
40
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
41
+ );
42
+ return match ? { id: match[1], valid: true } : { id: url, valid: false };
43
+ }
44
+
45
+ return { id: url, valid: !!url };
46
+ }
47
+
48
+ export default function VideoBlockEditor({ block }: Props) {
49
+ const store = useBuilderStore();
50
+ const viewport = useActiveViewport();
51
+
52
+ const snapshotOnFocus = () => store._pushSnapshot();
53
+
54
+ // Responsive-aware update for layout/appearance properties
55
+ const updateResponsive = (property: string, value: unknown) => {
56
+ if (viewport === "desktop") {
57
+ store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
58
+ } else {
59
+ const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
60
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
61
+ }
62
+ };
63
+
64
+ const resetOverride = (property: string) => {
65
+ const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
66
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
67
+ };
68
+
69
+ // Direct update (base block, not responsive)
70
+ const update = (updates: Partial<VideoBlock>) => {
71
+ store.updateBlock(block._key, updates as Partial<ContentBlock>);
72
+ };
73
+
74
+ const updateDebounced = (updates: Partial<VideoBlock>) => {
75
+ store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
76
+ };
77
+
78
+ // Effective values for the active viewport
79
+ const effectiveWidth = getEffectiveValue<string>(
80
+ block as ContentBlock, viewport, "width", block.width || "full"
81
+ );
82
+ const effectiveAspect = getEffectiveValue<string>(
83
+ block as ContentBlock, viewport, "aspect_ratio", block.aspect_ratio || "16:9"
84
+ );
85
+
86
+ const embed = extractEmbedId(block.url_or_path || "", block.video_type);
87
+
88
+ return (
89
+ <>
90
+ <ViewportBadge />
91
+
92
+ <SettingsSection title="Source" defaultOpen>
93
+ <SettingsField label="Video Type">
94
+ <div className="flex gap-1">
95
+ {(
96
+ [
97
+ { value: "vimeo", label: "Vimeo" },
98
+ { value: "youtube", label: "YouTube" },
99
+ { value: "mp4", label: "MP4" },
100
+ { value: "url", label: "URL" },
101
+ ] as const
102
+ ).map((opt) => (
103
+ <button
104
+ key={opt.value}
105
+ onClick={() => update({ video_type: opt.value })}
106
+ className={`flex-1 rounded border py-1 text-xs transition-colors ${
107
+ (block.video_type || "vimeo") === opt.value
108
+ ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
109
+ : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
110
+ }`}
111
+ >
112
+ {opt.label}
113
+ </button>
114
+ ))}
115
+ </div>
116
+ </SettingsField>
117
+
118
+ <SettingsField
119
+ label="URL / Path"
120
+ hint={
121
+ block.video_type === "vimeo"
122
+ ? "Paste Vimeo URL or video ID"
123
+ : block.video_type === "youtube"
124
+ ? "Paste YouTube URL or video ID"
125
+ : block.video_type === "mp4"
126
+ ? "Relative path from seed URL"
127
+ : "Direct video URL"
128
+ }
129
+ >
130
+ {block.video_type === "mp4" ? (
131
+ <AssetPathInput
132
+ value={block.url_or_path || ""}
133
+ onFocus={snapshotOnFocus}
134
+ onChange={(v) => updateDebounced({ url_or_path: v })}
135
+ placeholder="projects/slug/video.mp4"
136
+ filterType="video"
137
+ />
138
+ ) : (
139
+ <input
140
+ type="text"
141
+ value={block.url_or_path || ""}
142
+ onFocus={snapshotOnFocus}
143
+ onChange={(e) => updateDebounced({ url_or_path: e.target.value })}
144
+ className={INPUT_CLASS}
145
+ placeholder="https://vimeo.com/..."
146
+ />
147
+ )}
148
+ </SettingsField>
149
+
150
+ {/* URL validation indicator */}
151
+ {block.url_or_path && (block.video_type === "vimeo" || block.video_type === "youtube") && (
152
+ <div className="flex items-center gap-1.5">
153
+ <div
154
+ className={`w-1.5 h-1.5 rounded-full ${
155
+ embed.valid ? "bg-[var(--admin-success)]" : "bg-[var(--admin-error)]"
156
+ }`}
157
+ />
158
+ <span className="text-[11px] text-neutral-600">
159
+ {embed.valid
160
+ ? `ID: ${embed.id}`
161
+ : "Could not extract video ID"}
162
+ </span>
163
+ </div>
164
+ )}
165
+
166
+ <SettingsField label="Poster Image" hint="Shown before video plays">
167
+ <AssetPathInput
168
+ value={block.poster || ""}
169
+ onFocus={snapshotOnFocus}
170
+ onChange={(v) => updateDebounced({ poster: v })}
171
+ placeholder="projects/slug/poster.jpg"
172
+ filterType="image"
173
+ />
174
+ </SettingsField>
175
+ </SettingsSection>
176
+
177
+ <SettingsSection title="Layout">
178
+ <ResponsiveField
179
+ label="Width"
180
+ block={block as ContentBlock}
181
+ property="width"
182
+ onReset={() => resetOverride("width")}
183
+ >
184
+ <div className="flex gap-1">
185
+ {(
186
+ [
187
+ { value: "full", label: "Full" },
188
+ { value: "contained", label: "Contained" },
189
+ ] as const
190
+ ).map((opt) => (
191
+ <button
192
+ key={opt.value}
193
+ onClick={() => updateResponsive("width", opt.value)}
194
+ className={`flex-1 rounded border py-1 text-xs transition-colors ${
195
+ effectiveWidth === opt.value
196
+ ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
197
+ : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
198
+ }`}
199
+ >
200
+ {opt.label}
201
+ </button>
202
+ ))}
203
+ </div>
204
+ </ResponsiveField>
205
+
206
+ <ResponsiveField
207
+ label="Aspect Ratio"
208
+ block={block as ContentBlock}
209
+ property="aspect_ratio"
210
+ onReset={() => resetOverride("aspect_ratio")}
211
+ >
212
+ <select
213
+ value={effectiveAspect}
214
+ onChange={(e) =>
215
+ updateResponsive("aspect_ratio", e.target.value)
216
+ }
217
+ className={SELECT_CLASS}
218
+ >
219
+ <option value="16:9">16:9</option>
220
+ <option value="21:9">21:9</option>
221
+ <option value="4:3">4:3</option>
222
+ <option value="auto">Auto</option>
223
+ </select>
224
+ </ResponsiveField>
225
+ </SettingsSection>
226
+
227
+ <SettingsSection title="Appearance">
228
+ <ResponsiveField
229
+ label="Border Radius"
230
+ block={block as ContentBlock}
231
+ property="border_radius"
232
+ onReset={() => resetOverride("border_radius")}
233
+ >
234
+ <div className="flex items-center gap-1.5">
235
+ <input
236
+ type="number"
237
+ value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
238
+ onFocus={snapshotOnFocus}
239
+ onChange={(e) => {
240
+ store._pushSnapshot();
241
+ updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
242
+ }}
243
+ className={INPUT_CLASS}
244
+ placeholder="0"
245
+ min={0}
246
+ />
247
+ <span className="text-[10px] text-neutral-400 shrink-0">px</span>
248
+ </div>
249
+ </ResponsiveField>
250
+ </SettingsSection>
251
+
252
+ <SettingsSection title="Playback">
253
+ <div className="space-y-1.5">
254
+ <StyledCheckbox
255
+ label="Autoplay"
256
+ checked={block.autoplay || false}
257
+ onChange={(checked) => update({ autoplay: checked })}
258
+ />
259
+ <StyledCheckbox
260
+ label="Loop"
261
+ checked={block.loop || false}
262
+ onChange={(checked) => update({ loop: checked })}
263
+ />
264
+ <StyledCheckbox
265
+ label="Muted"
266
+ checked={block.muted !== false}
267
+ onChange={(checked) => update({ muted: checked })}
268
+ />
269
+ <StyledCheckbox
270
+ label="Show Controls"
271
+ checked={block.controls !== false}
272
+ onChange={(checked) => update({ controls: checked })}
273
+ />
274
+ </div>
275
+ </SettingsSection>
276
+ </>
277
+ );
278
+ }
@@ -0,0 +1,10 @@
1
+ export { default as TextBlockEditor } from "./TextBlockEditor";
2
+ export { default as ImageBlockEditor } from "./ImageBlockEditor";
3
+ export { default as ImageGridBlockEditor } from "./ImageGridBlockEditor";
4
+ export { default as VideoBlockEditor } from "./VideoBlockEditor";
5
+ export { default as SpacerBlockEditor } from "./SpacerBlockEditor";
6
+ export { default as ButtonBlockEditor } from "./ButtonBlockEditor";
7
+ export { default as CoverBlockEditor } from "./CoverBlockEditor";
8
+ export { default as ProjectGridEditor } from "./ProjectGridEditor";
9
+ export { SettingsField, SettingsSection, StyledSelect, StyledInput, StyledCheckbox } from "./shared";
10
+ export { getSpacerPx } from "./SpacerBlockEditor";
@@ -0,0 +1,345 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { useBuilderStore } from "../../../lib/builder/store";
5
+ import { hasOverride } from "../../../lib/builder/responsive";
6
+ import type { DeviceViewport } from "../../../lib/builder/types";
7
+ import type { ContentBlock } from "../../../lib/sanity/types";
8
+ import AssetBrowser from "../AssetBrowser";
9
+
10
+ // ============================================
11
+ // Shared CSS classes — Framer-style design system
12
+ // ============================================
13
+
14
+ /** Base input class: gray bg, no border, border on focus */
15
+ const INPUT_CLASS =
16
+ "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
17
+
18
+ /** Select class — same as input */
19
+ const SELECT_CLASS =
20
+ "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
21
+
22
+ // ============================================
23
+ // Hooks
24
+ // ============================================
25
+
26
+ /**
27
+ * Hook to get the active viewport from the builder store.
28
+ */
29
+ export function useActiveViewport(): DeviceViewport {
30
+ return useBuilderStore((s) => s.activeViewport);
31
+ }
32
+
33
+ // ============================================
34
+ // Viewport Badge
35
+ // ============================================
36
+
37
+ /**
38
+ * Badge that shows when editing non-desktop viewport settings.
39
+ */
40
+ export function ViewportBadge() {
41
+ const viewport = useActiveViewport();
42
+ if (viewport === "desktop") return null;
43
+
44
+ const labels: Record<string, string> = {
45
+ tablet: "Tablet",
46
+ phone: "Phone",
47
+ };
48
+
49
+ return (
50
+ <div className="flex items-center gap-1.5 px-3 py-1.5 mb-2 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
51
+ <span className="text-[11px] font-medium text-[#076bff]">
52
+ Editing {labels[viewport]} overrides
53
+ </span>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ // ============================================
59
+ // Responsive Field
60
+ // ============================================
61
+
62
+ /**
63
+ * Wrapper for a responsive field that shows inherited/overridden state.
64
+ */
65
+ export function ResponsiveField({
66
+ label,
67
+ block,
68
+ property,
69
+ children,
70
+ hint,
71
+ onReset,
72
+ }: {
73
+ label: string;
74
+ block: ContentBlock;
75
+ property: string;
76
+ children: ReactNode;
77
+ hint?: string;
78
+ onReset?: () => void;
79
+ }) {
80
+ const viewport = useActiveViewport();
81
+ const isOverridden = viewport !== "desktop" && hasOverride(block, viewport, property);
82
+
83
+ return (
84
+ <div className="flex items-start gap-3 mb-2 last:mb-0">
85
+ <label className="text-[11px] text-neutral-400 w-[68px] min-w-[68px] shrink-0 pt-[7px] leading-tight">
86
+ {label}
87
+ {viewport !== "desktop" && !isOverridden && (
88
+ <span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
89
+ )}
90
+ {isOverridden && (
91
+ <span className="block text-[9px] text-[#076bff] mt-0.5">overridden</span>
92
+ )}
93
+ </label>
94
+ <div className="flex-1 min-w-0">
95
+ {children}
96
+ {hint && (
97
+ <p className="text-[10px] text-neutral-400 mt-1">{hint}</p>
98
+ )}
99
+ {isOverridden && onReset && (
100
+ <button
101
+ onClick={onReset}
102
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors mt-0.5"
103
+ >
104
+ Reset
105
+ </button>
106
+ )}
107
+ </div>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ // ============================================
113
+ // Settings Field — Framer inline layout
114
+ // ============================================
115
+
116
+ /**
117
+ * Reusable settings field: label left (68px), control right.
118
+ * If no label provided, control takes full width.
119
+ */
120
+ export function SettingsField({
121
+ label,
122
+ children,
123
+ hint,
124
+ }: {
125
+ label?: ReactNode;
126
+ children: ReactNode;
127
+ hint?: string;
128
+ }) {
129
+ if (!label) {
130
+ return (
131
+ <div className="mb-2 last:mb-0">
132
+ {children}
133
+ {hint && <p className="text-[10px] text-neutral-400 mt-1">{hint}</p>}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <div className="flex items-center gap-3 mb-2 last:mb-0">
140
+ <label className="text-[11px] text-neutral-400 w-[68px] min-w-[68px] shrink-0">
141
+ {label}
142
+ </label>
143
+ <div className="flex-1 min-w-0">
144
+ {children}
145
+ {hint && <p className="text-[10px] text-neutral-400 mt-1">{hint}</p>}
146
+ </div>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ // ============================================
152
+ // Settings Section — Framer collapsible
153
+ // ============================================
154
+
155
+ /**
156
+ * Collapsible section: clean text header + thin border separator.
157
+ * No gray background, no uppercase.
158
+ */
159
+ export function SettingsSection({
160
+ title,
161
+ children,
162
+ defaultOpen = true,
163
+ icon,
164
+ }: {
165
+ title: string;
166
+ children: ReactNode;
167
+ defaultOpen?: boolean;
168
+ icon?: ReactNode;
169
+ }) {
170
+ const [isOpen, setIsOpen] = useState(defaultOpen);
171
+
172
+ return (
173
+ <div className="border-b border-[#f0f0f0] last:border-b-0">
174
+ <button
175
+ onClick={() => setIsOpen(!isOpen)}
176
+ className="w-full flex items-center justify-between px-4 py-3 transition-colors hover:bg-[#fafafa] group"
177
+ >
178
+ <span className="flex items-center gap-1.5 text-xs font-medium text-neutral-900">
179
+ {icon && <span className="text-neutral-400 flex-shrink-0">{icon}</span>}
180
+ {title}
181
+ </span>
182
+ <span className="text-base text-neutral-300 font-light leading-none transition-colors group-hover:text-neutral-500">
183
+ {isOpen ? "−" : "+"}
184
+ </span>
185
+ </button>
186
+ {isOpen && (
187
+ <div className="px-4 pb-3.5 space-y-0">
188
+ {children}
189
+ </div>
190
+ )}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ // ============================================
196
+ // Styled Select
197
+ // ============================================
198
+
199
+ export function StyledSelect({
200
+ value,
201
+ onChange,
202
+ options,
203
+ }: {
204
+ value: string;
205
+ onChange: (value: string) => void;
206
+ options: { value: string; label: string }[];
207
+ }) {
208
+ return (
209
+ <select
210
+ value={value}
211
+ onChange={(e) => onChange(e.target.value)}
212
+ className={SELECT_CLASS}
213
+ >
214
+ {options.map((opt) => (
215
+ <option key={opt.value} value={opt.value}>
216
+ {opt.label}
217
+ </option>
218
+ ))}
219
+ </select>
220
+ );
221
+ }
222
+
223
+ // ============================================
224
+ // Styled Input
225
+ // ============================================
226
+
227
+ export function StyledInput({
228
+ value,
229
+ onChange,
230
+ onFocus,
231
+ placeholder,
232
+ type = "text",
233
+ }: {
234
+ value: string;
235
+ onChange: (value: string) => void;
236
+ onFocus?: () => void;
237
+ placeholder?: string;
238
+ type?: string;
239
+ }) {
240
+ return (
241
+ <input
242
+ type={type}
243
+ value={value}
244
+ onFocus={onFocus}
245
+ onChange={(e) => onChange(e.target.value)}
246
+ placeholder={placeholder}
247
+ className={INPUT_CLASS}
248
+ />
249
+ );
250
+ }
251
+
252
+ // ============================================
253
+ // Styled Checkbox
254
+ // ============================================
255
+
256
+ export function StyledCheckbox({
257
+ label,
258
+ checked,
259
+ onChange,
260
+ }: {
261
+ label: string;
262
+ checked: boolean;
263
+ onChange: (checked: boolean) => void;
264
+ }) {
265
+ return (
266
+ <label className="flex items-center gap-3 mb-2 last:mb-0 cursor-pointer group">
267
+ <span className="text-[11px] text-neutral-400 w-[68px] min-w-[68px] shrink-0">
268
+ {label}
269
+ </span>
270
+ <div className="flex items-center gap-2">
271
+ <button
272
+ type="button"
273
+ onClick={() => onChange(!checked)}
274
+ className={`relative w-8 h-[18px] rounded-full transition-colors ${
275
+ checked ? "bg-[#076bff]" : "bg-neutral-200 group-hover:bg-neutral-300"
276
+ }`}
277
+ >
278
+ <span
279
+ className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow-sm transition-transform ${
280
+ checked ? "left-[16px]" : "left-[2px]"
281
+ }`}
282
+ />
283
+ </button>
284
+ </div>
285
+ </label>
286
+ );
287
+ }
288
+
289
+ // ============================================
290
+ // Asset Path Input
291
+ // ============================================
292
+
293
+ export function AssetPathInput({
294
+ value,
295
+ onChange,
296
+ onFocus,
297
+ placeholder,
298
+ filterType = "all",
299
+ }: {
300
+ value: string;
301
+ onChange: (value: string) => void;
302
+ onFocus?: () => void;
303
+ placeholder?: string;
304
+ filterType?: "image" | "video" | "all";
305
+ }) {
306
+ const [browserOpen, setBrowserOpen] = useState(false);
307
+
308
+ return (
309
+ <>
310
+ <div className="flex gap-1.5">
311
+ <input
312
+ type="text"
313
+ value={value}
314
+ onFocus={onFocus}
315
+ onChange={(e) => onChange(e.target.value)}
316
+ placeholder={placeholder || "projects/slug/file.jpg"}
317
+ className={`flex-1 min-w-0 ${INPUT_CLASS}`}
318
+ />
319
+ <button
320
+ type="button"
321
+ onClick={() => setBrowserOpen(true)}
322
+ className="shrink-0 rounded-lg bg-[#f5f5f5] px-2.5 py-[7px] text-[11px] text-neutral-500 hover:text-neutral-900 hover:bg-[#efefef] transition-colors"
323
+ title="Browse assets"
324
+ >
325
+ Browse
326
+ </button>
327
+ </div>
328
+ <AssetBrowser
329
+ open={browserOpen}
330
+ onSelect={(path) => {
331
+ onChange(path);
332
+ setBrowserOpen(false);
333
+ }}
334
+ onClose={() => setBrowserOpen(false)}
335
+ filterType={filterType}
336
+ />
337
+ </>
338
+ );
339
+ }
340
+
341
+ // ============================================
342
+ // Export input class for editors that need raw classes
343
+ // ============================================
344
+
345
+ export { INPUT_CLASS, SELECT_CLASS };