@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,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 };
|