@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
package/app/robots.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { getSiteConfig } from "../lib/config";
|
|
3
|
+
|
|
4
|
+
const cfg = getSiteConfig();
|
|
5
|
+
|
|
6
|
+
export default function robots(): MetadataRoute.Robots {
|
|
7
|
+
return {
|
|
8
|
+
rules: [
|
|
9
|
+
{
|
|
10
|
+
userAgent: "*",
|
|
11
|
+
allow: "/",
|
|
12
|
+
disallow: ["/admin/", "/studio/", "/api/admin/"],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
sitemap: `${cfg.domain}/sitemap.xml`,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/app/sitemap.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { client } from "../lib/sanity/client";
|
|
3
|
+
import { allPageSlugsQuery, allProjectSlugsQuery } from "../lib/sanity/queries";
|
|
4
|
+
import { getSiteConfig } from "../lib/config";
|
|
5
|
+
|
|
6
|
+
const cfg = getSiteConfig();
|
|
7
|
+
|
|
8
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
9
|
+
const baseUrl = cfg.domain;
|
|
10
|
+
|
|
11
|
+
// Fetch all published slugs
|
|
12
|
+
const [pageSlugs, projectSlugs] = await Promise.all([
|
|
13
|
+
client.fetch<string[]>(allPageSlugsQuery).catch(() => [] as string[]),
|
|
14
|
+
client.fetch<string[]>(allProjectSlugsQuery).catch(() => [] as string[]),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// Homepage
|
|
18
|
+
const routes: MetadataRoute.Sitemap = [
|
|
19
|
+
{
|
|
20
|
+
url: baseUrl,
|
|
21
|
+
lastModified: new Date(),
|
|
22
|
+
changeFrequency: "weekly",
|
|
23
|
+
priority: 1,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Dynamic pages (About, Contact, Archive, etc.)
|
|
28
|
+
for (const slug of pageSlugs) {
|
|
29
|
+
routes.push({
|
|
30
|
+
url: `${baseUrl}/${slug}`,
|
|
31
|
+
lastModified: new Date(),
|
|
32
|
+
changeFrequency: "monthly",
|
|
33
|
+
priority: 0.8,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Project pages
|
|
38
|
+
for (const slug of projectSlugs) {
|
|
39
|
+
routes.push({
|
|
40
|
+
url: `${baseUrl}/work/${slug}`,
|
|
41
|
+
lastModified: new Date(),
|
|
42
|
+
changeFrequency: "monthly",
|
|
43
|
+
priority: 0.7,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return routes;
|
|
48
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { getSiteConfig } from "../../lib/config";
|
|
5
|
+
|
|
6
|
+
interface MetadataData {
|
|
7
|
+
default_title: string;
|
|
8
|
+
default_description: string;
|
|
9
|
+
default_og_image: string;
|
|
10
|
+
favicon_path: string;
|
|
11
|
+
analytics_id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MetadataEditorProps {
|
|
15
|
+
initialData: MetadataData;
|
|
16
|
+
onSave: (data: MetadataData) => Promise<void>;
|
|
17
|
+
saving: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function MetadataEditor({
|
|
21
|
+
initialData,
|
|
22
|
+
onSave,
|
|
23
|
+
saving,
|
|
24
|
+
}: MetadataEditorProps) {
|
|
25
|
+
const [title, setTitle] = useState(initialData.default_title);
|
|
26
|
+
const [description, setDescription] = useState(
|
|
27
|
+
initialData.default_description
|
|
28
|
+
);
|
|
29
|
+
const [ogImage, setOgImage] = useState(initialData.default_og_image);
|
|
30
|
+
const [favicon, setFavicon] = useState(initialData.favicon_path);
|
|
31
|
+
const [analyticsId, setAnalyticsId] = useState(initialData.analytics_id);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setTitle(initialData.default_title);
|
|
35
|
+
setDescription(initialData.default_description);
|
|
36
|
+
setOgImage(initialData.default_og_image);
|
|
37
|
+
setFavicon(initialData.favicon_path);
|
|
38
|
+
setAnalyticsId(initialData.analytics_id);
|
|
39
|
+
}, [initialData]);
|
|
40
|
+
|
|
41
|
+
const handleSave = () => {
|
|
42
|
+
onSave({
|
|
43
|
+
default_title: title,
|
|
44
|
+
default_description: description,
|
|
45
|
+
default_og_image: ogImage,
|
|
46
|
+
favicon_path: favicon,
|
|
47
|
+
analytics_id: analyticsId,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<section className="space-y-4">
|
|
53
|
+
<div className="flex items-center justify-between border-b border-neutral-200 pb-2">
|
|
54
|
+
<h2 className="text-base font-semibold text-neutral-800">
|
|
55
|
+
Site Metadata
|
|
56
|
+
</h2>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<p className="text-xs text-neutral-400">
|
|
60
|
+
Default SEO metadata used when pages don't specify their own.
|
|
61
|
+
Affects search results and social sharing.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
{/* Title */}
|
|
65
|
+
<div className="space-y-1">
|
|
66
|
+
<label className="text-xs font-medium text-neutral-500">
|
|
67
|
+
Default Page Title
|
|
68
|
+
</label>
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
value={title}
|
|
72
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
73
|
+
placeholder={getSiteConfig().defaults.metaTitle}
|
|
74
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
|
|
75
|
+
/>
|
|
76
|
+
<p className="text-xs text-neutral-400">
|
|
77
|
+
Shown in browser tabs and search results when no page-specific title is
|
|
78
|
+
set.
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Description */}
|
|
83
|
+
<div className="space-y-1">
|
|
84
|
+
<label className="text-xs font-medium text-neutral-500">
|
|
85
|
+
Default Description
|
|
86
|
+
</label>
|
|
87
|
+
<textarea
|
|
88
|
+
value={description}
|
|
89
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
90
|
+
placeholder="Motion graphics studio based in Barcelona..."
|
|
91
|
+
rows={3}
|
|
92
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none resize-none"
|
|
93
|
+
/>
|
|
94
|
+
<div className="flex justify-between">
|
|
95
|
+
<p className="text-xs text-neutral-400">
|
|
96
|
+
Used in search results and social media previews.
|
|
97
|
+
</p>
|
|
98
|
+
<span
|
|
99
|
+
className={`text-xs ${
|
|
100
|
+
description.length > 160 ? "text-red-400" : "text-neutral-400"
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
103
|
+
{description.length}/160
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* OG Image */}
|
|
109
|
+
<div className="space-y-1">
|
|
110
|
+
<label className="text-xs font-medium text-neutral-500">
|
|
111
|
+
Default OG Image Path
|
|
112
|
+
</label>
|
|
113
|
+
<input
|
|
114
|
+
type="text"
|
|
115
|
+
value={ogImage}
|
|
116
|
+
onChange={(e) => setOgImage(e.target.value)}
|
|
117
|
+
placeholder="meta/og-image.jpg"
|
|
118
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
|
|
119
|
+
/>
|
|
120
|
+
<p className="text-xs text-neutral-400">
|
|
121
|
+
Relative path to the default image shown when sharing on social media.
|
|
122
|
+
Resolved via the asset seed URL.
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Favicon */}
|
|
127
|
+
<div className="space-y-1">
|
|
128
|
+
<label className="text-xs font-medium text-neutral-500">
|
|
129
|
+
Favicon Path
|
|
130
|
+
</label>
|
|
131
|
+
<input
|
|
132
|
+
type="text"
|
|
133
|
+
value={favicon}
|
|
134
|
+
onChange={(e) => setFavicon(e.target.value)}
|
|
135
|
+
placeholder="meta/favicon.ico"
|
|
136
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
|
|
137
|
+
/>
|
|
138
|
+
<p className="text-xs text-neutral-400">
|
|
139
|
+
Relative path to the favicon. Resolved via the asset seed URL.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Analytics ID */}
|
|
144
|
+
<div className="space-y-1">
|
|
145
|
+
<label className="text-xs font-medium text-neutral-500">
|
|
146
|
+
Analytics ID (optional)
|
|
147
|
+
</label>
|
|
148
|
+
<input
|
|
149
|
+
type="text"
|
|
150
|
+
value={analyticsId}
|
|
151
|
+
onChange={(e) => setAnalyticsId(e.target.value)}
|
|
152
|
+
placeholder="G-XXXXXXXXXX or plausible domain"
|
|
153
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
|
|
154
|
+
/>
|
|
155
|
+
<p className="text-xs text-neutral-400">
|
|
156
|
+
Google Analytics measurement ID or Plausible domain. Leave empty to
|
|
157
|
+
disable analytics.
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Save */}
|
|
162
|
+
<div className="flex justify-end">
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleSave}
|
|
165
|
+
disabled={saving}
|
|
166
|
+
className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50"
|
|
167
|
+
>
|
|
168
|
+
{saving ? "Saving..." : "Save Metadata"}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* PublishToggle */
|
|
7
|
+
/* Segmented toggle: [ Draft | Published ] */
|
|
8
|
+
/* Active side is highlighted, inactive is subdued. */
|
|
9
|
+
/* Two modes: */
|
|
10
|
+
/* 1. Builder (store) — uses Zustand store actions directly */
|
|
11
|
+
/* 2. List (api) — calls POST to /api/admin/pages/[slug] */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
|
|
14
|
+
interface BuilderModeProps {
|
|
15
|
+
mode: "builder";
|
|
16
|
+
isDraft: boolean;
|
|
17
|
+
onPublish: () => void;
|
|
18
|
+
onUnpublish: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ApiModeProps {
|
|
22
|
+
mode: "api";
|
|
23
|
+
isDraft: boolean;
|
|
24
|
+
slug: string;
|
|
25
|
+
onToggled?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PublishToggleProps = BuilderModeProps | ApiModeProps;
|
|
29
|
+
|
|
30
|
+
export default function PublishToggle(props: PublishToggleProps) {
|
|
31
|
+
const { isDraft } = props;
|
|
32
|
+
const [busy, setBusy] = useState(false);
|
|
33
|
+
|
|
34
|
+
const setDraft = useCallback(async (makeDraft: boolean) => {
|
|
35
|
+
// Already in the desired state
|
|
36
|
+
if (makeDraft === isDraft) return;
|
|
37
|
+
|
|
38
|
+
if (props.mode === "builder") {
|
|
39
|
+
if (makeDraft) props.onUnpublish();
|
|
40
|
+
else props.onPublish();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// API mode
|
|
45
|
+
setBusy(true);
|
|
46
|
+
try {
|
|
47
|
+
const csrfToken =
|
|
48
|
+
document.cookie
|
|
49
|
+
.split("; ")
|
|
50
|
+
.find((c) => c.startsWith("csrf_token="))
|
|
51
|
+
?.split("=")[1] ?? "";
|
|
52
|
+
|
|
53
|
+
const res = await fetch(`/api/admin/pages/${props.slug}`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"X-CSRF-Token": csrfToken,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
draft_mode: makeDraft,
|
|
61
|
+
published_at: makeDraft ? null : new Date().toISOString(),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
props.onToggled?.();
|
|
67
|
+
} else {
|
|
68
|
+
const data = await res.json().catch(() => ({ error: "Failed" }));
|
|
69
|
+
console.error("Toggle publish failed:", data.error || `HTTP ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("Toggle publish failed:", err);
|
|
73
|
+
} finally {
|
|
74
|
+
setBusy(false);
|
|
75
|
+
}
|
|
76
|
+
}, [props, isDraft]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className="inline-flex items-center rounded-full bg-neutral-100 select-none"
|
|
81
|
+
style={{ padding: 2, gap: 0 }}
|
|
82
|
+
onClick={(e) => e.stopPropagation()}
|
|
83
|
+
>
|
|
84
|
+
{/* Draft side */}
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => setDraft(true)}
|
|
88
|
+
disabled={busy}
|
|
89
|
+
style={{
|
|
90
|
+
padding: "2px 10px",
|
|
91
|
+
fontSize: 11,
|
|
92
|
+
fontWeight: 500,
|
|
93
|
+
lineHeight: "20px",
|
|
94
|
+
borderRadius: 9999,
|
|
95
|
+
border: "none",
|
|
96
|
+
cursor: "pointer",
|
|
97
|
+
whiteSpace: "nowrap",
|
|
98
|
+
transition: "background-color 150ms, color 150ms",
|
|
99
|
+
backgroundColor: isDraft ? "#f59e0b" : "transparent",
|
|
100
|
+
color: isDraft ? "#fff" : "#a3a3a3",
|
|
101
|
+
...(busy ? { opacity: 0.5 } : {}),
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
Draft
|
|
105
|
+
</button>
|
|
106
|
+
{/* Published side */}
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setDraft(false)}
|
|
110
|
+
disabled={busy}
|
|
111
|
+
style={{
|
|
112
|
+
padding: "2px 10px",
|
|
113
|
+
fontSize: 11,
|
|
114
|
+
fontWeight: 500,
|
|
115
|
+
lineHeight: "20px",
|
|
116
|
+
borderRadius: 9999,
|
|
117
|
+
border: "none",
|
|
118
|
+
cursor: "pointer",
|
|
119
|
+
whiteSpace: "nowrap",
|
|
120
|
+
transition: "background-color 150ms, color 150ms",
|
|
121
|
+
backgroundColor: !isDraft ? "#10b981" : "transparent",
|
|
122
|
+
color: !isDraft ? "#fff" : "#a3a3a3",
|
|
123
|
+
...(busy ? { opacity: 0.5 } : {}),
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
Published
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// Shared Admin Action Icons
|
|
3
|
+
// ============================================
|
|
4
|
+
// Extracted from pages/page.tsx and projects/page.tsx (Session 131)
|
|
5
|
+
|
|
6
|
+
export function EditIcon() {
|
|
7
|
+
return (
|
|
8
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
9
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
10
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DuplicateIcon() {
|
|
16
|
+
return (
|
|
17
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
18
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
19
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function DeleteIcon() {
|
|
25
|
+
return (
|
|
26
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
27
|
+
<polyline points="3 6 5 6 21 6" />
|
|
28
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PreviewIcon() {
|
|
34
|
+
return (
|
|
35
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
36
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
37
|
+
<circle cx="12" cy="12" r="3" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from "react";
|
|
4
|
+
import type { NavItem, NavDesign, PageListItem } from "../../../lib/sanity/types";
|
|
5
|
+
import { migrateNavItems, validateLayout } from "./nav-builder-utils";
|
|
6
|
+
import NavBuilderGrid from "./NavBuilderGrid";
|
|
7
|
+
import NavLivePreview from "./NavLivePreview";
|
|
8
|
+
import NavSettingsPanel from "./NavSettingsPanel";
|
|
9
|
+
|
|
10
|
+
interface NavBuilderProps {
|
|
11
|
+
initialItems: NavItem[];
|
|
12
|
+
initialDesign: NavDesign;
|
|
13
|
+
onSave: (items: NavItem[], design: NavDesign) => Promise<void>;
|
|
14
|
+
saving: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function NavBuilder({
|
|
18
|
+
initialItems,
|
|
19
|
+
initialDesign,
|
|
20
|
+
onSave,
|
|
21
|
+
saving,
|
|
22
|
+
}: NavBuilderProps) {
|
|
23
|
+
// ── Migrate items from old format if needed ──
|
|
24
|
+
const [items, setItems] = useState<NavItem[]>(() =>
|
|
25
|
+
migrateNavItems(initialItems, initialDesign)
|
|
26
|
+
);
|
|
27
|
+
const [design, setDesign] = useState<NavDesign>(initialDesign);
|
|
28
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
29
|
+
const [pages, setPages] = useState<PageListItem[]>([]);
|
|
30
|
+
const [fonts, setFonts] = useState<string[]>([]);
|
|
31
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
32
|
+
|
|
33
|
+
// Sync from parent on external refresh
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setItems(migrateNavItems(initialItems, initialDesign));
|
|
36
|
+
}, [initialItems, initialDesign]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setDesign(initialDesign);
|
|
40
|
+
}, [initialDesign]);
|
|
41
|
+
|
|
42
|
+
// Load pages and fonts
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch("/api/admin/pages");
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
setPages(data.pages || []);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
/* silent */
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch("/api/admin/styles", { credentials: "include" });
|
|
58
|
+
if (res.ok) {
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
const families = (data.styles?.fonts || []).map(
|
|
61
|
+
(f: { family: string }) => f.family
|
|
62
|
+
);
|
|
63
|
+
if (families.length > 0) setFonts(families);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* silent */
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Mark changes
|
|
72
|
+
const markChanged = useCallback(() => {
|
|
73
|
+
setHasChanges(true);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// ── Item CRUD ──
|
|
77
|
+
const handleAddItem = useCallback(
|
|
78
|
+
(newItem: NavItem) => {
|
|
79
|
+
setItems((prev) => [...prev, newItem]);
|
|
80
|
+
markChanged();
|
|
81
|
+
},
|
|
82
|
+
[markChanged]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleDeleteItem = useCallback(
|
|
86
|
+
(key: string) => {
|
|
87
|
+
setItems((prev) => prev.filter((i) => i._key !== key));
|
|
88
|
+
if (selectedKey === key) setSelectedKey(null);
|
|
89
|
+
markChanged();
|
|
90
|
+
},
|
|
91
|
+
[selectedKey, markChanged]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const handleUpdateItem = useCallback(
|
|
95
|
+
(updated: NavItem) => {
|
|
96
|
+
setItems((prev) =>
|
|
97
|
+
prev.map((i) => (i._key === updated._key ? updated : i))
|
|
98
|
+
);
|
|
99
|
+
markChanged();
|
|
100
|
+
},
|
|
101
|
+
[markChanged]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const handleDesignChange = useCallback(
|
|
105
|
+
(newDesign: NavDesign) => {
|
|
106
|
+
setDesign(newDesign);
|
|
107
|
+
markChanged();
|
|
108
|
+
},
|
|
109
|
+
[markChanged]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── Save ──
|
|
113
|
+
const handleSave = useCallback(async () => {
|
|
114
|
+
const validation = validateLayout(items);
|
|
115
|
+
if (!validation.valid) {
|
|
116
|
+
alert(`Layout conflicts:\n${validation.conflicts.join("\n")}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await onSave(items, design);
|
|
120
|
+
setHasChanges(false);
|
|
121
|
+
}, [items, design, onSave]);
|
|
122
|
+
|
|
123
|
+
const selectedItem = items.find((i) => i._key === selectedKey) || null;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="bg-white rounded-2xl overflow-hidden border border-neutral-200">
|
|
127
|
+
{/* Top bar */}
|
|
128
|
+
<div className="px-5 py-3 border-b border-neutral-200 flex items-center justify-between">
|
|
129
|
+
<div>
|
|
130
|
+
<div className="text-sm font-semibold text-neutral-900">Grid Layout · 12 Columns</div>
|
|
131
|
+
<div className="text-[11px] text-neutral-400 mt-0.5">
|
|
132
|
+
{items.length} items · Click empty columns to add · Drag to reposition
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex items-center gap-3">
|
|
136
|
+
{hasChanges && (
|
|
137
|
+
<span className="text-[11px] text-amber-500 font-medium">
|
|
138
|
+
Unsaved changes
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
<button
|
|
142
|
+
onClick={handleSave}
|
|
143
|
+
disabled={saving || !hasChanges}
|
|
144
|
+
className={`px-5 py-1.5 text-sm font-medium rounded-lg transition-all ${
|
|
145
|
+
saving || !hasChanges
|
|
146
|
+
? "bg-neutral-100 text-neutral-400 cursor-not-allowed"
|
|
147
|
+
: "bg-[#076bff] text-white hover:bg-[#0559d4]"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
{saving ? "Saving..." : "Save"}
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Live preview */}
|
|
156
|
+
<NavLivePreview items={items} design={design} />
|
|
157
|
+
|
|
158
|
+
{/* Grid editor */}
|
|
159
|
+
<NavBuilderGrid
|
|
160
|
+
items={items}
|
|
161
|
+
selectedKey={selectedKey}
|
|
162
|
+
onSelect={setSelectedKey}
|
|
163
|
+
onAddItem={handleAddItem}
|
|
164
|
+
onDeleteItem={handleDeleteItem}
|
|
165
|
+
onUpdateItem={handleUpdateItem}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
{/* Settings panel — below the grid */}
|
|
169
|
+
<div className="border-t border-neutral-200">
|
|
170
|
+
<NavSettingsPanel
|
|
171
|
+
selectedItem={selectedItem}
|
|
172
|
+
items={items}
|
|
173
|
+
design={design}
|
|
174
|
+
onDesignChange={handleDesignChange}
|
|
175
|
+
onUpdateItem={handleUpdateItem}
|
|
176
|
+
pages={pages}
|
|
177
|
+
fonts={fonts}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|