@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,274 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColorSwatchPicker — Dropdown color picker used in block editors.
|
|
5
|
+
* Shows the user's palette swatches + common colors + custom picker.
|
|
6
|
+
*
|
|
7
|
+
* Used in: SettingsPanel (row/block bg, border), TextBlockEditor (text color),
|
|
8
|
+
* CoverBlockEditor (text color), and any future color field.
|
|
9
|
+
*
|
|
10
|
+
* The dropdown uses a portal + fixed positioning so it is never clipped
|
|
11
|
+
* by ancestor `overflow: auto/hidden` containers (e.g. SettingsPanel).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
15
|
+
import { createPortal } from "react-dom";
|
|
16
|
+
import ColorPicker, { isValidHex } from "./ColorPicker";
|
|
17
|
+
import type { ColorSwatch } from "../../lib/sanity/types";
|
|
18
|
+
|
|
19
|
+
// Common neutral colors always available
|
|
20
|
+
const COMMON_COLORS = [
|
|
21
|
+
"#ffffff", "#f5f5f5", "#e5e5e5", "#a3a3a3",
|
|
22
|
+
"#525252", "#262626", "#171717", "#000000",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
interface ColorSwatchPickerProps {
|
|
26
|
+
/** Current color value (hex string or empty) */
|
|
27
|
+
value: string;
|
|
28
|
+
/** Callback when color changes */
|
|
29
|
+
onChange: (hex: string) => void;
|
|
30
|
+
/** Palette swatches from global styles */
|
|
31
|
+
swatches?: ColorSwatch[];
|
|
32
|
+
/** Optional label */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Allow clearing the color */
|
|
35
|
+
allowClear?: boolean;
|
|
36
|
+
/** Compact inline mode (no popover, just the swatch row) */
|
|
37
|
+
inline?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function ColorSwatchPicker({
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
swatches = [],
|
|
44
|
+
label,
|
|
45
|
+
allowClear = true,
|
|
46
|
+
inline = false,
|
|
47
|
+
}: ColorSwatchPickerProps) {
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
const [customOpen, setCustomOpen] = useState(false);
|
|
50
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
const portalRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
|
53
|
+
|
|
54
|
+
// Compute dropdown position from trigger button's bounding rect
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open || !containerRef.current) return;
|
|
57
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
58
|
+
// Position below the trigger, right-aligned
|
|
59
|
+
const panelWidth = 244; // min-w-[220px] + padding
|
|
60
|
+
let left = rect.right - panelWidth;
|
|
61
|
+
// Clamp to viewport left edge
|
|
62
|
+
if (left < 8) left = 8;
|
|
63
|
+
setDropdownPos({ top: rect.bottom + 4, left });
|
|
64
|
+
}, [open]);
|
|
65
|
+
|
|
66
|
+
// Close on outside click — check both container and portal
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open) return;
|
|
69
|
+
const handler = (e: MouseEvent) => {
|
|
70
|
+
const target = e.target as Node;
|
|
71
|
+
const inContainer = containerRef.current?.contains(target);
|
|
72
|
+
const inPortal = portalRef.current?.contains(target);
|
|
73
|
+
if (!inContainer && !inPortal) {
|
|
74
|
+
setOpen(false);
|
|
75
|
+
setCustomOpen(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
document.addEventListener("mousedown", handler);
|
|
79
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
80
|
+
}, [open]);
|
|
81
|
+
|
|
82
|
+
// Close on scroll of any ancestor (reposition would be complex; just close)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!open) return;
|
|
85
|
+
const handler = () => {
|
|
86
|
+
if (!containerRef.current) return;
|
|
87
|
+
// Recompute position on scroll instead of closing
|
|
88
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
89
|
+
const panelWidth = 244;
|
|
90
|
+
let left = rect.right - panelWidth;
|
|
91
|
+
if (left < 8) left = 8;
|
|
92
|
+
setDropdownPos({ top: rect.bottom + 4, left });
|
|
93
|
+
};
|
|
94
|
+
// Listen on capture phase to catch scrolls on any ancestor
|
|
95
|
+
window.addEventListener("scroll", handler, { capture: true, passive: true });
|
|
96
|
+
return () => window.removeEventListener("scroll", handler, { capture: true });
|
|
97
|
+
}, [open]);
|
|
98
|
+
|
|
99
|
+
const handleSelect = useCallback((hex: string) => {
|
|
100
|
+
onChange(hex);
|
|
101
|
+
if (!inline) {
|
|
102
|
+
setOpen(false);
|
|
103
|
+
setCustomOpen(false);
|
|
104
|
+
}
|
|
105
|
+
}, [onChange, inline]);
|
|
106
|
+
|
|
107
|
+
const handleClear = useCallback(() => {
|
|
108
|
+
onChange("");
|
|
109
|
+
setOpen(false);
|
|
110
|
+
}, [onChange]);
|
|
111
|
+
|
|
112
|
+
// ─── Trigger button (the colored swatch + hex label) ───
|
|
113
|
+
const trigger = (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => setOpen(!open)}
|
|
117
|
+
className="flex items-center gap-2 px-1.5 py-1 rounded-lg border border-neutral-200 bg-white hover:border-neutral-300 transition-colors cursor-pointer w-full"
|
|
118
|
+
>
|
|
119
|
+
<div
|
|
120
|
+
className="w-6 h-6 rounded-md border border-neutral-200 shrink-0"
|
|
121
|
+
style={{
|
|
122
|
+
background: value && isValidHex(value) ? value : "transparent",
|
|
123
|
+
backgroundImage: !value ? "linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%), linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%)" : undefined,
|
|
124
|
+
backgroundSize: !value ? "6px 6px" : undefined,
|
|
125
|
+
backgroundPosition: !value ? "0 0, 3px 3px" : undefined,
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
<span className="text-[11px] text-neutral-500 font-mono truncate">
|
|
129
|
+
{value ? value.toUpperCase() : "None"}
|
|
130
|
+
</span>
|
|
131
|
+
</button>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// ─── Dropdown panel ───
|
|
135
|
+
const panel = (
|
|
136
|
+
<div className="bg-white rounded-xl border border-neutral-200 p-3 shadow-xl min-w-[220px]">
|
|
137
|
+
|
|
138
|
+
{/* User palette swatches */}
|
|
139
|
+
{swatches.length > 0 && (
|
|
140
|
+
<div className="mb-3">
|
|
141
|
+
<div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-1.5">Palette</div>
|
|
142
|
+
<div className="flex flex-wrap gap-1.5">
|
|
143
|
+
{swatches.map((s, i) => (
|
|
144
|
+
<button
|
|
145
|
+
key={s._key || i}
|
|
146
|
+
onClick={() => handleSelect(s.hex)}
|
|
147
|
+
title={`${s.name}: ${s.hex}`}
|
|
148
|
+
className={`w-7 h-7 rounded-lg cursor-pointer transition-all ${
|
|
149
|
+
value === s.hex
|
|
150
|
+
? "ring-2 ring-[#076bff] ring-offset-1 ring-offset-white"
|
|
151
|
+
: "border border-neutral-200 hover:border-neutral-400"
|
|
152
|
+
}`}
|
|
153
|
+
style={{ background: s.hex }}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Common colors */}
|
|
161
|
+
<div className="mb-3">
|
|
162
|
+
<div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-1.5">Common</div>
|
|
163
|
+
<div className="flex flex-wrap gap-1.5">
|
|
164
|
+
{COMMON_COLORS.map((c) => (
|
|
165
|
+
<button
|
|
166
|
+
key={c}
|
|
167
|
+
onClick={() => handleSelect(c)}
|
|
168
|
+
title={c}
|
|
169
|
+
className={`w-7 h-7 rounded-lg cursor-pointer transition-all ${
|
|
170
|
+
value === c
|
|
171
|
+
? "ring-2 ring-[#076bff] ring-offset-1 ring-offset-white"
|
|
172
|
+
: "border border-neutral-200 hover:border-neutral-400"
|
|
173
|
+
}`}
|
|
174
|
+
style={{ background: c }}
|
|
175
|
+
/>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Custom color toggle */}
|
|
181
|
+
{!customOpen ? (
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => setCustomOpen(true)}
|
|
184
|
+
className="w-full py-1.5 rounded-lg border border-dashed border-neutral-300 text-neutral-400 text-[10px] uppercase tracking-widest cursor-pointer hover:border-neutral-400 hover:text-neutral-600 transition-colors"
|
|
185
|
+
>
|
|
186
|
+
Custom color
|
|
187
|
+
</button>
|
|
188
|
+
) : (
|
|
189
|
+
<div className="mt-1">
|
|
190
|
+
<ColorPicker
|
|
191
|
+
color={value || "#ffffff"}
|
|
192
|
+
onChange={(hex) => handleSelect(hex)}
|
|
193
|
+
onClose={() => setCustomOpen(false)}
|
|
194
|
+
confirmLabel="Apply"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Clear button */}
|
|
200
|
+
{allowClear && value && (
|
|
201
|
+
<button
|
|
202
|
+
onClick={handleClear}
|
|
203
|
+
className="w-full mt-2 py-1.5 rounded-lg border border-neutral-200 text-neutral-400 text-[10px] uppercase tracking-widest cursor-pointer hover:border-red-300 hover:text-red-500 transition-colors"
|
|
204
|
+
>
|
|
205
|
+
Clear color
|
|
206
|
+
</button>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div ref={containerRef} className="relative">
|
|
213
|
+
{label && (
|
|
214
|
+
<label className="text-[10px] text-neutral-500 uppercase tracking-wider block mb-1">
|
|
215
|
+
{label}
|
|
216
|
+
</label>
|
|
217
|
+
)}
|
|
218
|
+
{trigger}
|
|
219
|
+
{open && typeof document !== "undefined" &&
|
|
220
|
+
createPortal(
|
|
221
|
+
<div
|
|
222
|
+
ref={portalRef}
|
|
223
|
+
className="fixed z-[9999]"
|
|
224
|
+
style={{ top: dropdownPos.top, left: dropdownPos.left }}
|
|
225
|
+
>
|
|
226
|
+
{panel}
|
|
227
|
+
</div>,
|
|
228
|
+
document.body
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Hook: fetch palette swatches from admin styles API ───
|
|
236
|
+
|
|
237
|
+
let cachedSwatches: ColorSwatch[] | null = null;
|
|
238
|
+
let cachePromise: Promise<ColorSwatch[]> | null = null;
|
|
239
|
+
|
|
240
|
+
export function usePaletteSwatches(): ColorSwatch[] {
|
|
241
|
+
const [swatches, setSwatches] = useState<ColorSwatch[]>(cachedSwatches || []);
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (cachedSwatches) {
|
|
245
|
+
setSwatches(cachedSwatches);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!cachePromise) {
|
|
250
|
+
cachePromise = fetch("/api/admin/styles")
|
|
251
|
+
.then((res) => (res.ok ? res.json() : { styles: { colors: { swatches: [] } } }))
|
|
252
|
+
.then((data) => {
|
|
253
|
+
const s = data?.styles?.colors?.swatches || [];
|
|
254
|
+
cachedSwatches = s;
|
|
255
|
+
return s;
|
|
256
|
+
})
|
|
257
|
+
.catch(() => {
|
|
258
|
+
/* Color palette unavailable — use empty swatches */
|
|
259
|
+
cachedSwatches = [];
|
|
260
|
+
return [];
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cachePromise.then((s) => setSwatches(s));
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
267
|
+
return swatches;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Invalidate the cached swatches (call after saving palette) */
|
|
271
|
+
export function invalidatePaletteCache() {
|
|
272
|
+
cachedSwatches = null;
|
|
273
|
+
cachePromise = null;
|
|
274
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
import { useColumnDrag, type UseColumnDragReturn } from "./hooks/useColumnDrag";
|
|
5
|
+
import ColumnDragOverlay from "./ColumnDragOverlay";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Context
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
const ColumnDragContext = createContext<UseColumnDragReturn | null>(null);
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// Provider
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
interface ColumnDragProviderProps {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ColumnDragProvider({ children }: ColumnDragProviderProps) {
|
|
22
|
+
const columnDrag = useColumnDrag();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ColumnDragContext.Provider value={columnDrag}>
|
|
26
|
+
{children}
|
|
27
|
+
{columnDrag.isDragging &&
|
|
28
|
+
columnDrag.overlayPosition &&
|
|
29
|
+
columnDrag.draggedSectionKey &&
|
|
30
|
+
columnDrag.draggedColumnKey && (
|
|
31
|
+
<ColumnDragOverlay
|
|
32
|
+
sectionKey={columnDrag.draggedSectionKey}
|
|
33
|
+
columnKey={columnDrag.draggedColumnKey}
|
|
34
|
+
position={columnDrag.overlayPosition}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</ColumnDragContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// Consumer Hook
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
export function useColumnDragContext(): UseColumnDragReturn {
|
|
46
|
+
const context = useContext(ColumnDragContext);
|
|
47
|
+
if (!context) {
|
|
48
|
+
throw new Error("useColumnDragContext must be used within a ColumnDragProvider");
|
|
49
|
+
}
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
6
|
+
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
7
|
+
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
8
|
+
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
9
|
+
|
|
10
|
+
interface ColumnDragOverlayProps {
|
|
11
|
+
sectionKey: string;
|
|
12
|
+
columnKey: string;
|
|
13
|
+
position: { x: number; y: number };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
17
|
+
sectionKey,
|
|
18
|
+
columnKey,
|
|
19
|
+
position,
|
|
20
|
+
}: ColumnDragOverlayProps) {
|
|
21
|
+
const rows = useBuilderStore((s) => s.rows);
|
|
22
|
+
const item = rows.find((r) => r._key === sectionKey);
|
|
23
|
+
if (!item || !isPageSectionV2(item)) return null;
|
|
24
|
+
|
|
25
|
+
const v2Section = item as PageSectionV2;
|
|
26
|
+
const col = v2Section.columns?.find((c) => c._key === columnKey);
|
|
27
|
+
if (!col) return null;
|
|
28
|
+
|
|
29
|
+
const blockCount = (col.blocks || []).length;
|
|
30
|
+
const gridColumns = v2Section.settings.grid_columns || 12;
|
|
31
|
+
|
|
32
|
+
const overlay = (
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
position: "fixed",
|
|
36
|
+
left: position.x,
|
|
37
|
+
top: position.y,
|
|
38
|
+
transform: "translate(-50%, -50%)",
|
|
39
|
+
pointerEvents: "none",
|
|
40
|
+
zIndex: 99999,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
width: 180,
|
|
46
|
+
minHeight: 80,
|
|
47
|
+
background: "rgba(7, 107, 255, 0.08)",
|
|
48
|
+
backdropFilter: "blur(8px)",
|
|
49
|
+
opacity: 0.85,
|
|
50
|
+
borderRadius: 8,
|
|
51
|
+
border: `2px solid ${BUILDER_BLUE}`,
|
|
52
|
+
boxShadow: "0 8px 32px rgba(7, 107, 255, 0.3)",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{/* Column header badge */}
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: 8,
|
|
61
|
+
padding: "8px 12px",
|
|
62
|
+
borderBottom: "1px solid rgba(7, 107, 255, 0.2)",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill={BUILDER_BLUE}>
|
|
66
|
+
<circle cx="3" cy="3" r="1" />
|
|
67
|
+
<circle cx="7" cy="3" r="1" />
|
|
68
|
+
<circle cx="3" cy="7" r="1" />
|
|
69
|
+
<circle cx="7" cy="7" r="1" />
|
|
70
|
+
</svg>
|
|
71
|
+
<span style={{ fontSize: 12, color: "white", fontWeight: 500 }}>
|
|
72
|
+
Column {col.span}/{gridColumns}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
{/* Block count indicators */}
|
|
76
|
+
<div style={{ padding: "8px 12px" }}>
|
|
77
|
+
{blockCount > 0 ? (
|
|
78
|
+
<>
|
|
79
|
+
{Array.from({ length: Math.min(blockCount, 3) }).map((_, i) => (
|
|
80
|
+
<div
|
|
81
|
+
key={i}
|
|
82
|
+
style={{
|
|
83
|
+
height: 12,
|
|
84
|
+
borderRadius: 4,
|
|
85
|
+
background: "rgba(255,255,255,0.1)",
|
|
86
|
+
marginBottom: i < Math.min(blockCount, 3) - 1 ? 6 : 0,
|
|
87
|
+
width: `${90 - i * 15}%`,
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
{blockCount > 3 && (
|
|
92
|
+
<span style={{ fontSize: 10, color: "rgba(255,255,255,0.4)", marginTop: 4, display: "block" }}>
|
|
93
|
+
+{blockCount - 3} more
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
97
|
+
) : (
|
|
98
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "8px 0" }}>
|
|
99
|
+
<span style={{ fontSize: 10, color: "rgba(255,255,255,0.3)" }}>Empty column</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return createPortal(overlay, document.body);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export default ColumnDragOverlay;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CustomSectionInstanceCard — Builder canvas card for custom section instances.
|
|
5
|
+
*
|
|
6
|
+
* Renders a live preview of the linked section content in the builder canvas.
|
|
7
|
+
* Edit/Detach actions are in the SettingsPanel (not here).
|
|
8
|
+
*
|
|
9
|
+
* Session 108: Created as part of Custom Sections Phase 2.
|
|
10
|
+
* Session 110: M1 fix — auto-sync stale cached titles.
|
|
11
|
+
* Session 130: Removed violet header bar — Edit/Detach moved to SettingsPanel.
|
|
12
|
+
* Exposed sectionData via callback for SettingsPanel to use.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState, useEffect } from "react";
|
|
16
|
+
import type { CustomSectionInstance, PageSectionV2 } from "../../lib/sanity/types";
|
|
17
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
18
|
+
import SectionV2Canvas from "./SectionV2Canvas";
|
|
19
|
+
|
|
20
|
+
interface CustomSectionInstanceCardProps {
|
|
21
|
+
instance: CustomSectionInstance;
|
|
22
|
+
onAddBlockTarget?: (sectionKey: string, colKey: string, insertIndex?: number) => void;
|
|
23
|
+
/** Callback to expose fetched section data to parent (for SettingsPanel actions) */
|
|
24
|
+
onSectionDataLoaded?: (data: PageSectionV2 | null) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function CustomSectionInstanceCard({
|
|
28
|
+
instance,
|
|
29
|
+
onAddBlockTarget,
|
|
30
|
+
onSectionDataLoaded,
|
|
31
|
+
}: CustomSectionInstanceCardProps) {
|
|
32
|
+
const cacheSettings = useBuilderStore((s) => s.cacheCustomSectionSettings);
|
|
33
|
+
const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
// Fetch the section data for preview
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
setLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
|
|
43
|
+
fetch(`/api/custom-sections/${instance.custom_section_id}`)
|
|
44
|
+
.then((res) => {
|
|
45
|
+
if (!res.ok) throw new Error("Section not found");
|
|
46
|
+
return res.json();
|
|
47
|
+
})
|
|
48
|
+
.then((data) => {
|
|
49
|
+
if (!cancelled && data.section) {
|
|
50
|
+
setSectionData(data.section);
|
|
51
|
+
onSectionDataLoaded?.(data.section);
|
|
52
|
+
// Cache base settings so SortableRow can merge with per-instance overrides
|
|
53
|
+
if (data.section.settings) {
|
|
54
|
+
cacheSettings(instance.custom_section_id, data.section.settings);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.catch(() => {
|
|
59
|
+
if (!cancelled) setError("Could not load section");
|
|
60
|
+
})
|
|
61
|
+
.finally(() => {
|
|
62
|
+
if (!cancelled) setLoading(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return () => { cancelled = true; };
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [instance.custom_section_id]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="relative">
|
|
71
|
+
{loading ? (
|
|
72
|
+
<div className="flex items-center justify-center py-12">
|
|
73
|
+
<p className="text-xs text-neutral-400 animate-pulse">Loading section...</p>
|
|
74
|
+
</div>
|
|
75
|
+
) : error ? (
|
|
76
|
+
<div className="flex items-center justify-center py-12">
|
|
77
|
+
<p className="text-xs text-red-400">{error}</p>
|
|
78
|
+
</div>
|
|
79
|
+
) : sectionData ? (
|
|
80
|
+
<div className="pointer-events-none opacity-90">
|
|
81
|
+
<SectionV2Canvas
|
|
82
|
+
section={(() => {
|
|
83
|
+
let merged = instance.settings_overrides
|
|
84
|
+
? { ...sectionData, settings: { ...sectionData.settings, ...instance.settings_overrides } }
|
|
85
|
+
: sectionData;
|
|
86
|
+
if (instance.responsive_overrides) {
|
|
87
|
+
merged = { ...merged, responsive: { ...merged.responsive, ...instance.responsive_overrides } };
|
|
88
|
+
}
|
|
89
|
+
return merged;
|
|
90
|
+
})()}
|
|
91
|
+
onAddBlockTarget={onAddBlockTarget || (() => {})}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { DeviceViewport } from "../../lib/builder/types";
|
|
5
|
+
import { DEVICE_WIDTHS } from "../../lib/builder/types";
|
|
6
|
+
import { ADMIN_ACCENT } from "../../lib/builder/constants";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// DeviceFrame — Responsive preview frame
|
|
10
|
+
// ============================================
|
|
11
|
+
// Wraps content in a device-sized frame with header label.
|
|
12
|
+
// Active frame is editable; inactive frames are read-only mirrors.
|
|
13
|
+
|
|
14
|
+
const DEVICE_LABELS: Record<DeviceViewport, string> = {
|
|
15
|
+
desktop: "Desktop",
|
|
16
|
+
tablet: "Tablet",
|
|
17
|
+
phone: "Phone",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEVICE_ICONS: Record<DeviceViewport, ReactNode> = {
|
|
21
|
+
desktop: (
|
|
22
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
23
|
+
<rect x="1" y="1.5" width="10" height="7" rx="1" stroke="currentColor" strokeWidth="1.2" />
|
|
24
|
+
<path d="M4 10.5H8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
25
|
+
<path d="M6 8.5V10.5" stroke="currentColor" strokeWidth="1.2" />
|
|
26
|
+
</svg>
|
|
27
|
+
),
|
|
28
|
+
tablet: (
|
|
29
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
30
|
+
<rect x="2" y="0.5" width="8" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
|
|
31
|
+
<circle cx="6" cy="9.5" r="0.6" fill="currentColor" />
|
|
32
|
+
</svg>
|
|
33
|
+
),
|
|
34
|
+
phone: (
|
|
35
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
36
|
+
<rect x="3" y="0.5" width="6" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
|
|
37
|
+
<path d="M5 1.5H7" stroke="currentColor" strokeWidth="0.8" strokeLinecap="round" />
|
|
38
|
+
<circle cx="6" cy="9.5" r="0.5" fill="currentColor" />
|
|
39
|
+
</svg>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface DeviceFrameProps {
|
|
44
|
+
device: DeviceViewport;
|
|
45
|
+
isActive: boolean;
|
|
46
|
+
onActivate: () => void;
|
|
47
|
+
onDoubleClick?: () => void;
|
|
48
|
+
backgroundColor: string;
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function DeviceFrame({
|
|
53
|
+
device,
|
|
54
|
+
isActive,
|
|
55
|
+
onActivate,
|
|
56
|
+
onDoubleClick,
|
|
57
|
+
backgroundColor,
|
|
58
|
+
children,
|
|
59
|
+
}: DeviceFrameProps) {
|
|
60
|
+
const width = DEVICE_WIDTHS[device];
|
|
61
|
+
const label = DEVICE_LABELS[device];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className="relative flex-shrink-0"
|
|
66
|
+
style={{ width }}
|
|
67
|
+
>
|
|
68
|
+
{/* Frame header — click to activate */}
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={(e) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
onActivate();
|
|
74
|
+
}}
|
|
75
|
+
onDoubleClick={(e) => {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
onDoubleClick?.();
|
|
78
|
+
}}
|
|
79
|
+
className="flex items-center gap-2 w-full px-3 py-1.5 rounded-t-lg border border-b-0 text-left transition-colors"
|
|
80
|
+
style={{
|
|
81
|
+
backgroundColor: isActive ? "#ffffff" : "#fafafa",
|
|
82
|
+
borderColor: isActive ? ADMIN_ACCENT : "#d4d4d4",
|
|
83
|
+
cursor: isActive ? "default" : "pointer",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<span
|
|
87
|
+
style={{ color: isActive ? ADMIN_ACCENT : "#a3a3a3" }}
|
|
88
|
+
className="transition-colors"
|
|
89
|
+
>
|
|
90
|
+
{DEVICE_ICONS[device]}
|
|
91
|
+
</span>
|
|
92
|
+
<span
|
|
93
|
+
className="text-[10px] font-medium transition-colors"
|
|
94
|
+
style={{ color: isActive ? ADMIN_ACCENT : "#737373" }}
|
|
95
|
+
>
|
|
96
|
+
{label}
|
|
97
|
+
</span>
|
|
98
|
+
<span className="text-[10px] text-neutral-400 ml-auto">
|
|
99
|
+
{width}px
|
|
100
|
+
</span>
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
{/* Frame content area — contain: layout style paint limits browser work to this subtree */}
|
|
104
|
+
<div
|
|
105
|
+
className="border rounded-b-lg transition-[border-color,box-shadow,opacity]"
|
|
106
|
+
style={{
|
|
107
|
+
width,
|
|
108
|
+
backgroundColor,
|
|
109
|
+
borderColor: isActive ? ADMIN_ACCENT : "#d4d4d4",
|
|
110
|
+
borderTopWidth: 0,
|
|
111
|
+
minHeight: 400,
|
|
112
|
+
boxShadow: isActive
|
|
113
|
+
? "0 4px 24px rgba(7, 107, 255, 0.08), 0 1px 4px rgba(0,0,0,0.05)"
|
|
114
|
+
: "0 1px 4px rgba(0,0,0,0.04)",
|
|
115
|
+
opacity: isActive ? 1 : 0.75,
|
|
116
|
+
contain: "layout style",
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|