@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,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from "react";
|
|
4
|
+
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
|
+
import type { WizardStepProps } from "./SetupWizard";
|
|
6
|
+
|
|
7
|
+
// ── Icons ──
|
|
8
|
+
|
|
9
|
+
function CheckCircle() {
|
|
10
|
+
return (
|
|
11
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
12
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
13
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Spinner() {
|
|
19
|
+
return (
|
|
20
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
|
|
21
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function R2Icon() {
|
|
27
|
+
return (
|
|
28
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F6821F" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
29
|
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
30
|
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
|
31
|
+
<line x1="12" y1="22.08" x2="12" y2="12" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Types ──
|
|
37
|
+
|
|
38
|
+
interface R2Status {
|
|
39
|
+
connected: boolean;
|
|
40
|
+
bucket_name: string | null;
|
|
41
|
+
public_url: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type StepState = "loading" | "disconnected" | "connected";
|
|
45
|
+
|
|
46
|
+
// ── Component ──
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Step 3 — Storage (Cloudflare R2 Connection)
|
|
50
|
+
*
|
|
51
|
+
* Reuses the same R2 connect API as /admin/storage.
|
|
52
|
+
* Shows a simplified 5-field form or "Connected" status.
|
|
53
|
+
*/
|
|
54
|
+
export function StorageStep({ onNext, onBack }: WizardStepProps) {
|
|
55
|
+
const [state, setState] = useState<StepState>("loading");
|
|
56
|
+
const [r2Status, setR2Status] = useState<R2Status | null>(null);
|
|
57
|
+
|
|
58
|
+
// Form fields
|
|
59
|
+
const [endpoint, setEndpoint] = useState("");
|
|
60
|
+
const [bucketName, setBucketName] = useState("");
|
|
61
|
+
const [accessKeyId, setAccessKeyId] = useState("");
|
|
62
|
+
const [secretAccessKey, setSecretAccessKey] = useState("");
|
|
63
|
+
const [publicUrl, setPublicUrl] = useState("");
|
|
64
|
+
|
|
65
|
+
const [testing, setTesting] = useState(false);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
// Check R2 status on mount
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
fetch("/api/admin/r2/status")
|
|
71
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
72
|
+
.then((data: R2Status | null) => {
|
|
73
|
+
if (data?.connected) {
|
|
74
|
+
setR2Status(data);
|
|
75
|
+
setState("connected");
|
|
76
|
+
} else {
|
|
77
|
+
setState("disconnected");
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch(() => setState("disconnected"));
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleConnect = useCallback(
|
|
84
|
+
async (e: React.FormEvent) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
setTesting(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/admin/r2/connect", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
...csrfHeaders(),
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
endpoint: endpoint.trim(),
|
|
98
|
+
bucketName: bucketName.trim(),
|
|
99
|
+
accessKeyId: accessKeyId.trim(),
|
|
100
|
+
secretAccessKey: secretAccessKey.trim(),
|
|
101
|
+
publicUrl: publicUrl.trim(),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
setError(data.error || "Connection failed");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Success — update state and auto-advance
|
|
112
|
+
setR2Status({
|
|
113
|
+
connected: true,
|
|
114
|
+
bucket_name: bucketName.trim(),
|
|
115
|
+
public_url: publicUrl.trim(),
|
|
116
|
+
});
|
|
117
|
+
setState("connected");
|
|
118
|
+
|
|
119
|
+
// Small delay so user sees the success state
|
|
120
|
+
setTimeout(() => onNext(), 800);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
setError(err instanceof Error ? err.message : "Connection failed");
|
|
123
|
+
} finally {
|
|
124
|
+
setTesting(false);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[endpoint, bucketName, accessKeyId, secretAccessKey, publicUrl, onNext]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const inputClass =
|
|
131
|
+
"w-full rounded-lg border border-black/[0.08] bg-white px-3 py-2 text-sm text-[#333] placeholder:text-[#bbb] focus:outline-none focus:ring-2 focus:ring-[#076bff]/20 focus:border-[#076bff]/40";
|
|
132
|
+
const labelClass = "block text-xs font-medium text-[#666] mb-1";
|
|
133
|
+
|
|
134
|
+
if (state === "loading") {
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
137
|
+
<Spinner />
|
|
138
|
+
<p className="text-[#999] text-xs mt-3">Checking storage status...</p>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="pt-8">
|
|
145
|
+
<div className="flex items-center gap-2.5 mb-1">
|
|
146
|
+
<R2Icon />
|
|
147
|
+
<h2 className="text-lg font-semibold text-[#111]">Storage</h2>
|
|
148
|
+
</div>
|
|
149
|
+
<p className="text-sm text-[#666] mb-8">
|
|
150
|
+
Connect Cloudflare R2 to store images, videos, and other assets.
|
|
151
|
+
</p>
|
|
152
|
+
|
|
153
|
+
{/* Already connected */}
|
|
154
|
+
{state === "connected" && r2Status && (
|
|
155
|
+
<div className="bg-white rounded-xl border border-green-200 p-5 mb-6">
|
|
156
|
+
<div className="flex items-center gap-3 mb-3">
|
|
157
|
+
<CheckCircle />
|
|
158
|
+
<span className="text-sm font-medium text-[#333]">
|
|
159
|
+
Cloudflare R2 Connected
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
{r2Status.bucket_name && (
|
|
163
|
+
<p className="text-xs text-[#999]">
|
|
164
|
+
Bucket: <span className="font-mono">{r2Status.bucket_name}</span>
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
{r2Status.public_url && (
|
|
168
|
+
<p className="text-xs text-[#999] mt-0.5">
|
|
169
|
+
URL: <span className="font-mono">{r2Status.public_url}</span>
|
|
170
|
+
</p>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* Connection form */}
|
|
176
|
+
{state === "disconnected" && (
|
|
177
|
+
<form onSubmit={handleConnect} className="space-y-3 mb-6">
|
|
178
|
+
<div className="bg-white rounded-xl border border-black/[0.06] p-5 space-y-3">
|
|
179
|
+
<div>
|
|
180
|
+
<label className={labelClass}>S3 Endpoint</label>
|
|
181
|
+
<input
|
|
182
|
+
type="url"
|
|
183
|
+
value={endpoint}
|
|
184
|
+
onChange={(e) => setEndpoint(e.target.value)}
|
|
185
|
+
placeholder="https://<account-id>.r2.cloudflarestorage.com"
|
|
186
|
+
className={inputClass}
|
|
187
|
+
required
|
|
188
|
+
/>
|
|
189
|
+
<p className="text-[11px] text-[#bbb] mt-0.5">
|
|
190
|
+
Found in Cloudflare R2 dashboard → Account Details
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div>
|
|
195
|
+
<label className={labelClass}>Bucket Name</label>
|
|
196
|
+
<input
|
|
197
|
+
type="text"
|
|
198
|
+
value={bucketName}
|
|
199
|
+
onChange={(e) => setBucketName(e.target.value)}
|
|
200
|
+
placeholder="my-assets"
|
|
201
|
+
className={inputClass}
|
|
202
|
+
required
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div>
|
|
207
|
+
<label className={labelClass}>Access Key ID</label>
|
|
208
|
+
<input
|
|
209
|
+
type="text"
|
|
210
|
+
value={accessKeyId}
|
|
211
|
+
onChange={(e) => setAccessKeyId(e.target.value)}
|
|
212
|
+
placeholder="R2 API token Access Key ID"
|
|
213
|
+
className={inputClass}
|
|
214
|
+
required
|
|
215
|
+
autoComplete="off"
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div>
|
|
220
|
+
<label className={labelClass}>Secret Access Key</label>
|
|
221
|
+
<input
|
|
222
|
+
type="password"
|
|
223
|
+
value={secretAccessKey}
|
|
224
|
+
onChange={(e) => setSecretAccessKey(e.target.value)}
|
|
225
|
+
placeholder="R2 API token Secret Access Key"
|
|
226
|
+
className={inputClass}
|
|
227
|
+
required
|
|
228
|
+
autoComplete="off"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div>
|
|
233
|
+
<label className={labelClass}>Public Bucket URL</label>
|
|
234
|
+
<input
|
|
235
|
+
type="url"
|
|
236
|
+
value={publicUrl}
|
|
237
|
+
onChange={(e) => setPublicUrl(e.target.value)}
|
|
238
|
+
placeholder="https://pub-xxx.r2.dev or https://assets.example.com"
|
|
239
|
+
className={inputClass}
|
|
240
|
+
required
|
|
241
|
+
/>
|
|
242
|
+
<p className="text-[11px] text-[#bbb] mt-0.5">
|
|
243
|
+
R2.dev subdomain or custom domain with public access enabled
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{error && (
|
|
249
|
+
<div className="p-3 rounded-lg bg-red-50 border border-red-200">
|
|
250
|
+
<p className="text-xs text-red-700">{error}</p>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
<button
|
|
255
|
+
type="submit"
|
|
256
|
+
disabled={testing}
|
|
257
|
+
className="inline-flex items-center gap-2 rounded-lg bg-[#F6821F] text-white text-sm font-medium px-4 py-2.5 hover:bg-[#F6821F]/90 transition-colors disabled:opacity-50"
|
|
258
|
+
>
|
|
259
|
+
{testing ? (
|
|
260
|
+
<>
|
|
261
|
+
<Spinner />
|
|
262
|
+
Testing Connection...
|
|
263
|
+
</>
|
|
264
|
+
) : (
|
|
265
|
+
"Test & Connect"
|
|
266
|
+
)}
|
|
267
|
+
</button>
|
|
268
|
+
</form>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{/* Actions */}
|
|
272
|
+
<div className="flex items-center justify-between">
|
|
273
|
+
<div>
|
|
274
|
+
{onBack && (
|
|
275
|
+
<button
|
|
276
|
+
onClick={onBack}
|
|
277
|
+
className="px-4 py-2 text-sm text-[#666] hover:text-[#333] transition-colors"
|
|
278
|
+
>
|
|
279
|
+
Back
|
|
280
|
+
</button>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="flex items-center gap-3">
|
|
285
|
+
{/* Skip — storage can be configured later */}
|
|
286
|
+
{state === "disconnected" && (
|
|
287
|
+
<button
|
|
288
|
+
onClick={onNext}
|
|
289
|
+
className="px-4 py-2 text-sm text-[#999] hover:text-[#333] transition-colors"
|
|
290
|
+
>
|
|
291
|
+
Skip for now
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Next (when already connected) */}
|
|
296
|
+
{state === "connected" && (
|
|
297
|
+
<button
|
|
298
|
+
onClick={onNext}
|
|
299
|
+
className="px-5 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
|
|
300
|
+
>
|
|
301
|
+
Next
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getSiteConfig } from "../../../lib/config";
|
|
4
|
+
import type { WizardStepProps } from "./SetupWizard";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 1 — Welcome
|
|
8
|
+
*
|
|
9
|
+
* Displays the site name, a welcome message, and a brief overview
|
|
10
|
+
* of what the wizard will configure.
|
|
11
|
+
*/
|
|
12
|
+
export function WelcomeStep({ onNext }: WizardStepProps) {
|
|
13
|
+
const config = getSiteConfig();
|
|
14
|
+
const primaryColor = config.palette.primary;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="pt-12 text-center">
|
|
18
|
+
{/* Color accent bar */}
|
|
19
|
+
<div
|
|
20
|
+
className="w-12 h-1 rounded-full mx-auto mb-8"
|
|
21
|
+
style={{ backgroundColor: primaryColor }}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<h1 className="text-2xl font-semibold text-[#111] mb-2">
|
|
25
|
+
Welcome to {config.name}
|
|
26
|
+
</h1>
|
|
27
|
+
|
|
28
|
+
<p className="text-sm text-[#666] mb-10 max-w-sm mx-auto leading-relaxed">
|
|
29
|
+
Let's set up your site in a few quick steps. We'll connect your
|
|
30
|
+
database, configure storage, and set up your brand.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
{/* What we'll configure */}
|
|
34
|
+
<div className="text-left bg-white rounded-xl border border-black/[0.06] p-6 mb-10">
|
|
35
|
+
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#999] mb-4">
|
|
36
|
+
What we'll configure
|
|
37
|
+
</h2>
|
|
38
|
+
<ul className="space-y-3">
|
|
39
|
+
{[
|
|
40
|
+
{
|
|
41
|
+
icon: (
|
|
42
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
43
|
+
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
|
44
|
+
<path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" />
|
|
45
|
+
<path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" />
|
|
46
|
+
</svg>
|
|
47
|
+
),
|
|
48
|
+
label: "Database",
|
|
49
|
+
desc: "Connect to Sanity CMS for content storage",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
icon: (
|
|
53
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
54
|
+
<path d="M22 12H2" />
|
|
55
|
+
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" />
|
|
56
|
+
<line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
|
|
57
|
+
<line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
|
|
58
|
+
</svg>
|
|
59
|
+
),
|
|
60
|
+
label: "Storage",
|
|
61
|
+
desc: "Set up Cloudflare R2 for images and videos",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
icon: (
|
|
65
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
66
|
+
<circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
|
|
67
|
+
<circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
|
|
68
|
+
<circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
|
|
69
|
+
<circle cx="6.5" cy="12" r="0.5" fill="currentColor" />
|
|
70
|
+
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" />
|
|
71
|
+
</svg>
|
|
72
|
+
),
|
|
73
|
+
label: "Branding",
|
|
74
|
+
desc: "Upload fonts, set colors, and configure your identity",
|
|
75
|
+
},
|
|
76
|
+
].map((item) => (
|
|
77
|
+
<li key={item.label} className="flex items-start gap-3">
|
|
78
|
+
<span className="text-[#076bff] mt-0.5 shrink-0">{item.icon}</span>
|
|
79
|
+
<div>
|
|
80
|
+
<span className="text-sm font-medium text-[#333]">{item.label}</span>
|
|
81
|
+
<p className="text-xs text-[#999] mt-0.5">{item.desc}</p>
|
|
82
|
+
</div>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
onClick={onNext}
|
|
90
|
+
className="px-6 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
|
|
91
|
+
>
|
|
92
|
+
Get Started
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Wizard — onboarding flow for new Visual Builder instances.
|
|
3
|
+
*/
|
|
4
|
+
export { SetupWizard } from "./SetupWizard";
|
|
5
|
+
export { WelcomeStep } from "./WelcomeStep";
|
|
6
|
+
export { DatabaseStep } from "./DatabaseStep";
|
|
7
|
+
export { StorageStep } from "./StorageStep";
|
|
8
|
+
export { BrandingStep } from "./BrandingStep";
|
|
9
|
+
export { DoneStep } from "./DoneStep";
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import ColorPicker, { isValidHex } from "../../../components/builder/ColorPicker";
|
|
5
|
+
import { invalidatePaletteCache } from "../../../components/builder/ColorSwatchPicker";
|
|
6
|
+
import type { SiteStyles, ColorSwatch } from "../../../lib/sanity/types";
|
|
7
|
+
import { Section, SaveButton } from "./shared";
|
|
8
|
+
|
|
9
|
+
function getContrastColor(hex: string): string {
|
|
10
|
+
if (!isValidHex(hex)) return "#000";
|
|
11
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
12
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
13
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
14
|
+
return (r * 0.299 + g * 0.587 + b * 0.114) > 160 ? "#000000" : "#ffffff";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ColorsEditor({
|
|
18
|
+
colors,
|
|
19
|
+
onSave,
|
|
20
|
+
saving,
|
|
21
|
+
}: {
|
|
22
|
+
colors?: SiteStyles["colors"];
|
|
23
|
+
onSave: (data: Record<string, unknown>) => void;
|
|
24
|
+
saving: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
const [swatches, setSwatches] = useState<ColorSwatch[]>(colors?.swatches || []);
|
|
27
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
28
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
29
|
+
const [renamingIndex, setRenamingIndex] = useState<number | null>(null);
|
|
30
|
+
const [renameValue, setRenameValue] = useState("");
|
|
31
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setSwatches(colors?.swatches || []);
|
|
35
|
+
}, [colors]);
|
|
36
|
+
|
|
37
|
+
const addSwatch = (hex: string) => {
|
|
38
|
+
if (editingIndex !== null) {
|
|
39
|
+
setSwatches((prev) => prev.map((s, i) => i === editingIndex ? { ...s, hex } : s));
|
|
40
|
+
setEditingIndex(null);
|
|
41
|
+
} else {
|
|
42
|
+
const key = crypto.randomUUID().slice(0, 8);
|
|
43
|
+
setSwatches((prev) => [...prev, { _key: key, name: `Color ${prev.length + 1}`, hex }]);
|
|
44
|
+
}
|
|
45
|
+
setPickerOpen(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const removeSwatch = (index: number) => {
|
|
49
|
+
setSwatches((prev) => prev.filter((_, i) => i !== index));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const startRename = (index: number) => {
|
|
53
|
+
setRenamingIndex(index);
|
|
54
|
+
setRenameValue(swatches[index].name);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const finishRename = () => {
|
|
58
|
+
if (renamingIndex !== null && renameValue.trim()) {
|
|
59
|
+
setSwatches((prev) => prev.map((s, i) => i === renamingIndex ? { ...s, name: renameValue.trim() } : s));
|
|
60
|
+
}
|
|
61
|
+
setRenamingIndex(null);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleSave = () => {
|
|
65
|
+
onSave({
|
|
66
|
+
color_palette: swatches.map((s) => ({
|
|
67
|
+
_key: s._key || crypto.randomUUID().slice(0, 8),
|
|
68
|
+
_type: "object",
|
|
69
|
+
name: s.name,
|
|
70
|
+
hex: s.hex,
|
|
71
|
+
})),
|
|
72
|
+
});
|
|
73
|
+
invalidatePaletteCache();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Section title="Color Palette" description="Build your project's color palette. These colors will appear as presets in every block editor.">
|
|
78
|
+
{swatches.length === 0 && !pickerOpen ? (
|
|
79
|
+
/* ─── Empty state ─── */
|
|
80
|
+
<div className="border-2 border-dashed border-neutral-200 rounded-2xl py-12 px-6 flex flex-col items-center text-center bg-neutral-50/50">
|
|
81
|
+
{/* 4-color icon */}
|
|
82
|
+
<div className="grid grid-cols-2 gap-1 w-12 h-12 mb-4">
|
|
83
|
+
<div className="rounded-tl-lg bg-[var(--admin-accent)] opacity-40" />
|
|
84
|
+
<div className="rounded-tr-lg bg-[var(--admin-error)] opacity-40" />
|
|
85
|
+
<div className="rounded-bl-lg bg-brand-accent opacity-40" />
|
|
86
|
+
<div className="rounded-br-lg bg-brand-accent-2 opacity-40" />
|
|
87
|
+
</div>
|
|
88
|
+
<p className="text-sm font-medium text-neutral-700 mb-1">No colors yet</p>
|
|
89
|
+
<p className="text-xs text-neutral-500 mb-5 max-w-xs leading-relaxed">
|
|
90
|
+
Start building your palette. Colors you add here will be available as quick presets in every block editor — text, backgrounds, borders, and more.
|
|
91
|
+
</p>
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => { setEditingIndex(null); setPickerOpen(true); }}
|
|
94
|
+
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-neutral-900 text-white text-sm font-medium hover:bg-neutral-700 transition-colors"
|
|
95
|
+
>
|
|
96
|
+
<span className="text-lg leading-none">+</span>
|
|
97
|
+
Add your first color
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<>
|
|
102
|
+
{/* ─── Swatches grid ─── */}
|
|
103
|
+
{swatches.length > 0 && (
|
|
104
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3 mb-4">
|
|
105
|
+
{swatches.map((s, i) => (
|
|
106
|
+
<div
|
|
107
|
+
key={s._key || i}
|
|
108
|
+
className="rounded-xl border border-neutral-200 overflow-hidden transition-all hover:shadow-md hover:-translate-y-0.5"
|
|
109
|
+
onMouseEnter={() => setHoveredIndex(i)}
|
|
110
|
+
onMouseLeave={() => setHoveredIndex(null)}
|
|
111
|
+
>
|
|
112
|
+
{/* Color area */}
|
|
113
|
+
<div
|
|
114
|
+
className="h-16 relative cursor-pointer"
|
|
115
|
+
style={{ background: s.hex }}
|
|
116
|
+
onClick={() => { setEditingIndex(i); setPickerOpen(true); }}
|
|
117
|
+
>
|
|
118
|
+
{hoveredIndex === i && (
|
|
119
|
+
<div className="absolute inset-0 bg-black/15 flex items-center justify-center gap-1.5">
|
|
120
|
+
<button
|
|
121
|
+
onClick={(e) => { e.stopPropagation(); setEditingIndex(i); setPickerOpen(true); }}
|
|
122
|
+
className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[11px] cursor-pointer hover:bg-white transition-colors border-none"
|
|
123
|
+
title="Edit color"
|
|
124
|
+
>✎</button>
|
|
125
|
+
<button
|
|
126
|
+
onClick={(e) => { e.stopPropagation(); removeSwatch(i); }}
|
|
127
|
+
className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[13px] text-red-500 cursor-pointer hover:bg-white transition-colors border-none"
|
|
128
|
+
title="Remove"
|
|
129
|
+
>×</button>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
{/* Info */}
|
|
134
|
+
<div className="px-2.5 py-2 bg-white">
|
|
135
|
+
{renamingIndex === i ? (
|
|
136
|
+
<input
|
|
137
|
+
autoFocus
|
|
138
|
+
value={renameValue}
|
|
139
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
140
|
+
onBlur={finishRename}
|
|
141
|
+
onKeyDown={(e) => e.key === "Enter" && finishRename()}
|
|
142
|
+
className="w-full border-none border-b border-neutral-300 text-[11px] font-semibold text-neutral-800 outline-none p-0 bg-transparent"
|
|
143
|
+
/>
|
|
144
|
+
) : (
|
|
145
|
+
<div
|
|
146
|
+
onClick={() => startRename(i)}
|
|
147
|
+
className="text-[11px] font-semibold text-neutral-700 cursor-text truncate"
|
|
148
|
+
>{s.name}</div>
|
|
149
|
+
)}
|
|
150
|
+
<div className="text-[10px] text-neutral-400 mt-0.5 uppercase font-mono">{s.hex}</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
|
|
155
|
+
{/* Add swatch card */}
|
|
156
|
+
{!pickerOpen && (
|
|
157
|
+
<button
|
|
158
|
+
onClick={() => { setEditingIndex(null); setPickerOpen(true); }}
|
|
159
|
+
className="rounded-xl border-2 border-dashed border-neutral-200 h-[100px] flex flex-col items-center justify-center gap-1 cursor-pointer hover:border-[#076bff] hover:text-[#076bff] text-neutral-400 transition-colors bg-transparent"
|
|
160
|
+
>
|
|
161
|
+
<span className="text-xl leading-none">+</span>
|
|
162
|
+
<span className="text-[10px] uppercase tracking-wider">Add</span>
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* Palette strip preview */}
|
|
169
|
+
{swatches.length >= 2 && !pickerOpen && (
|
|
170
|
+
<div className="px-4 py-3 bg-neutral-50 rounded-xl border border-neutral-100 mb-4">
|
|
171
|
+
<div className="text-[10px] text-neutral-400 uppercase tracking-widest mb-2">Palette preview</div>
|
|
172
|
+
<div className="flex rounded-lg overflow-hidden h-8">
|
|
173
|
+
{swatches.map((s, i) => (
|
|
174
|
+
<div
|
|
175
|
+
key={s._key || i}
|
|
176
|
+
className="flex-1 flex items-center justify-center"
|
|
177
|
+
style={{ background: s.hex }}
|
|
178
|
+
>
|
|
179
|
+
<span
|
|
180
|
+
className="text-[7px] uppercase tracking-wider font-mono opacity-70"
|
|
181
|
+
style={{ color: getContrastColor(s.hex) }}
|
|
182
|
+
>{s.name}</span>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Picker */}
|
|
190
|
+
{pickerOpen && (
|
|
191
|
+
<div className="flex justify-center mb-4">
|
|
192
|
+
<ColorPicker
|
|
193
|
+
color={editingIndex !== null ? swatches[editingIndex].hex : "#076bff"}
|
|
194
|
+
onChange={addSwatch}
|
|
195
|
+
onClose={() => { setPickerOpen(false); setEditingIndex(null); }}
|
|
196
|
+
confirmLabel={editingIndex !== null ? "Update color" : "Add to palette"}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Save footer */}
|
|
204
|
+
{(swatches.length > 0 || (colors?.swatches?.length || 0) > 0) && (
|
|
205
|
+
<div className="flex items-center justify-between mt-4 pt-4 border-t border-neutral-100">
|
|
206
|
+
<span className="text-[11px] text-neutral-400">
|
|
207
|
+
{swatches.length} color{swatches.length !== 1 ? "s" : ""} in palette
|
|
208
|
+
</span>
|
|
209
|
+
<SaveButton onClick={handleSave} saving={saving} />
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</Section>
|
|
213
|
+
);
|
|
214
|
+
}
|