@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.
- package/LICENSE +21 -0
- package/README.md +50 -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 +212 -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,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getSpacerPx } from "../editors/SpacerBlockEditor";
|
|
4
|
+
import type { SpacerBlock } from "../../../lib/sanity/types";
|
|
5
|
+
|
|
6
|
+
export default function LiveSpacerPreview({ block }: { block: SpacerBlock }) {
|
|
7
|
+
const height = getSpacerPx(block);
|
|
8
|
+
return <div style={{ height: `${height}px` }} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
4
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
+
import type { TextBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
6
|
+
|
|
7
|
+
/** Hook to get page-level text color for default block text rendering */
|
|
8
|
+
export function usePageTextColor(): string {
|
|
9
|
+
return useBuilderStore((s) => s.pageSettings.text_color) || "#0a0a0a";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Resolve fontSize: supports new numeric px and legacy string enum */
|
|
13
|
+
function resolveTextFontSize(fontSize?: number | string): string {
|
|
14
|
+
if (typeof fontSize === "number") return `${fontSize}px`;
|
|
15
|
+
// Legacy string enum fallback
|
|
16
|
+
const legacyMap: Record<string, string> = {
|
|
17
|
+
small: "12px", base: "14px", large: "20px",
|
|
18
|
+
xl: "24px", "2xl": "32px", "3xl": "48px",
|
|
19
|
+
};
|
|
20
|
+
return legacyMap[fontSize || "base"] || "14px";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve fontWeight: supports new string numbers and legacy names */
|
|
24
|
+
function resolveTextFontWeight(fw?: string): number {
|
|
25
|
+
if (!fw) return 400;
|
|
26
|
+
const num = parseInt(fw, 10);
|
|
27
|
+
if (!isNaN(num)) return num;
|
|
28
|
+
// Legacy name fallback
|
|
29
|
+
if (fw === "bold") return 700;
|
|
30
|
+
if (fw === "medium") return 500;
|
|
31
|
+
return 400;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function LiveTextEditor({ block, editable = false }: { block: TextBlock; editable?: boolean }) {
|
|
35
|
+
const pageTextColor = usePageTextColor();
|
|
36
|
+
const store = useBuilderStore();
|
|
37
|
+
const previewMode = useBuilderStore((s) => s.previewMode);
|
|
38
|
+
const isSelected = useBuilderStore((s) => s.selectedBlockKey) === block._key;
|
|
39
|
+
const editableRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const isComposingRef = useRef(false);
|
|
41
|
+
const snapshotPushedRef = useRef(false);
|
|
42
|
+
|
|
43
|
+
const plainText = Array.isArray(block.text)
|
|
44
|
+
? block.text
|
|
45
|
+
.map(
|
|
46
|
+
(blk) =>
|
|
47
|
+
blk?.children
|
|
48
|
+
?.map((c: { text?: string }) => c.text || "")
|
|
49
|
+
.join("") || ""
|
|
50
|
+
)
|
|
51
|
+
.join("\n")
|
|
52
|
+
: "";
|
|
53
|
+
|
|
54
|
+
const style = block.style || {};
|
|
55
|
+
|
|
56
|
+
// Debounce timer ref — commits text 500ms after last keystroke (not on every input)
|
|
57
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
58
|
+
|
|
59
|
+
// Cleanup debounce timer on unmount
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
return () => {
|
|
62
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// Sync content when block text changes externally (undo/redo)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!editableRef.current || document.activeElement === editableRef.current) return;
|
|
69
|
+
const currentHtml = editableRef.current.innerText || "";
|
|
70
|
+
if (currentHtml !== plainText) {
|
|
71
|
+
editableRef.current.innerText = plainText || "";
|
|
72
|
+
}
|
|
73
|
+
}, [plainText]);
|
|
74
|
+
|
|
75
|
+
const commitText = useCallback(() => {
|
|
76
|
+
if (!editableRef.current) return;
|
|
77
|
+
const raw = editableRef.current.innerText || "";
|
|
78
|
+
const lines = raw.split("\n");
|
|
79
|
+
const portableText = lines.map((line, i) => ({
|
|
80
|
+
_type: "block" as const,
|
|
81
|
+
_key: `pt_${i}`,
|
|
82
|
+
style: "normal" as const,
|
|
83
|
+
markDefs: [],
|
|
84
|
+
children: [
|
|
85
|
+
{
|
|
86
|
+
_type: "span" as const,
|
|
87
|
+
_key: `sp_${i}`,
|
|
88
|
+
text: line,
|
|
89
|
+
marks: [],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}));
|
|
93
|
+
store.updateBlockDebounced(block._key, {
|
|
94
|
+
text: portableText,
|
|
95
|
+
} as Partial<ContentBlock>);
|
|
96
|
+
}, [block._key, store]);
|
|
97
|
+
|
|
98
|
+
const handleFocus = useCallback(() => {
|
|
99
|
+
if (!snapshotPushedRef.current) {
|
|
100
|
+
store._pushSnapshot();
|
|
101
|
+
snapshotPushedRef.current = true;
|
|
102
|
+
}
|
|
103
|
+
}, [store]);
|
|
104
|
+
|
|
105
|
+
const handleBlur = useCallback(() => {
|
|
106
|
+
// Flush any pending debounced commit immediately on blur
|
|
107
|
+
if (debounceRef.current) {
|
|
108
|
+
clearTimeout(debounceRef.current);
|
|
109
|
+
debounceRef.current = null;
|
|
110
|
+
}
|
|
111
|
+
snapshotPushedRef.current = false;
|
|
112
|
+
commitText();
|
|
113
|
+
}, [commitText]);
|
|
114
|
+
|
|
115
|
+
const handleInput = useCallback(() => {
|
|
116
|
+
if (isComposingRef.current) return;
|
|
117
|
+
// Clear any pending debounce and schedule a new one
|
|
118
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
119
|
+
debounceRef.current = setTimeout(() => {
|
|
120
|
+
commitText();
|
|
121
|
+
debounceRef.current = null;
|
|
122
|
+
}, 500);
|
|
123
|
+
}, [commitText]);
|
|
124
|
+
|
|
125
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
126
|
+
// Stop propagation so builder shortcuts don't fire while typing
|
|
127
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
}
|
|
130
|
+
// Allow Ctrl+S, Ctrl+Z, Ctrl+Shift+Z to pass through
|
|
131
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "z" || e.key === "Z" || e.key === "y")) {
|
|
132
|
+
// Don't stop propagation for these
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const cols = block.columns && block.columns > 1 ? block.columns : undefined;
|
|
138
|
+
|
|
139
|
+
const computedStyle: React.CSSProperties = {
|
|
140
|
+
fontSize: resolveTextFontSize(style.fontSize),
|
|
141
|
+
fontWeight: resolveTextFontWeight(style.fontWeight),
|
|
142
|
+
textAlign: style.alignment || "left",
|
|
143
|
+
color: style.color || pageTextColor,
|
|
144
|
+
opacity: style.opacity ?? 1,
|
|
145
|
+
lineHeight: style.lineHeight || "1.6",
|
|
146
|
+
letterSpacing: style.letterSpacing || "normal",
|
|
147
|
+
maxWidth: style.maxWidth || "none",
|
|
148
|
+
textTransform: style.textTransform || "none",
|
|
149
|
+
fontFamily: "inherit",
|
|
150
|
+
outline: "none",
|
|
151
|
+
whiteSpace: "pre-wrap",
|
|
152
|
+
wordBreak: "break-word",
|
|
153
|
+
minHeight: "1em",
|
|
154
|
+
// Multi-column layout: gap inherits global grid gutter
|
|
155
|
+
...(cols ? {
|
|
156
|
+
columnCount: cols,
|
|
157
|
+
columnGap: "var(--grid-gutter, 24px)",
|
|
158
|
+
} : {}),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Preview mode or read-only frame: static render
|
|
162
|
+
if (previewMode || !editable) {
|
|
163
|
+
if (!plainText) {
|
|
164
|
+
return (
|
|
165
|
+
<p className="text-neutral-400 italic text-sm py-4 text-center">
|
|
166
|
+
Empty text block
|
|
167
|
+
</p>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return (
|
|
171
|
+
<div style={computedStyle}>
|
|
172
|
+
{plainText.split("\n").map((line, i) => (
|
|
173
|
+
<p key={i} className={i > 0 ? "mt-3" : ""}>
|
|
174
|
+
{line || "\u00A0"}
|
|
175
|
+
</p>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Design mode: contentEditable inline editing
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
ref={editableRef}
|
|
185
|
+
contentEditable
|
|
186
|
+
suppressContentEditableWarning
|
|
187
|
+
onFocus={handleFocus}
|
|
188
|
+
onBlur={handleBlur}
|
|
189
|
+
onInput={handleInput}
|
|
190
|
+
onKeyDown={handleKeyDown}
|
|
191
|
+
onCompositionStart={() => { isComposingRef.current = true; }}
|
|
192
|
+
onCompositionEnd={() => { isComposingRef.current = false; commitText(); }}
|
|
193
|
+
style={computedStyle}
|
|
194
|
+
data-placeholder="Type something..."
|
|
195
|
+
className={`${!plainText && !isSelected ? "before:content-[attr(data-placeholder)] before:text-neutral-400 before:italic before:pointer-events-none" : ""}`}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { adminAssetUrl } from "../../../lib/assets";
|
|
4
|
+
import type { VideoBlock } from "../../../lib/sanity/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* LiveVideoPreview — Static placeholder for builder canvas.
|
|
8
|
+
*
|
|
9
|
+
* Shows the correct aspect ratio + a lightweight thumbnail instead of
|
|
10
|
+
* loading real Vimeo/YouTube iframes or streaming MP4 files.
|
|
11
|
+
* This eliminates all video bandwidth in the builder.
|
|
12
|
+
*
|
|
13
|
+
* Thumbnail sources:
|
|
14
|
+
* - Vimeo: vumbnail.com (lightweight, no API key needed)
|
|
15
|
+
* - YouTube: ytimg.com (standard Google CDN)
|
|
16
|
+
* - MP4/URL: poster image if set, otherwise generic placeholder
|
|
17
|
+
*/
|
|
18
|
+
export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
|
|
19
|
+
if (!block.url_or_path) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="border border-dashed border-neutral-700 rounded bg-neutral-900/50 flex items-center justify-center py-12">
|
|
22
|
+
<span className="text-neutral-600 text-xs">No video set</span>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const aspectMap: Record<string, string> = {
|
|
28
|
+
"16:9": "56.25%",
|
|
29
|
+
"21:9": "42.86%",
|
|
30
|
+
"4:3": "75%",
|
|
31
|
+
auto: "56.25%",
|
|
32
|
+
};
|
|
33
|
+
const paddingBottom = aspectMap[block.aspect_ratio || "16:9"] || "56.25%";
|
|
34
|
+
const widthStyle = block.width === "contained" ? "75%" : "100%";
|
|
35
|
+
|
|
36
|
+
// Resolve thumbnail URL based on video type (no iframes, no streaming)
|
|
37
|
+
let thumbnailUrl: string | null = null;
|
|
38
|
+
let videoLabel = (block.video_type || "video").toUpperCase();
|
|
39
|
+
|
|
40
|
+
if (block.video_type === "vimeo") {
|
|
41
|
+
const match = block.url_or_path.match(/vimeo\.com\/(\d+)/);
|
|
42
|
+
if (match) {
|
|
43
|
+
thumbnailUrl = `https://vumbnail.com/${match[1]}.jpg`;
|
|
44
|
+
videoLabel = "VIMEO";
|
|
45
|
+
}
|
|
46
|
+
} else if (block.video_type === "youtube") {
|
|
47
|
+
const match = block.url_or_path.match(
|
|
48
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/
|
|
49
|
+
);
|
|
50
|
+
if (match) {
|
|
51
|
+
thumbnailUrl = `https://i.ytimg.com/vi/${match[1]}/hqdefault.jpg`;
|
|
52
|
+
videoLabel = "YOUTUBE";
|
|
53
|
+
}
|
|
54
|
+
} else if (block.poster) {
|
|
55
|
+
// MP4/URL with poster — use poster image via admin proxy
|
|
56
|
+
thumbnailUrl = adminAssetUrl(block.poster);
|
|
57
|
+
videoLabel = block.video_type === "mp4" ? "MP4" : "VIDEO";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div style={{ width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
|
|
64
|
+
<div style={{ position: "relative", paddingBottom, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
|
|
65
|
+
{thumbnailUrl ? (
|
|
66
|
+
<>
|
|
67
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
68
|
+
<img
|
|
69
|
+
src={thumbnailUrl}
|
|
70
|
+
alt="Video thumbnail"
|
|
71
|
+
loading="lazy"
|
|
72
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
73
|
+
/>
|
|
74
|
+
{/* Play icon + type badge overlay */}
|
|
75
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
76
|
+
<div className="w-14 h-14 rounded-full bg-black/50 flex items-center justify-center">
|
|
77
|
+
<span className="text-white text-xl ml-0.5">▶</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="absolute bottom-2 left-2 px-1.5 py-0.5 rounded bg-black/60 text-[10px] text-neutral-300 font-mono">
|
|
81
|
+
{videoLabel}
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
/* Generic placeholder — no thumbnail available */
|
|
86
|
+
<div className="absolute inset-0 bg-neutral-900 flex items-center justify-center">
|
|
87
|
+
<div className="text-center">
|
|
88
|
+
<span className="text-neutral-600 text-3xl block mb-2">▶</span>
|
|
89
|
+
<span className="text-neutral-600 text-xs font-mono">
|
|
90
|
+
{videoLabel}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as LiveTextEditor } from "./LiveTextEditor";
|
|
2
|
+
export { usePageTextColor } from "./LiveTextEditor";
|
|
3
|
+
export { default as LiveImagePreview } from "./LiveImagePreview";
|
|
4
|
+
export { default as LiveImageGridPreview } from "./LiveImageGridPreview";
|
|
5
|
+
export { default as LiveVideoPreview } from "./LiveVideoPreview";
|
|
6
|
+
export { default as LiveSpacerPreview } from "./LiveSpacerPreview";
|
|
7
|
+
export { default as LiveButtonPreview } from "./LiveButtonPreview";
|
|
8
|
+
export { default as LiveCoverPreview } from "./LiveCoverPreview";
|
|
9
|
+
export { default as LiveProjectGridPreview } from "./LiveProjectGridPreview";
|
|
10
|
+
export { ThumbBadge, LivePlaceholder, useProjectThumbnails, ProjectGridCard } from "./shared";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useThumbStatus } from "../../../lib/contexts/ThumbStatusContext";
|
|
5
|
+
import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Thumbnail status badge (builder-only)
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
/** Tiny green/amber dot showing thumbnail availability on image blocks. */
|
|
12
|
+
export function ThumbBadge({ assetPath }: { assetPath: string }) {
|
|
13
|
+
const { hasThumb } = useThumbStatus();
|
|
14
|
+
const status = hasThumb(assetPath);
|
|
15
|
+
// undefined = not raster or unknown — don't show badge
|
|
16
|
+
if (status === undefined) return null;
|
|
17
|
+
return (
|
|
18
|
+
<span
|
|
19
|
+
title={status ? "Thumbnail available" : "No thumbnail — full resolution"}
|
|
20
|
+
className="absolute bottom-1.5 right-1.5 z-10 flex items-center justify-center"
|
|
21
|
+
style={{
|
|
22
|
+
width: 16,
|
|
23
|
+
height: 16,
|
|
24
|
+
borderRadius: "50%",
|
|
25
|
+
backgroundColor: status ? "rgba(34,197,94,0.8)" : "rgba(245,158,11,0.8)",
|
|
26
|
+
pointerEvents: "auto",
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{status ? (
|
|
30
|
+
// Checkmark
|
|
31
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
|
32
|
+
<path d="M2 5.5L4 7.5L8 3" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
33
|
+
</svg>
|
|
34
|
+
) : (
|
|
35
|
+
// Info dot
|
|
36
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
|
37
|
+
<circle cx="5" cy="3.5" r="1" fill="white" />
|
|
38
|
+
<rect x="4.25" y="5" width="1.5" height="3" rx="0.5" fill="white" />
|
|
39
|
+
</svg>
|
|
40
|
+
)}
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Shared project thumbnail hook
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
/** Shared hook: fetch project thumbnails from /api/projects for builder previews */
|
|
50
|
+
export function useProjectThumbnails(slugs: string[]) {
|
|
51
|
+
const [thumbMap, setThumbMap] = useState<Map<string, string | undefined>>(new Map());
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!slugs.length) return;
|
|
55
|
+
|
|
56
|
+
fetch("/api/projects")
|
|
57
|
+
.then((r) => (r.ok ? r.json() : { projects: [] }))
|
|
58
|
+
.then((data) => {
|
|
59
|
+
const map = new Map<string, string | undefined>();
|
|
60
|
+
for (const proj of data.projects || []) {
|
|
61
|
+
const s = proj.slug?.current || proj.slug;
|
|
62
|
+
if (s && slugs.includes(s)) {
|
|
63
|
+
map.set(s, proj.thumbnail_path || undefined);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
setThumbMap(map);
|
|
67
|
+
})
|
|
68
|
+
.catch(() => { /* silent */ });
|
|
69
|
+
}, [slugs.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
70
|
+
|
|
71
|
+
return thumbMap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================
|
|
75
|
+
// Shared project grid card
|
|
76
|
+
// ============================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Project card with optional thumbnail background.
|
|
80
|
+
* Supports two sizing modes:
|
|
81
|
+
* - `aspectRatio` (preferred): CSS aspect-ratio for responsive sizing that matches public site
|
|
82
|
+
* - `height` (legacy fallback): fixed px height
|
|
83
|
+
*/
|
|
84
|
+
export function ProjectGridCard({
|
|
85
|
+
slug,
|
|
86
|
+
thumbPath,
|
|
87
|
+
customThumb,
|
|
88
|
+
height,
|
|
89
|
+
aspectRatio,
|
|
90
|
+
borderRadius,
|
|
91
|
+
style,
|
|
92
|
+
className,
|
|
93
|
+
}: {
|
|
94
|
+
slug: string;
|
|
95
|
+
thumbPath?: string;
|
|
96
|
+
customThumb?: string;
|
|
97
|
+
height?: number;
|
|
98
|
+
aspectRatio?: string;
|
|
99
|
+
borderRadius?: string;
|
|
100
|
+
style?: React.CSSProperties;
|
|
101
|
+
className?: string;
|
|
102
|
+
}) {
|
|
103
|
+
const imgPath = customThumb || thumbPath;
|
|
104
|
+
const src = imgPath ? adminThumbUrl(imgPath) : undefined;
|
|
105
|
+
const [fallback, setFallback] = useState(false);
|
|
106
|
+
const fullSrc = imgPath ? adminAssetUrl(imgPath) : undefined;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={`relative overflow-hidden flex items-end ${className || ""}`}
|
|
111
|
+
style={{
|
|
112
|
+
...(aspectRatio ? { aspectRatio } : { height: height || 160 }),
|
|
113
|
+
borderRadius: borderRadius ? `${borderRadius}px` : undefined,
|
|
114
|
+
backgroundColor: "#e5e5e5",
|
|
115
|
+
...style,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{src && (
|
|
119
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
120
|
+
<img
|
|
121
|
+
src={fallback && fullSrc ? fullSrc : src}
|
|
122
|
+
alt=""
|
|
123
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
124
|
+
onError={() => { if (!fallback) setFallback(true); }}
|
|
125
|
+
draggable={false}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
<span
|
|
129
|
+
className="relative z-[1] text-[10px] truncate px-3 pb-2"
|
|
130
|
+
style={{ color: src ? "rgba(255,255,255,0.8)" : "#a3a3a3", textShadow: src ? "0 1px 3px rgba(0,0,0,0.5)" : "none" }}
|
|
131
|
+
>
|
|
132
|
+
{slug}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================
|
|
139
|
+
// Placeholder for unknown block types
|
|
140
|
+
// ============================================
|
|
141
|
+
|
|
142
|
+
export function LivePlaceholder({ type }: { type: string }) {
|
|
143
|
+
const label = type
|
|
144
|
+
.replace("Block", "")
|
|
145
|
+
.replace(/([A-Z])/g, " $1")
|
|
146
|
+
.trim();
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="border border-dashed border-neutral-700 rounded bg-neutral-900/30 p-6 flex items-center justify-center">
|
|
150
|
+
<span className="text-xs text-neutral-500">{label}</span>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|