@morphika/andami 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +212 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { csrfHeaders } from "../../../lib/csrf-client";
5
+ import type { FontFamily, FontVariant } from "../../../lib/sanity/types";
6
+ import { Section, SaveButton } from "./shared";
7
+
8
+ export function FontsEditor({ fonts, onSave, saving }: { fonts: FontFamily[]; onSave: (fonts: FontFamily[]) => void; saving: boolean }) {
9
+ const [localFonts, setLocalFonts] = useState<FontFamily[]>(fonts);
10
+ const [uploading, setUploading] = useState(false);
11
+ const fileInputRef = useRef<HTMLInputElement>(null);
12
+ const [uploadTarget, setUploadTarget] = useState<string | null>(null);
13
+ const [expandedFont, setExpandedFont] = useState<string | null>(null);
14
+
15
+ useEffect(() => { setLocalFonts(fonts); }, [fonts]);
16
+
17
+ const addFont = () => {
18
+ const key = crypto.randomUUID().slice(0, 8);
19
+ setLocalFonts([
20
+ ...localFonts,
21
+ {
22
+ _key: key,
23
+ family: "",
24
+ is_builtin: false,
25
+ variants: [],
26
+ },
27
+ ]);
28
+ setExpandedFont(key);
29
+ };
30
+
31
+ const updateFont = (key: string, updates: Partial<FontFamily>) => {
32
+ setLocalFonts(localFonts.map((f) => (f._key === key ? { ...f, ...updates } : f)));
33
+ };
34
+
35
+ const removeFont = (key: string) => {
36
+ setLocalFonts(localFonts.filter((f) => f._key !== key));
37
+ if (expandedFont === key) setExpandedFont(null);
38
+ };
39
+
40
+ const handleFileUpload = async (fontKey: string, file: File) => {
41
+ setUploading(true);
42
+ try {
43
+ const formData = new FormData();
44
+ formData.append("font", file);
45
+
46
+ const res = await fetch("/api/admin/styles/fonts", {
47
+ method: "POST",
48
+ headers: csrfHeaders(),
49
+ body: formData,
50
+ });
51
+
52
+ if (!res.ok) {
53
+ const errData = await res.json();
54
+ throw new Error(errData.error || "Upload failed");
55
+ }
56
+
57
+ const result = await res.json();
58
+ const newVariant: FontVariant = {
59
+ _key: crypto.randomUUID().slice(0, 8),
60
+ weight: "400",
61
+ style: "normal",
62
+ file_url: result.file_url,
63
+ file_id: result.file_id,
64
+ original_filename: result.original_filename,
65
+ };
66
+
67
+ setLocalFonts(
68
+ localFonts.map((f) =>
69
+ f._key === fontKey
70
+ ? { ...f, variants: [...f.variants, newVariant] }
71
+ : f
72
+ )
73
+ );
74
+ } catch (err) {
75
+ alert(err instanceof Error ? err.message : "Upload failed");
76
+ } finally {
77
+ setUploading(false);
78
+ }
79
+ };
80
+
81
+ const removeVariant = (fontKey: string, variantKey: string) => {
82
+ setLocalFonts(
83
+ localFonts.map((f) =>
84
+ f._key === fontKey
85
+ ? { ...f, variants: f.variants.filter((v) => v._key !== variantKey) }
86
+ : f
87
+ )
88
+ );
89
+ };
90
+
91
+ const updateVariant = (fontKey: string, variantKey: string, updates: Partial<FontVariant>) => {
92
+ setLocalFonts(
93
+ localFonts.map((f) =>
94
+ f._key === fontKey
95
+ ? {
96
+ ...f,
97
+ variants: f.variants.map((v) =>
98
+ v._key === variantKey ? { ...v, ...updates } : v
99
+ ),
100
+ }
101
+ : f
102
+ )
103
+ );
104
+ };
105
+
106
+ return (
107
+ <Section title="Fonts" description="Upload custom fonts (.woff2 recommended) or manage built-in fonts. These will be available in the typography settings below.">
108
+ {/* ─── Font cards grid ─── */}
109
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3 mb-4">
110
+ {localFonts.map((font) => {
111
+ const isExpanded = expandedFont === font._key;
112
+ const weights = font.variants.map((v) => v.weight).sort();
113
+ return (
114
+ <div
115
+ key={font._key}
116
+ className={`rounded-xl border overflow-hidden transition-all cursor-pointer ${
117
+ isExpanded
118
+ ? "border-[#076bff] shadow-sm ring-1 ring-[#076bff]/20"
119
+ : "border-neutral-200 hover:shadow-md hover:-translate-y-0.5"
120
+ }`}
121
+ onClick={() => setExpandedFont(isExpanded ? null : font._key)}
122
+ >
123
+ {/* Preview area */}
124
+ <div className="px-5 pt-5 pb-3 bg-white">
125
+ <div className="flex items-start justify-between mb-2">
126
+ <span className="text-4xl font-medium text-neutral-800 leading-none select-none">Aa</span>
127
+ {font.is_builtin && (
128
+ <span className="text-[9px] bg-neutral-100 text-neutral-500 px-1.5 py-0.5 rounded-full uppercase tracking-wider mt-1">
129
+ Built-in
130
+ </span>
131
+ )}
132
+ </div>
133
+ <div className="text-[10px] text-neutral-400 uppercase tracking-widest mt-1">Typeface</div>
134
+ <div className="text-xs font-semibold text-neutral-700 truncate">
135
+ {font.family || "Untitled Font"}
136
+ </div>
137
+ {weights.length > 0 && (
138
+ <div className="text-[10px] text-neutral-400 mt-0.5">
139
+ Weights: {weights.join(" \u00B7 ")}
140
+ </div>
141
+ )}
142
+ </div>
143
+ </div>
144
+ );
145
+ })}
146
+
147
+ {/* Add font card */}
148
+ <button
149
+ onClick={addFont}
150
+ className="rounded-xl border-2 border-dashed border-neutral-200 min-h-[120px] flex flex-col items-center justify-center gap-1.5 cursor-pointer hover:border-[#076bff] hover:text-[#076bff] text-neutral-400 transition-colors bg-transparent"
151
+ >
152
+ <span className="text-xl leading-none">+</span>
153
+ <span className="text-[10px] uppercase tracking-wider">Add Font Family</span>
154
+ </button>
155
+ </div>
156
+
157
+ {/* ─── Expanded font editor ─── */}
158
+ {expandedFont && (() => {
159
+ const font = localFonts.find((f) => f._key === expandedFont);
160
+ if (!font) return null;
161
+ return (
162
+ <div className="border border-neutral-200 rounded-xl p-5 mb-4 bg-neutral-50/50">
163
+ <div className="flex items-center gap-3 mb-4">
164
+ <input
165
+ type="text"
166
+ value={font.family}
167
+ onChange={(e) => updateFont(font._key, { family: e.target.value })}
168
+ placeholder="Font Family Name (e.g. Inter)"
169
+ className="flex-1 rounded-lg border border-neutral-200 bg-white px-3 py-1.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
170
+ disabled={font.is_builtin}
171
+ onClick={(e) => e.stopPropagation()}
172
+ />
173
+ {!font.is_builtin && (
174
+ <button
175
+ onClick={() => removeFont(font._key)}
176
+ className="text-xs text-red-500 hover:text-red-700 transition-colors"
177
+ >
178
+ Remove Font
179
+ </button>
180
+ )}
181
+ </div>
182
+
183
+ {/* Variants */}
184
+ <div className="space-y-2">
185
+ {font.variants.length > 0 && (
186
+ <div className="text-[10px] text-neutral-400 uppercase tracking-widest mb-1">Variants</div>
187
+ )}
188
+ {font.variants.map((v) => (
189
+ <div key={v._key} className="flex items-center gap-2 text-xs bg-white rounded-lg px-3 py-2 border border-neutral-100">
190
+ <select
191
+ value={v.weight}
192
+ onChange={(e) => updateVariant(font._key, v._key, { weight: e.target.value })}
193
+ className="rounded border border-neutral-200 bg-white px-2 py-1 text-xs focus:border-[#076bff] focus:outline-none"
194
+ >
195
+ {["100", "300", "400", "500", "600", "700", "800", "900"].map((w) => (
196
+ <option key={w} value={w}>{w}</option>
197
+ ))}
198
+ </select>
199
+ <select
200
+ value={v.style}
201
+ onChange={(e) => updateVariant(font._key, v._key, { style: e.target.value as "normal" | "italic" })}
202
+ className="rounded border border-neutral-200 bg-white px-2 py-1 text-xs focus:border-[#076bff] focus:outline-none"
203
+ >
204
+ <option value="normal">Normal</option>
205
+ <option value="italic">Italic</option>
206
+ </select>
207
+ <span className="text-neutral-400 truncate flex-1" title={v.original_filename}>
208
+ {v.original_filename}
209
+ </span>
210
+ {!font.is_builtin && (
211
+ <button
212
+ onClick={() => removeVariant(font._key, v._key)}
213
+ className="text-red-400 hover:text-red-600 text-sm"
214
+ >
215
+ ×
216
+ </button>
217
+ )}
218
+ </div>
219
+ ))}
220
+
221
+ {!font.is_builtin && (
222
+ <button
223
+ onClick={() => {
224
+ setUploadTarget(font._key);
225
+ fileInputRef.current?.click();
226
+ }}
227
+ disabled={uploading}
228
+ className="text-xs text-[#076bff] hover:text-[#0559d4] transition-colors mt-1"
229
+ >
230
+ {uploading && uploadTarget === font._key ? "Uploading..." : "+ Add variant file"}
231
+ </button>
232
+ )}
233
+ </div>
234
+ </div>
235
+ );
236
+ })()}
237
+
238
+ <input
239
+ ref={fileInputRef}
240
+ type="file"
241
+ accept=".woff2,.woff,.ttf,.otf"
242
+ className="hidden"
243
+ onChange={(e) => {
244
+ const file = e.target.files?.[0];
245
+ if (file && uploadTarget) {
246
+ handleFileUpload(uploadTarget, file);
247
+ }
248
+ e.target.value = "";
249
+ }}
250
+ />
251
+
252
+ {/* Save footer */}
253
+ <div className="flex items-center justify-end pt-3 border-t border-neutral-100">
254
+ <SaveButton onClick={() => onSave(localFonts)} saving={saving} />
255
+ </div>
256
+ </Section>
257
+ );
258
+ }
@@ -0,0 +1,292 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import type { SiteStyles } from "../../../lib/sanity/types";
5
+ import { DEFAULT_GRID_WIDTH, BREAKPOINTS } from "../../../lib/builder/constants";
6
+ import { Section, SaveButton } from "./shared";
7
+
8
+ const DEFAULT_GRID = {
9
+ width: DEFAULT_GRID_WIDTH,
10
+ outer_padding: "30",
11
+ gutter_desktop: "30",
12
+ gutter_responsive: "30",
13
+ gutter_phone: "16",
14
+ };
15
+
16
+ /** SVG illustration for the Column Gutter card */
17
+ function GutterIcon() {
18
+ return (
19
+ <svg viewBox="0 0 120 72" fill="none" className="w-full h-full" style={{ color: "#b0b5bd" }}>
20
+ {/* 5 columns */}
21
+ <rect x="6" y="8" width="16" height="56" rx="2" fill="currentColor" opacity="0.22" />
22
+ <rect x="28" y="8" width="16" height="56" rx="2" fill="currentColor" opacity="0.22" />
23
+ <rect x="50" y="8" width="16" height="56" rx="2" fill="currentColor" opacity="0.22" />
24
+ <rect x="72" y="8" width="16" height="56" rx="2" fill="currentColor" opacity="0.22" />
25
+ <rect x="94" y="8" width="16" height="56" rx="2" fill="currentColor" opacity="0.22" />
26
+ {/* Gap arrows between columns */}
27
+ <line x1="22" y1="36" x2="28" y2="36" stroke="currentColor" strokeWidth="1" opacity="0.55" />
28
+ <path d="M26 34l2 2-2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
29
+ <path d="M24 34l-2 2 2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
30
+ <line x1="44" y1="36" x2="50" y2="36" stroke="currentColor" strokeWidth="1" opacity="0.55" />
31
+ <path d="M48 34l2 2-2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
32
+ <path d="M46 34l-2 2 2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
33
+ <line x1="66" y1="36" x2="72" y2="36" stroke="currentColor" strokeWidth="1" opacity="0.55" />
34
+ <path d="M70 34l2 2-2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
35
+ <path d="M68 34l-2 2 2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
36
+ <line x1="88" y1="36" x2="94" y2="36" stroke="currentColor" strokeWidth="1" opacity="0.55" />
37
+ <path d="M92 34l2 2-2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
38
+ <path d="M90 34l-2 2 2 2" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.55" />
39
+ </svg>
40
+ );
41
+ }
42
+
43
+ /** SVG illustration for the Max Page Width card */
44
+ function MaxWidthIcon() {
45
+ return (
46
+ <svg viewBox="0 0 120 72" fill="none" className="w-full h-full" style={{ color: "#b0b5bd" }}>
47
+ {/* Browser frame */}
48
+ <rect x="10" y="10" width="100" height="52" rx="4" stroke="currentColor" strokeWidth="1.5" opacity="0.3" />
49
+ {/* Title bar dots */}
50
+ <circle cx="18" cy="17" r="1.5" fill="currentColor" opacity="0.25" />
51
+ <circle cx="23" cy="17" r="1.5" fill="currentColor" opacity="0.25" />
52
+ <circle cx="28" cy="17" r="1.5" fill="currentColor" opacity="0.25" />
53
+ {/* Divider */}
54
+ <line x1="10" y1="22" x2="110" y2="22" stroke="currentColor" strokeWidth="1" opacity="0.15" />
55
+ {/* Content area */}
56
+ <rect x="28" y="28" width="64" height="28" rx="2" fill="currentColor" opacity="0.1" />
57
+ {/* Left arrow */}
58
+ <line x1="15" y1="42" x2="28" y2="42" stroke="currentColor" strokeWidth="1.2" opacity="0.5" />
59
+ <path d="M26 40l2 2-2 2" stroke="currentColor" strokeWidth="1.2" fill="none" opacity="0.5" />
60
+ {/* Right arrow */}
61
+ <line x1="92" y1="42" x2="105" y2="42" stroke="currentColor" strokeWidth="1.2" opacity="0.5" />
62
+ <path d="M94 40l-2 2 2 2" stroke="currentColor" strokeWidth="1.2" fill="none" opacity="0.5" />
63
+ {/* Dashed boundary lines */}
64
+ <line x1="28" y1="26" x2="28" y2="58" stroke="currentColor" strokeWidth="1" strokeDasharray="2 2" opacity="0.3" />
65
+ <line x1="92" y1="26" x2="92" y2="58" stroke="currentColor" strokeWidth="1" strokeDasharray="2 2" opacity="0.3" />
66
+ </svg>
67
+ );
68
+ }
69
+
70
+ /** SVG illustration for the Scroll Animations card */
71
+ function ScrollAnimIcon() {
72
+ return (
73
+ <svg viewBox="0 0 120 72" fill="none" className="w-full h-full" style={{ color: "#b0b5bd" }}>
74
+ {/* 3 stacked layers with increasing opacity (fade-in effect) */}
75
+ <rect x="30" y="6" width="60" height="14" rx="3" fill="currentColor" opacity="0.06" stroke="currentColor" strokeWidth="0.8" />
76
+ <rect x="30" y="24" width="60" height="14" rx="3" fill="currentColor" opacity="0.14" stroke="currentColor" strokeWidth="0.8" />
77
+ <rect x="30" y="42" width="60" height="14" rx="3" fill="currentColor" opacity="0.28" stroke="currentColor" strokeWidth="0.8" />
78
+ {/* Scroll-down arrow */}
79
+ <line x1="60" y1="60" x2="60" y2="70" stroke="currentColor" strokeWidth="1.5" opacity="0.5" />
80
+ <path d="M56 67l4 4 4-4" stroke="currentColor" strokeWidth="1.5" fill="none" opacity="0.5" />
81
+ {/* Motion lines */}
82
+ <line x1="24" y1="46" x2="28" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
83
+ <line x1="24" y1="52" x2="28" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
84
+ <line x1="92" y1="46" x2="96" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
85
+ <line x1="92" y1="52" x2="96" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
86
+ </svg>
87
+ );
88
+ }
89
+
90
+ /** Device icon (desktop/tablet/phone) for responsive gutter labels */
91
+ function DeviceIcon({ device }: { device: "desktop" | "tablet" | "phone" }) {
92
+ const base = { width: 14, height: 14, viewBox: "0 0 16 16", fill: "none" as const, stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
93
+ switch (device) {
94
+ case "desktop":
95
+ return (
96
+ <svg {...base}>
97
+ <rect x="1" y="2" width="14" height="10" rx="1.5" />
98
+ <line x1="5" y1="14" x2="11" y2="14" />
99
+ <line x1="8" y1="12" x2="8" y2="14" />
100
+ </svg>
101
+ );
102
+ case "tablet":
103
+ return (
104
+ <svg {...base}>
105
+ <rect x="3" y="1" width="10" height="14" rx="1.5" />
106
+ <line x1="6" y1="13" x2="10" y2="13" />
107
+ </svg>
108
+ );
109
+ case "phone":
110
+ return (
111
+ <svg {...base}>
112
+ <rect x="4" y="1" width="8" height="14" rx="2" />
113
+ <line x1="6.5" y1="13" x2="9.5" y2="13" />
114
+ </svg>
115
+ );
116
+ }
117
+ }
118
+
119
+ export function GridLayoutEditor({
120
+ grid,
121
+ disableMobile,
122
+ onSaveGrid,
123
+ onSaveAnimations,
124
+ savingGrid,
125
+ savingAnimations,
126
+ }: {
127
+ grid?: SiteStyles["grid"];
128
+ disableMobile: boolean;
129
+ onSaveGrid: (data: Record<string, unknown>) => void;
130
+ onSaveAnimations: (data: Record<string, unknown>) => void;
131
+ savingGrid: boolean;
132
+ savingAnimations: boolean;
133
+ }) {
134
+ const [local, setLocal] = useState({
135
+ grid_width: grid?.width || DEFAULT_GRID.width,
136
+ grid_outer_padding: grid?.outer_padding || DEFAULT_GRID.outer_padding,
137
+ grid_gutter_desktop: grid?.gutter_desktop || DEFAULT_GRID.gutter_desktop,
138
+ grid_gutter_responsive: grid?.gutter_responsive || DEFAULT_GRID.gutter_responsive,
139
+ grid_gutter_phone: grid?.gutter_phone || DEFAULT_GRID.gutter_phone,
140
+ });
141
+ const [animLocal, setAnimLocal] = useState(disableMobile);
142
+
143
+ useEffect(() => {
144
+ setLocal({
145
+ grid_width: grid?.width || DEFAULT_GRID.width,
146
+ grid_outer_padding: grid?.outer_padding || DEFAULT_GRID.outer_padding,
147
+ grid_gutter_desktop: grid?.gutter_desktop || DEFAULT_GRID.gutter_desktop,
148
+ grid_gutter_responsive: grid?.gutter_responsive || DEFAULT_GRID.gutter_responsive,
149
+ grid_gutter_phone: grid?.gutter_phone || DEFAULT_GRID.gutter_phone,
150
+ });
151
+ }, [grid]);
152
+
153
+ useEffect(() => { setAnimLocal(disableMobile); }, [disableMobile]);
154
+
155
+ const saving = savingGrid || savingAnimations;
156
+
157
+ return (
158
+ <Section title="Grid & Layout" description="Define the global grid, maximum page width, and animation behavior.">
159
+ {/* ── Top row: 3 icon cards ── */}
160
+ <div className="grid grid-cols-3 gap-5 mb-8" style={{ maxWidth: "680px" }}>
161
+ {/* Column Gutter card */}
162
+ <div className="flex flex-col">
163
+ <div className="w-full aspect-[16/10] bg-neutral-50 border border-neutral-100 rounded-xl flex items-center justify-center p-4 mb-3 hover:border-neutral-200 transition-colors">
164
+ <GutterIcon />
165
+ </div>
166
+ <label className="text-[11px] font-medium text-neutral-500 mb-2">Column gutter</label>
167
+ <div className="relative w-[120px]">
168
+ <input
169
+ type="text"
170
+ value={local.grid_gutter_desktop}
171
+ onChange={(e) => setLocal({ ...local, grid_gutter_desktop: e.target.value })}
172
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-1.5 pr-8 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
173
+ placeholder="30"
174
+ />
175
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-neutral-400 pointer-events-none">px</span>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Max Width card */}
180
+ <div className="flex flex-col">
181
+ <div className="w-full aspect-[16/10] bg-neutral-50 border border-neutral-100 rounded-xl flex items-center justify-center p-4 mb-3 hover:border-neutral-200 transition-colors">
182
+ <MaxWidthIcon />
183
+ </div>
184
+ <label className="text-[11px] font-medium text-neutral-500 mb-2">Max page width</label>
185
+ <div className="relative w-[120px]">
186
+ <input
187
+ type="text"
188
+ value={local.grid_width}
189
+ onChange={(e) => setLocal({ ...local, grid_width: e.target.value })}
190
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-1.5 pr-8 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
191
+ placeholder={DEFAULT_GRID_WIDTH}
192
+ />
193
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-neutral-400 pointer-events-none">px</span>
194
+ </div>
195
+ </div>
196
+
197
+ {/* Scroll Animations card */}
198
+ <div className="flex flex-col">
199
+ <div className="w-full aspect-[16/10] bg-neutral-50 border border-neutral-100 rounded-xl flex items-center justify-center p-4 mb-3 hover:border-neutral-200 transition-colors">
200
+ <ScrollAnimIcon />
201
+ </div>
202
+ <label className="text-[11px] font-medium text-neutral-500 mb-2">Scroll animations</label>
203
+ <div className="flex items-center gap-2">
204
+ <span className="text-[11px] text-neutral-400">Disable on mobile</span>
205
+ <button
206
+ type="button"
207
+ onClick={() => setAnimLocal(!animLocal)}
208
+ className={`relative w-9 h-5 rounded-full transition-colors shrink-0 ${
209
+ animLocal ? "bg-[#076bff]" : "bg-neutral-300"
210
+ }`}
211
+ >
212
+ <span
213
+ className={`absolute top-[2px] w-4 h-4 rounded-full bg-white shadow-sm transition-all ${
214
+ animLocal ? "left-[18px]" : "left-[2px]"
215
+ }`}
216
+ />
217
+ </button>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ {/* ── Responsive gutter overrides ── */}
223
+ <div className="border-t border-neutral-100 pt-6">
224
+ <h3 className="text-[10px] font-semibold text-neutral-400 uppercase tracking-wider mb-4">Responsive gutter overrides</h3>
225
+ <div className="grid grid-cols-3 gap-5" style={{ maxWidth: "480px" }}>
226
+ {/* Desktop */}
227
+ <div>
228
+ <label className="flex items-center gap-1.5 text-[11px] font-medium text-neutral-500 mb-1.5">
229
+ <DeviceIcon device="desktop" />
230
+ Desktop
231
+ </label>
232
+ <div className="relative">
233
+ <input
234
+ type="text"
235
+ value={local.grid_gutter_desktop}
236
+ onChange={(e) => setLocal({ ...local, grid_gutter_desktop: e.target.value })}
237
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-1.5 pr-8 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
238
+ placeholder="30"
239
+ />
240
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-neutral-400 pointer-events-none">px</span>
241
+ </div>
242
+ <p className="text-[10px] text-neutral-400 mt-1">{`>${BREAKPOINTS.tablet}px`}</p>
243
+ </div>
244
+
245
+ {/* Tablet */}
246
+ <div>
247
+ <label className="flex items-center gap-1.5 text-[11px] font-medium text-neutral-500 mb-1.5">
248
+ <DeviceIcon device="tablet" />
249
+ Tablet
250
+ </label>
251
+ <div className="relative">
252
+ <input
253
+ type="text"
254
+ value={local.grid_gutter_responsive}
255
+ onChange={(e) => setLocal({ ...local, grid_gutter_responsive: e.target.value })}
256
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-1.5 pr-8 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
257
+ placeholder="30"
258
+ />
259
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-neutral-400 pointer-events-none">px</span>
260
+ </div>
261
+ <p className="text-[10px] text-neutral-400 mt-1">{`≤${BREAKPOINTS.tablet}px`}</p>
262
+ </div>
263
+
264
+ {/* Phone */}
265
+ <div>
266
+ <label className="flex items-center gap-1.5 text-[11px] font-medium text-neutral-500 mb-1.5">
267
+ <DeviceIcon device="phone" />
268
+ Phone
269
+ </label>
270
+ <div className="relative">
271
+ <input
272
+ type="text"
273
+ value={local.grid_gutter_phone || "16"}
274
+ onChange={(e) => setLocal({ ...local, grid_gutter_phone: e.target.value })}
275
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-1.5 pr-8 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
276
+ placeholder="16"
277
+ />
278
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-neutral-400 pointer-events-none">px</span>
279
+ </div>
280
+ <p className="text-[10px] text-neutral-400 mt-1">{`≤${BREAKPOINTS.phone}px`}</p>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ {/* ── Save buttons ── */}
286
+ <div className="flex justify-end gap-3 pt-6">
287
+ <SaveButton onClick={() => onSaveAnimations({ disable_scroll_animations_mobile: animLocal })} saving={savingAnimations} label="Save Animations" />
288
+ <SaveButton onClick={() => onSaveGrid(local)} saving={savingGrid} label="Save Grid" />
289
+ </div>
290
+ </Section>
291
+ );
292
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import type { SiteStyles } from "../../../lib/sanity/types";
5
+ import { Section, SaveButton, ColorField, FieldInput } from "./shared";
6
+
7
+ export function LinksButtonsEditor({
8
+ linkStyle,
9
+ buttonStyle,
10
+ onSave,
11
+ saving,
12
+ }: {
13
+ linkStyle?: SiteStyles["link_style"];
14
+ buttonStyle?: SiteStyles["button_style"];
15
+ onSave: (data: Record<string, unknown>) => void;
16
+ saving: boolean;
17
+ }) {
18
+ const [local, setLocal] = useState({
19
+ link_color: linkStyle?.color || "#076bff",
20
+ link_hover_color: linkStyle?.hover_color || "#0559d4",
21
+ link_underline: linkStyle?.underline ?? true,
22
+ button_primary_bg: buttonStyle?.primary_bg || "#ffffff",
23
+ button_primary_text: buttonStyle?.primary_text || "#000000",
24
+ button_secondary_bg: buttonStyle?.secondary_bg || "#1a1a1a",
25
+ button_secondary_text: buttonStyle?.secondary_text || "#ffffff",
26
+ button_border_radius: buttonStyle?.border_radius || "8px",
27
+ });
28
+
29
+ useEffect(() => {
30
+ setLocal({
31
+ link_color: linkStyle?.color || "#076bff",
32
+ link_hover_color: linkStyle?.hover_color || "#0559d4",
33
+ link_underline: linkStyle?.underline ?? true,
34
+ button_primary_bg: buttonStyle?.primary_bg || "#ffffff",
35
+ button_primary_text: buttonStyle?.primary_text || "#000000",
36
+ button_secondary_bg: buttonStyle?.secondary_bg || "#1a1a1a",
37
+ button_secondary_text: buttonStyle?.secondary_text || "#ffffff",
38
+ button_border_radius: buttonStyle?.border_radius || "8px",
39
+ });
40
+ }, [linkStyle, buttonStyle]);
41
+
42
+ return (
43
+ <Section title="Links & Buttons" description="Default styles for links and buttons across the site.">
44
+ <h3 className="text-sm font-medium text-neutral-700 mb-3">Links</h3>
45
+ <div className="grid grid-cols-3 gap-4 mb-4">
46
+ <ColorField label="Link Color" value={local.link_color} onChange={(v) => setLocal({ ...local, link_color: v })} />
47
+ <ColorField label="Hover Color" value={local.link_hover_color} onChange={(v) => setLocal({ ...local, link_hover_color: v })} />
48
+ <div className="flex items-end">
49
+ <label className="flex items-center gap-2 cursor-pointer">
50
+ <input
51
+ type="checkbox"
52
+ checked={local.link_underline}
53
+ onChange={(e) => setLocal({ ...local, link_underline: e.target.checked })}
54
+ className="accent-[#076bff]"
55
+ />
56
+ <span className="text-xs text-neutral-700">Underline links</span>
57
+ </label>
58
+ </div>
59
+ </div>
60
+
61
+ <div className="mb-6 p-3 bg-neutral-50 rounded-lg">
62
+ <span
63
+ className="text-sm cursor-pointer"
64
+ style={{
65
+ color: local.link_color,
66
+ textDecoration: local.link_underline ? "underline" : "none",
67
+ textDecorationColor: local.link_color,
68
+ textUnderlineOffset: "3px",
69
+ }}
70
+ >
71
+ Sample link text
72
+ </span>
73
+ </div>
74
+
75
+ <h3 className="text-sm font-medium text-neutral-700 mb-3">Buttons</h3>
76
+ <div className="grid grid-cols-3 gap-4 mb-4">
77
+ <ColorField label="Primary BG" value={local.button_primary_bg} onChange={(v) => setLocal({ ...local, button_primary_bg: v })} />
78
+ <ColorField label="Primary Text" value={local.button_primary_text} onChange={(v) => setLocal({ ...local, button_primary_text: v })} />
79
+ <FieldInput
80
+ label="Border Radius"
81
+ value={local.button_border_radius}
82
+ onChange={(v) => setLocal({ ...local, button_border_radius: v })}
83
+ placeholder="8px"
84
+ />
85
+ </div>
86
+ <div className="grid grid-cols-3 gap-4 mb-4">
87
+ <ColorField label="Secondary BG" value={local.button_secondary_bg} onChange={(v) => setLocal({ ...local, button_secondary_bg: v })} />
88
+ <ColorField label="Secondary Text" value={local.button_secondary_text} onChange={(v) => setLocal({ ...local, button_secondary_text: v })} />
89
+ </div>
90
+
91
+ <div className="flex gap-3 p-3 bg-neutral-50 rounded-lg mb-4">
92
+ <span
93
+ className="inline-block px-5 py-2 text-sm cursor-pointer"
94
+ style={{
95
+ backgroundColor: local.button_primary_bg,
96
+ color: local.button_primary_text,
97
+ borderRadius: local.button_border_radius,
98
+ border: "1px solid rgba(0,0,0,0.1)",
99
+ }}
100
+ >
101
+ Primary
102
+ </span>
103
+ <span
104
+ className="inline-block px-5 py-2 text-sm cursor-pointer"
105
+ style={{
106
+ backgroundColor: local.button_secondary_bg,
107
+ color: local.button_secondary_text,
108
+ borderRadius: local.button_border_radius,
109
+ }}
110
+ >
111
+ Secondary
112
+ </span>
113
+ </div>
114
+
115
+ <div className="flex justify-end">
116
+ <SaveButton onClick={() => onSave(local)} saving={saving} />
117
+ </div>
118
+ </Section>
119
+ );
120
+ }