@morphika/webframe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +210 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ContentBlock, BlockLayout } from "../../lib/sanity/types";
|
|
4
|
+
import type { EnterAnimationConfig, TypewriterConfig } from "../../lib/animation/enter-types";
|
|
5
|
+
import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
|
|
6
|
+
import { isShaderPreset } from "../../lib/animation/hover-effect-presets";
|
|
7
|
+
import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
|
|
8
|
+
import { useAssetUrl } from "../../lib/contexts/AssetContext";
|
|
9
|
+
import { useViewport } from "../../lib/hooks/useViewport";
|
|
10
|
+
import { resolveBlock } from "../../lib/builder/responsive";
|
|
11
|
+
import { getBlockLayoutStyles, hasBlockLayout } from "../../lib/builder/layout-styles";
|
|
12
|
+
import { hexToRgba } from "../../lib/color-utils";
|
|
13
|
+
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
14
|
+
import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
15
|
+
import HoverAnimationWrapper from "./HoverAnimationWrapper";
|
|
16
|
+
import TypewriterWrapper from "./TypewriterWrapper";
|
|
17
|
+
|
|
18
|
+
import TextBlockRenderer, { getTextBlockStyles } from "./TextBlockRenderer";
|
|
19
|
+
import ImageBlockRenderer from "./ImageBlockRenderer";
|
|
20
|
+
import ImageGridBlockRenderer from "./ImageGridBlockRenderer";
|
|
21
|
+
import VideoBlockRenderer from "./VideoBlockRenderer";
|
|
22
|
+
import SpacerBlockRenderer from "./SpacerBlockRenderer";
|
|
23
|
+
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
24
|
+
import CoverBlockRenderer from "./CoverBlockRenderer";
|
|
25
|
+
import ProjectGridBlockRenderer from "./ProjectGridBlockRenderer";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Central block dispatcher for the public site.
|
|
29
|
+
*
|
|
30
|
+
* Resolves responsive overrides (tablet/phone) based on the current
|
|
31
|
+
* browser viewport width before passing the block to its renderer.
|
|
32
|
+
* This ensures all block types benefit from responsive overrides
|
|
33
|
+
* without each renderer needing to handle resolution individually.
|
|
34
|
+
*
|
|
35
|
+
* If the block has layout properties (spacing, background, offset, border),
|
|
36
|
+
* wraps the rendered block in a <div> with those styles applied.
|
|
37
|
+
* Layout responsive overrides (tablet/phone) generate @media CSS via
|
|
38
|
+
* buildBlockLayoutResponsiveCss().
|
|
39
|
+
*
|
|
40
|
+
* Enter animation: receives cascade from parent (page → section → column).
|
|
41
|
+
* Block-level enter_animation overrides the cascade entirely.
|
|
42
|
+
* Hover animation: block-level only, no cascade. Reads directly from
|
|
43
|
+
* block.hover_effect.
|
|
44
|
+
*
|
|
45
|
+
* Breakpoints match the builder device frame widths:
|
|
46
|
+
* Desktop: > 810px
|
|
47
|
+
* Tablet: 390–810px
|
|
48
|
+
* Phone: < 390px
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// ── Block layout responsive CSS generation ──
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build CSS rules for block LAYOUT overrides (spacing, border, background) for a viewport.
|
|
55
|
+
* These rules target the inner `.blk-layout-{key}` div inside BlockRenderer.
|
|
56
|
+
*/
|
|
57
|
+
function buildBlockLayoutOverrideRules(
|
|
58
|
+
overrides?: Partial<BlockLayout>
|
|
59
|
+
): string[] {
|
|
60
|
+
if (!overrides) return [];
|
|
61
|
+
const rules: string[] = [];
|
|
62
|
+
|
|
63
|
+
// px-value fields (excluding offset — vertical offset conflicts with alignment margin-auto)
|
|
64
|
+
const pxMap: Record<string, string> = {
|
|
65
|
+
spacing_top: "padding-top",
|
|
66
|
+
spacing_right: "padding-right",
|
|
67
|
+
spacing_bottom: "padding-bottom",
|
|
68
|
+
spacing_left: "padding-left",
|
|
69
|
+
offset_top: "margin-top",
|
|
70
|
+
offset_right: "margin-right",
|
|
71
|
+
offset_bottom: "margin-bottom",
|
|
72
|
+
offset_left: "margin-left",
|
|
73
|
+
border_radius: "border-radius",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (const [field, cssProp] of Object.entries(pxMap)) {
|
|
77
|
+
const val = (overrides as Record<string, unknown>)[field];
|
|
78
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
79
|
+
rules.push(`${cssProp}:${val}px!important`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Border width — side-aware
|
|
84
|
+
if (overrides.border_width !== undefined && overrides.border_width !== null && overrides.border_width !== "") {
|
|
85
|
+
const bw = overrides.border_width;
|
|
86
|
+
const sides = overrides.border_sides || "all";
|
|
87
|
+
switch (sides) {
|
|
88
|
+
case "top":
|
|
89
|
+
rules.push(`border-top-width:${bw}px!important`);
|
|
90
|
+
break;
|
|
91
|
+
case "right":
|
|
92
|
+
rules.push(`border-right-width:${bw}px!important`);
|
|
93
|
+
break;
|
|
94
|
+
case "bottom":
|
|
95
|
+
rules.push(`border-bottom-width:${bw}px!important`);
|
|
96
|
+
break;
|
|
97
|
+
case "left":
|
|
98
|
+
rules.push(`border-left-width:${bw}px!important`);
|
|
99
|
+
break;
|
|
100
|
+
case "top-bottom":
|
|
101
|
+
rules.push(`border-top-width:${bw}px!important`);
|
|
102
|
+
rules.push(`border-bottom-width:${bw}px!important`);
|
|
103
|
+
break;
|
|
104
|
+
case "left-right":
|
|
105
|
+
rules.push(`border-left-width:${bw}px!important`);
|
|
106
|
+
rules.push(`border-right-width:${bw}px!important`);
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
rules.push(`border-width:${bw}px!important`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Border color (no px)
|
|
115
|
+
if (overrides.border_color) {
|
|
116
|
+
rules.push(`border-color:${overrides.border_color}!important`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Border style (no px)
|
|
120
|
+
if (overrides.border_style) {
|
|
121
|
+
rules.push(`border-style:${overrides.border_style}!important`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Background color (with opacity support)
|
|
125
|
+
if (overrides.background_color) {
|
|
126
|
+
const opacity = overrides.background_opacity;
|
|
127
|
+
if (opacity !== undefined && opacity < 100) {
|
|
128
|
+
const rgba = hexToRgba(overrides.background_color, opacity / 100);
|
|
129
|
+
rules.push(`background-color:${rgba}!important`);
|
|
130
|
+
} else {
|
|
131
|
+
rules.push(`background-color:${overrides.background_color}!important`);
|
|
132
|
+
}
|
|
133
|
+
} else if (overrides.background_opacity !== undefined) {
|
|
134
|
+
// Opacity-only override (color inherited from desktop) — handled at render via resolveBlock
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return rules;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build CSS rules for block ALIGNMENT overrides (h-align, v-align) for a viewport.
|
|
142
|
+
* These rules target the outer `.blk-wrap-{key}` wrapper div — the direct flex child
|
|
143
|
+
* of the column — where align-self actually takes effect.
|
|
144
|
+
*
|
|
145
|
+
* NOTE: Vertical alignment (align_v) is NOT handled here — it's applied as
|
|
146
|
+
* justify-content on the column flex container at render time. Responsive vertical
|
|
147
|
+
* alignment changes require column-level CSS which the renderers handle.
|
|
148
|
+
*/
|
|
149
|
+
function buildBlockAlignOverrideRules(
|
|
150
|
+
overrides?: Partial<BlockLayout>
|
|
151
|
+
): string[] {
|
|
152
|
+
if (!overrides) return [];
|
|
153
|
+
const rules: string[] = [];
|
|
154
|
+
|
|
155
|
+
// Horizontal alignment → align-self within flex-col parent
|
|
156
|
+
if (overrides.align_h) {
|
|
157
|
+
const alignSelfMap: Record<string, string> = {
|
|
158
|
+
left: "flex-start",
|
|
159
|
+
center: "center",
|
|
160
|
+
right: "flex-end",
|
|
161
|
+
};
|
|
162
|
+
rules.push(`align-self:${alignSelfMap[overrides.align_h] || "flex-start"}!important`);
|
|
163
|
+
if (overrides.align_h === "left") {
|
|
164
|
+
rules.push("width:100%!important");
|
|
165
|
+
} else {
|
|
166
|
+
rules.push("width:auto!important");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return rules;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate responsive CSS <style> content for a block's layout AND alignment overrides.
|
|
175
|
+
*/
|
|
176
|
+
function buildBlockLayoutResponsiveCss(block: ContentBlock): string | null {
|
|
177
|
+
const responsive = (block as unknown as Record<string, unknown>).responsive as
|
|
178
|
+
| Record<string, Record<string, unknown>>
|
|
179
|
+
| undefined;
|
|
180
|
+
if (!responsive) return null;
|
|
181
|
+
|
|
182
|
+
const key = block._key;
|
|
183
|
+
const tabletLayout = responsive.tablet?.layout as Partial<BlockLayout> | undefined;
|
|
184
|
+
const phoneLayout = responsive.phone?.layout as Partial<BlockLayout> | undefined;
|
|
185
|
+
|
|
186
|
+
const tabletLayoutRules = buildBlockLayoutOverrideRules(tabletLayout);
|
|
187
|
+
const phoneLayoutRules = buildBlockLayoutOverrideRules(phoneLayout);
|
|
188
|
+
|
|
189
|
+
const tabletAlignRules = buildBlockAlignOverrideRules(tabletLayout);
|
|
190
|
+
const phoneAlignRules = buildBlockAlignOverrideRules(phoneLayout);
|
|
191
|
+
|
|
192
|
+
const hasAny = tabletLayoutRules.length + phoneLayoutRules.length +
|
|
193
|
+
tabletAlignRules.length + phoneAlignRules.length > 0;
|
|
194
|
+
if (!hasAny) return null;
|
|
195
|
+
|
|
196
|
+
let css = "";
|
|
197
|
+
|
|
198
|
+
if (tabletLayoutRules.length > 0 || tabletAlignRules.length > 0) {
|
|
199
|
+
const parts: string[] = [];
|
|
200
|
+
if (tabletLayoutRules.length > 0) {
|
|
201
|
+
parts.push(`.blk-layout-${key}{${tabletLayoutRules.join(";")}}`);
|
|
202
|
+
}
|
|
203
|
+
if (tabletAlignRules.length > 0) {
|
|
204
|
+
parts.push(`.blk-wrap-${key}{${tabletAlignRules.join(";")}}`);
|
|
205
|
+
}
|
|
206
|
+
css += `@media(max-width:${BREAKPOINTS.tablet}px){${parts.join("")}}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (phoneLayoutRules.length > 0 || phoneAlignRules.length > 0) {
|
|
210
|
+
const parts: string[] = [];
|
|
211
|
+
if (phoneLayoutRules.length > 0) {
|
|
212
|
+
parts.push(`.blk-layout-${key}{${phoneLayoutRules.join(";")}}`);
|
|
213
|
+
}
|
|
214
|
+
if (phoneAlignRules.length > 0) {
|
|
215
|
+
parts.push(`.blk-wrap-${key}{${phoneAlignRules.join(";")}}`);
|
|
216
|
+
}
|
|
217
|
+
css += `@media(max-width:${BREAKPOINTS.phone}px){${parts.join("")}}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return css;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Main component ──
|
|
224
|
+
|
|
225
|
+
interface BlockRendererProps {
|
|
226
|
+
block: ContentBlock;
|
|
227
|
+
/** Column-level enter animation (from SectionV2Renderer) */
|
|
228
|
+
columnEnterAnimation?: EnterAnimationConfig;
|
|
229
|
+
/** Section-level enter animation (from section settings) */
|
|
230
|
+
sectionEnterAnimation?: EnterAnimationConfig;
|
|
231
|
+
/** Page-level enter animation (from page_settings) */
|
|
232
|
+
pageEnterAnimation?: EnterAnimationConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default function BlockRenderer({
|
|
236
|
+
block,
|
|
237
|
+
columnEnterAnimation,
|
|
238
|
+
sectionEnterAnimation,
|
|
239
|
+
pageEnterAnimation,
|
|
240
|
+
}: BlockRendererProps) {
|
|
241
|
+
const viewport = useViewport();
|
|
242
|
+
const resolveAsset = useAssetUrl();
|
|
243
|
+
const resolved = resolveBlock(block, viewport);
|
|
244
|
+
|
|
245
|
+
let content: React.ReactNode;
|
|
246
|
+
|
|
247
|
+
switch (resolved._type) {
|
|
248
|
+
case "textBlock":
|
|
249
|
+
content = <TextBlockRenderer block={resolved} />;
|
|
250
|
+
break;
|
|
251
|
+
case "imageBlock":
|
|
252
|
+
content = <ImageBlockRenderer block={resolved} />;
|
|
253
|
+
break;
|
|
254
|
+
case "imageGridBlock":
|
|
255
|
+
content = <ImageGridBlockRenderer block={resolved} />;
|
|
256
|
+
break;
|
|
257
|
+
case "videoBlock":
|
|
258
|
+
content = <VideoBlockRenderer block={resolved} />;
|
|
259
|
+
break;
|
|
260
|
+
case "spacerBlock":
|
|
261
|
+
content = <SpacerBlockRenderer block={resolved} />;
|
|
262
|
+
break;
|
|
263
|
+
case "buttonBlock":
|
|
264
|
+
content = <ButtonBlockRenderer block={resolved} />;
|
|
265
|
+
break;
|
|
266
|
+
case "coverBlock":
|
|
267
|
+
content = <CoverBlockRenderer block={resolved} />;
|
|
268
|
+
break;
|
|
269
|
+
case "projectGridBlock":
|
|
270
|
+
content = <ProjectGridBlockRenderer block={resolved as import("@/lib/sanity/types").ProjectGridBlock} />;
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
if (process.env.NODE_ENV === "development") {
|
|
274
|
+
content = (
|
|
275
|
+
<div className="border border-dashed border-brand-secondary/50 p-4 font-mono text-xs text-brand-secondary">
|
|
276
|
+
Unknown block type: {(resolved as ContentBlock)._type}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Resolve enter animation once (used both for typewriter early-wrap and normal path) ──
|
|
285
|
+
const blockEnterAnim = (resolved as unknown as Record<string, unknown>).enter_animation as
|
|
286
|
+
| EnterAnimationConfig
|
|
287
|
+
| undefined;
|
|
288
|
+
const resolvedEnter = resolveEnterAnimation(
|
|
289
|
+
blockEnterAnim,
|
|
290
|
+
columnEnterAnimation,
|
|
291
|
+
sectionEnterAnimation,
|
|
292
|
+
pageEnterAnimation,
|
|
293
|
+
);
|
|
294
|
+
const isTypewriter = resolvedEnter?.preset === "typewriter" && resolved._type === "textBlock";
|
|
295
|
+
|
|
296
|
+
// ── Typewriter: wrap BEFORE layout so padding applies to the animation ──
|
|
297
|
+
// Must be before the layout wrapper so that block padding/background wraps
|
|
298
|
+
// the TypewriterWrapper. Single-phase: rich text with per-character animation,
|
|
299
|
+
// swaps to standard PortableText children after completion.
|
|
300
|
+
if (isTypewriter && resolvedEnter) {
|
|
301
|
+
const twConfig = (resolved as unknown as Record<string, unknown>).typewriter_config as
|
|
302
|
+
| TypewriterConfig
|
|
303
|
+
| undefined;
|
|
304
|
+
const textBlock = resolved as import("@/lib/sanity/types").TextBlock;
|
|
305
|
+
const { className: textClassName, style: textStyle } = getTextBlockStyles(textBlock);
|
|
306
|
+
content = (
|
|
307
|
+
<TypewriterWrapper
|
|
308
|
+
portableText={textBlock.text}
|
|
309
|
+
config={twConfig}
|
|
310
|
+
delay={resolvedEnter.delay}
|
|
311
|
+
textClassName={textClassName}
|
|
312
|
+
textStyle={textStyle}
|
|
313
|
+
>
|
|
314
|
+
{content}
|
|
315
|
+
</TypewriterWrapper>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Wrap in layout div if block has layout properties set (spacing, background, border).
|
|
320
|
+
const layout = (resolved as unknown as Record<string, unknown>).layout as ContentBlock["layout"] | undefined;
|
|
321
|
+
if (hasBlockLayout(layout)) {
|
|
322
|
+
const layoutStyles = getBlockLayoutStyles(layout, process.env.NEXT_PUBLIC_ASSET_BASE_URL);
|
|
323
|
+
const responsiveCss = buildBlockLayoutResponsiveCss(block);
|
|
324
|
+
|
|
325
|
+
content = (
|
|
326
|
+
<>
|
|
327
|
+
{responsiveCss && (
|
|
328
|
+
<style dangerouslySetInnerHTML={{ __html: responsiveCss }} />
|
|
329
|
+
)}
|
|
330
|
+
<div
|
|
331
|
+
className={responsiveCss ? `blk-layout-${block._key}` : undefined}
|
|
332
|
+
style={layoutStyles}
|
|
333
|
+
>
|
|
334
|
+
{content}
|
|
335
|
+
</div>
|
|
336
|
+
</>
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
const responsiveCss = buildBlockLayoutResponsiveCss(block);
|
|
340
|
+
if (responsiveCss) {
|
|
341
|
+
content = (
|
|
342
|
+
<>
|
|
343
|
+
<style dangerouslySetInnerHTML={{ __html: responsiveCss }} />
|
|
344
|
+
<div className={`blk-layout-${block._key}`}>{content}</div>
|
|
345
|
+
</>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Enter animation: apply non-typewriter presets after layout wrapper ──
|
|
351
|
+
// Typewriter was already applied BEFORE the layout wrapper (above) so that
|
|
352
|
+
// block padding wraps both phases. All other presets wrap after layout.
|
|
353
|
+
if (resolvedEnter && resolvedEnter.preset !== "none" && !isTypewriter) {
|
|
354
|
+
content = (
|
|
355
|
+
<EnterAnimationWrapper config={resolvedEnter}>
|
|
356
|
+
{content}
|
|
357
|
+
</EnterAnimationWrapper>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Hover animation: block-level only, no cascade ──
|
|
362
|
+
const blockHoverEffect = (resolved as unknown as Record<string, unknown>).hover_effect as
|
|
363
|
+
| HoverEffectConfig
|
|
364
|
+
| undefined;
|
|
365
|
+
if (blockHoverEffect?.preset && blockHoverEffect.preset !== "none") {
|
|
366
|
+
// Extract image src for shader presets (ripple, rgb-shift, pixelate)
|
|
367
|
+
let shaderSrc: string | undefined;
|
|
368
|
+
let shaderBorderRadius: string | undefined;
|
|
369
|
+
const isShader = isShaderPreset(blockHoverEffect.preset);
|
|
370
|
+
if (isShader) {
|
|
371
|
+
if (resolved._type === "imageBlock") {
|
|
372
|
+
shaderSrc = resolveAsset((resolved as import("@/lib/sanity/types").ImageBlock).asset_path);
|
|
373
|
+
const br = (resolved as import("@/lib/sanity/types").ImageBlock).border_radius;
|
|
374
|
+
if (br) shaderBorderRadius = `${String(br).replace(/px$/i, "")}px`;
|
|
375
|
+
} else if (resolved._type === "coverBlock") {
|
|
376
|
+
const mediaPath = (resolved as import("@/lib/sanity/types").CoverBlock).media_path;
|
|
377
|
+
if (mediaPath) shaderSrc = resolveAsset(mediaPath);
|
|
378
|
+
}
|
|
379
|
+
// Shader preset without image src: skip wrapper entirely
|
|
380
|
+
if (!shaderSrc) {
|
|
381
|
+
// No-op: can't apply shader without an image source
|
|
382
|
+
} else {
|
|
383
|
+
content = (
|
|
384
|
+
<HoverAnimationWrapper
|
|
385
|
+
config={blockHoverEffect}
|
|
386
|
+
shaderSrc={shaderSrc}
|
|
387
|
+
shaderContainerStyle={shaderBorderRadius ? { borderRadius: shaderBorderRadius } : undefined}
|
|
388
|
+
>
|
|
389
|
+
{content}
|
|
390
|
+
</HoverAnimationWrapper>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
// CSS hover preset — always safe to wrap
|
|
395
|
+
content = (
|
|
396
|
+
<HoverAnimationWrapper config={blockHoverEffect}>
|
|
397
|
+
{content}
|
|
398
|
+
</HoverAnimationWrapper>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return <>{content}</>;
|
|
404
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ButtonBlock } from "../../lib/sanity/types";
|
|
2
|
+
|
|
3
|
+
const styleMap: Record<string, string> = {
|
|
4
|
+
primary:
|
|
5
|
+
"bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
|
|
6
|
+
secondary: "bg-brand-primary text-white hover:opacity-90",
|
|
7
|
+
outline:
|
|
8
|
+
"border border-brand-text text-brand-text hover:bg-brand-text hover:text-brand-dark",
|
|
9
|
+
text: "text-brand-accent-alt underline underline-offset-4 hover:text-brand-accent",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const sizeMap: Record<string, string> = {
|
|
13
|
+
small: "px-4 py-2 text-xs",
|
|
14
|
+
medium: "px-6 py-3 text-sm",
|
|
15
|
+
large: "px-8 py-4 text-base",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const alignmentMap: Record<string, string> = {
|
|
19
|
+
left: "justify-start",
|
|
20
|
+
center: "justify-center",
|
|
21
|
+
right: "justify-end",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function ButtonBlockRenderer({
|
|
25
|
+
block,
|
|
26
|
+
}: {
|
|
27
|
+
block: ButtonBlock;
|
|
28
|
+
}) {
|
|
29
|
+
const variant = styleMap[block.style ?? "primary"];
|
|
30
|
+
const size = sizeMap[block.size ?? "medium"];
|
|
31
|
+
const alignment = alignmentMap[block.alignment ?? "left"];
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={`flex ${alignment}`}>
|
|
35
|
+
<a
|
|
36
|
+
href={block.url}
|
|
37
|
+
target={block.target ? "_blank" : undefined}
|
|
38
|
+
rel={block.target ? "noopener noreferrer" : undefined}
|
|
39
|
+
className={[
|
|
40
|
+
"inline-block font-mono uppercase tracking-wider transition-all",
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
block.full_width ? "w-full text-center" : "",
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ")}
|
|
47
|
+
>
|
|
48
|
+
{block.text}
|
|
49
|
+
</a>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CoverBlock } from "../../lib/sanity/types";
|
|
4
|
+
import { useAssetUrl } from "../../lib/contexts/AssetContext";
|
|
5
|
+
import { handleImageRetry, handleVideoRetry } from "../../lib/asset-retry";
|
|
6
|
+
|
|
7
|
+
function getOverlayStyle(
|
|
8
|
+
overlay: CoverBlock["overlay"],
|
|
9
|
+
opacity: number
|
|
10
|
+
): React.CSSProperties | null {
|
|
11
|
+
const alpha = opacity / 100;
|
|
12
|
+
switch (overlay) {
|
|
13
|
+
case "dark":
|
|
14
|
+
return { backgroundColor: `rgba(0,0,0,${alpha})` };
|
|
15
|
+
case "light":
|
|
16
|
+
return { backgroundColor: `rgba(255,255,255,${alpha})` };
|
|
17
|
+
case "gradient-bottom":
|
|
18
|
+
return {
|
|
19
|
+
background: `linear-gradient(to top, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
|
|
20
|
+
};
|
|
21
|
+
case "gradient-top":
|
|
22
|
+
return {
|
|
23
|
+
background: `linear-gradient(to bottom, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
|
|
24
|
+
};
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getAlignItems(v: CoverBlock["content_align_v"]): string {
|
|
31
|
+
switch (v) {
|
|
32
|
+
case "top":
|
|
33
|
+
return "flex-start";
|
|
34
|
+
case "bottom":
|
|
35
|
+
return "flex-end";
|
|
36
|
+
default:
|
|
37
|
+
return "center";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getJustify(h: CoverBlock["content_align_h"]): string {
|
|
42
|
+
switch (h) {
|
|
43
|
+
case "left":
|
|
44
|
+
return "flex-start";
|
|
45
|
+
case "right":
|
|
46
|
+
return "flex-end";
|
|
47
|
+
default:
|
|
48
|
+
return "center";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTextAlign(
|
|
53
|
+
h: CoverBlock["content_align_h"]
|
|
54
|
+
): "left" | "center" | "right" {
|
|
55
|
+
return h || "center";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default function CoverBlockRenderer({
|
|
59
|
+
block,
|
|
60
|
+
}: {
|
|
61
|
+
block: CoverBlock;
|
|
62
|
+
}) {
|
|
63
|
+
const height =
|
|
64
|
+
block.height === "custom" && block.custom_height
|
|
65
|
+
? block.custom_height
|
|
66
|
+
: block.height || "100vh";
|
|
67
|
+
|
|
68
|
+
const mobileHeight =
|
|
69
|
+
block.mobile_height && block.mobile_height !== "same"
|
|
70
|
+
? block.mobile_height
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
const overlayStyle = getOverlayStyle(
|
|
74
|
+
block.overlay ?? "none",
|
|
75
|
+
block.overlay_opacity ?? 50
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const resolveAsset = useAssetUrl();
|
|
79
|
+
const mediaSrc = block.media_path ? resolveAsset(block.media_path) : undefined;
|
|
80
|
+
const posterSrc = block.video_poster
|
|
81
|
+
? resolveAsset(block.video_poster)
|
|
82
|
+
: undefined;
|
|
83
|
+
const isVideo = block.media_type === "video";
|
|
84
|
+
|
|
85
|
+
const objectFit = block.background_size || "cover";
|
|
86
|
+
const objectPosition = block.background_position || "center center";
|
|
87
|
+
|
|
88
|
+
const textColor = block.text_color || "#ffffff";
|
|
89
|
+
|
|
90
|
+
const ctaStyleClasses: Record<string, string> = {
|
|
91
|
+
primary:
|
|
92
|
+
"bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
|
|
93
|
+
secondary:
|
|
94
|
+
"bg-brand-dark text-white hover:bg-neutral-800",
|
|
95
|
+
outline:
|
|
96
|
+
"border-2 border-current bg-transparent hover:bg-white/10",
|
|
97
|
+
text: "underline underline-offset-4 hover:opacity-70",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
{/* Mobile height override via inline style tag */}
|
|
103
|
+
{mobileHeight && (
|
|
104
|
+
<style
|
|
105
|
+
dangerouslySetInnerHTML={{
|
|
106
|
+
__html: `@media(max-width:767px){.cover-block-${block._key}{height:${mobileHeight}!important;min-height:${mobileHeight}!important;}}`,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<section
|
|
112
|
+
className={`cover-block-${block._key} relative flex overflow-hidden`}
|
|
113
|
+
style={{
|
|
114
|
+
height,
|
|
115
|
+
minHeight: height,
|
|
116
|
+
alignItems: getAlignItems(block.content_align_v),
|
|
117
|
+
justifyContent: getJustify(block.content_align_h),
|
|
118
|
+
color: textColor,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{/* Media layer — uses <img> instead of background-image for error recovery */}
|
|
122
|
+
{mediaSrc && !isVideo && (
|
|
123
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
124
|
+
<img
|
|
125
|
+
src={mediaSrc}
|
|
126
|
+
alt={block.headline || ""}
|
|
127
|
+
onError={handleImageRetry}
|
|
128
|
+
className="absolute inset-0 h-full w-full"
|
|
129
|
+
style={{
|
|
130
|
+
objectFit: objectFit as "cover" | "contain" | "none",
|
|
131
|
+
objectPosition,
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{mediaSrc && isVideo && (
|
|
137
|
+
<video
|
|
138
|
+
src={mediaSrc}
|
|
139
|
+
poster={posterSrc}
|
|
140
|
+
autoPlay
|
|
141
|
+
loop
|
|
142
|
+
muted
|
|
143
|
+
playsInline
|
|
144
|
+
onError={handleVideoRetry}
|
|
145
|
+
className="absolute inset-0 h-full w-full"
|
|
146
|
+
style={{
|
|
147
|
+
objectFit: objectFit as "cover" | "contain" | "none",
|
|
148
|
+
objectPosition,
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Fallback: poster only if no media_path but video_poster exists */}
|
|
154
|
+
{!mediaSrc && posterSrc && (
|
|
155
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
156
|
+
<img
|
|
157
|
+
src={posterSrc}
|
|
158
|
+
alt=""
|
|
159
|
+
onError={handleImageRetry}
|
|
160
|
+
className="absolute inset-0 h-full w-full"
|
|
161
|
+
style={{ objectFit: "cover", objectPosition: "center" }}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* No media fallback */}
|
|
166
|
+
{!mediaSrc && !posterSrc && (
|
|
167
|
+
<div className="absolute inset-0 bg-brand-dark" />
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Overlay */}
|
|
171
|
+
{overlayStyle && <div className="absolute inset-0" style={overlayStyle} />}
|
|
172
|
+
|
|
173
|
+
{/* Content */}
|
|
174
|
+
<div
|
|
175
|
+
className="relative z-10 flex flex-col gap-4 py-12"
|
|
176
|
+
style={{
|
|
177
|
+
maxWidth: block.content_max_width || "800px",
|
|
178
|
+
textAlign: getTextAlign(block.content_align_h),
|
|
179
|
+
width: "100%",
|
|
180
|
+
paddingLeft: "var(--grid-padding, 24px)",
|
|
181
|
+
paddingRight: "var(--grid-padding, 24px)",
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{block.headline && (
|
|
185
|
+
<h1 className="font-mono text-4xl uppercase tracking-widest md:text-6xl lg:text-7xl">
|
|
186
|
+
{block.headline}
|
|
187
|
+
</h1>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{block.subheadline && (
|
|
191
|
+
<p className="font-mono text-sm uppercase tracking-wider opacity-80 md:text-base">
|
|
192
|
+
{block.subheadline}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{block.cta_button?.text && block.cta_button.url && (
|
|
197
|
+
<div>
|
|
198
|
+
<a
|
|
199
|
+
href={block.cta_button.url}
|
|
200
|
+
target={
|
|
201
|
+
block.cta_button.target_blank ? "_blank" : undefined
|
|
202
|
+
}
|
|
203
|
+
rel={
|
|
204
|
+
block.cta_button.target_blank
|
|
205
|
+
? "noopener noreferrer"
|
|
206
|
+
: undefined
|
|
207
|
+
}
|
|
208
|
+
className={`inline-block px-6 py-3 font-mono text-sm uppercase tracking-wider transition ${
|
|
209
|
+
ctaStyleClasses[block.cta_button.style || "primary"]
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{block.cta_button.text}
|
|
213
|
+
</a>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Scroll indicator */}
|
|
219
|
+
{block.show_scroll_indicator && (
|
|
220
|
+
<div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 animate-bounce">
|
|
221
|
+
<svg
|
|
222
|
+
width="24"
|
|
223
|
+
height="24"
|
|
224
|
+
viewBox="0 0 24 24"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="2"
|
|
228
|
+
strokeLinecap="round"
|
|
229
|
+
strokeLinejoin="round"
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
>
|
|
232
|
+
<path d="M12 5v14M5 12l7 7 7-7" />
|
|
233
|
+
</svg>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</section>
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
}
|