@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.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -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 +210 -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,202 @@
1
+ "use client";
2
+
3
+ import { useRef, useState, useEffect, useCallback } from "react";
4
+ import { useBuilderStore } from "../../lib/builder/store";
5
+
6
+ // ============================================
7
+ // CanvasToolbar — Floating mini toolbar
8
+ // ============================================
9
+
10
+ const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
11
+
12
+ interface CanvasToolbarProps {
13
+ /** Ref to the viewport element — needed for zoomToFit dimensions */
14
+ viewportRef?: React.RefObject<HTMLDivElement | null>;
15
+ /** Callback to trigger smooth animation on programmatic zoom/pan */
16
+ onAnimatedAction?: () => void;
17
+ }
18
+
19
+ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasToolbarProps) {
20
+ const zoom = useBuilderStore((s) => s.canvasZoom);
21
+ const tool = useBuilderStore((s) => s.canvasTool);
22
+ const setCanvasZoom = useBuilderStore((s) => s.setCanvasZoom);
23
+ const setCanvasTool = useBuilderStore((s) => s.setCanvasTool);
24
+ const zoomToPoint = useBuilderStore((s) => s.zoomToPoint);
25
+ const zoomToFit = useBuilderStore((s) => s.zoomToFit);
26
+
27
+ const [showPresets, setShowPresets] = useState(false);
28
+ const presetsRef = useRef<HTMLDivElement>(null);
29
+
30
+ // Close dropdown on outside click
31
+ useEffect(() => {
32
+ if (!showPresets) return;
33
+ function handleClick(e: MouseEvent) {
34
+ if (presetsRef.current && !presetsRef.current.contains(e.target as Node)) {
35
+ setShowPresets(false);
36
+ }
37
+ }
38
+ document.addEventListener("mousedown", handleClick);
39
+ return () => document.removeEventListener("mousedown", handleClick);
40
+ }, [showPresets]);
41
+
42
+ const handleZoomIn = useCallback(() => {
43
+ onAnimatedAction?.();
44
+ const newZoom = Math.min(2, zoom + 0.1);
45
+ setCanvasZoom(newZoom);
46
+ }, [zoom, setCanvasZoom, onAnimatedAction]);
47
+
48
+ const handleZoomOut = useCallback(() => {
49
+ onAnimatedAction?.();
50
+ const newZoom = Math.max(0.1, zoom - 0.1);
51
+ setCanvasZoom(newZoom);
52
+ }, [zoom, setCanvasZoom, onAnimatedAction]);
53
+
54
+ const handleFit = useCallback(() => {
55
+ onAnimatedAction?.();
56
+ const el = viewportRef?.current;
57
+ if (el) {
58
+ const rect = el.getBoundingClientRect();
59
+ zoomToFit(rect.width, rect.height);
60
+ }
61
+ }, [viewportRef, zoomToFit, onAnimatedAction]);
62
+
63
+ const handlePresetSelect = useCallback(
64
+ (pct: number) => {
65
+ onAnimatedAction?.();
66
+ setCanvasZoom(pct / 100);
67
+ setShowPresets(false);
68
+ },
69
+ [setCanvasZoom, onAnimatedAction]
70
+ );
71
+
72
+ const zoomPercent = Math.round(zoom * 100);
73
+
74
+ return (
75
+ <div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-px rounded-full bg-[#1a1a1a] shadow-xl px-1 py-1"
76
+ style={{ userSelect: "none" }}
77
+ >
78
+ {/* Select tool */}
79
+ <button
80
+ onClick={() => setCanvasTool("select")}
81
+ className={`flex items-center justify-center w-8 h-8 rounded-full text-xs transition-colors ${
82
+ tool === "select"
83
+ ? "bg-white/15 text-white"
84
+ : "text-neutral-400 hover:text-white hover:bg-white/10"
85
+ }`}
86
+ title="Select tool (V)"
87
+ aria-label="Select tool"
88
+ >
89
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
90
+ <path
91
+ d="M2 1L12 7L7 8L5 13L2 1Z"
92
+ stroke="currentColor"
93
+ strokeWidth="1.5"
94
+ strokeLinejoin="round"
95
+ />
96
+ </svg>
97
+ </button>
98
+
99
+ {/* Hand tool */}
100
+ <button
101
+ onClick={() => setCanvasTool("hand")}
102
+ className={`flex items-center justify-center w-8 h-8 rounded-full text-xs transition-colors ${
103
+ tool === "hand"
104
+ ? "bg-white/15 text-white"
105
+ : "text-neutral-400 hover:text-white hover:bg-white/10"
106
+ }`}
107
+ title="Hand tool (H)"
108
+ aria-label="Hand tool"
109
+ >
110
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
111
+ {/* Five-finger open hand — cleaner icon */}
112
+ <path d="M18 11V6.5a1.5 1.5 0 0 0-3 0V11" />
113
+ <path d="M15 9.5V4a1.5 1.5 0 0 0-3 0v7" />
114
+ <path d="M12 11V5.5a1.5 1.5 0 0 0-3 0v6.5" />
115
+ <path d="M9 11V9a1.5 1.5 0 0 0-3 0v3c0 4.42 2.69 8 6 8h1c3.31 0 6-3.58 6-8V11" />
116
+ </svg>
117
+ </button>
118
+
119
+ {/* Divider */}
120
+ <div className="w-px h-5 bg-white/20 mx-1" />
121
+
122
+ {/* Zoom out */}
123
+ <button
124
+ onClick={handleZoomOut}
125
+ className="flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
126
+ title="Zoom out (Ctrl + −)"
127
+ aria-label="Zoom out"
128
+ >
129
+
130
+ </button>
131
+
132
+ {/* Zoom percentage (clickable dropdown) */}
133
+ <div className="relative" ref={presetsRef}>
134
+ <button
135
+ onClick={() => setShowPresets((v) => !v)}
136
+ className="flex items-center justify-center min-w-[48px] h-8 text-white text-[11px] font-medium hover:bg-white/10 rounded-md transition-colors px-1"
137
+ title="Click to select zoom level"
138
+ aria-label="Select zoom level"
139
+ >
140
+ {zoomPercent}%
141
+ </button>
142
+
143
+ {showPresets && (
144
+ <div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl overflow-hidden py-1 min-w-[80px]">
145
+ {ZOOM_PRESETS.map((pct) => (
146
+ <button
147
+ key={pct}
148
+ onClick={() => handlePresetSelect(pct)}
149
+ className={`w-full text-left px-3 py-1.5 text-[11px] transition-colors ${
150
+ zoomPercent === pct
151
+ ? "text-white bg-white/10"
152
+ : "text-neutral-400 hover:text-white hover:bg-white/5"
153
+ }`}
154
+ >
155
+ {pct}%
156
+ </button>
157
+ ))}
158
+ </div>
159
+ )}
160
+ </div>
161
+
162
+ {/* Zoom in */}
163
+ <button
164
+ onClick={handleZoomIn}
165
+ className="flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
166
+ title="Zoom in (Ctrl + +)"
167
+ aria-label="Zoom in"
168
+ >
169
+ +
170
+ </button>
171
+
172
+ {/* Divider */}
173
+ <div className="w-px h-5 bg-white/20 mx-1" />
174
+
175
+ {/* Fit */}
176
+ <button
177
+ onClick={handleFit}
178
+ className="flex items-center justify-center w-8 h-8 rounded-full text-neutral-400 hover:text-white hover:bg-white/10 transition-colors"
179
+ title="Zoom to fit (Ctrl + 0)"
180
+ aria-label="Zoom to fit"
181
+ >
182
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
183
+ <rect
184
+ x="2"
185
+ y="2"
186
+ width="10"
187
+ height="10"
188
+ rx="1.5"
189
+ stroke="currentColor"
190
+ strokeWidth="1.5"
191
+ />
192
+ <path
193
+ d="M5 2V0M9 2V0M5 14V12M9 14V12M0 5H2M0 9H2M12 5H14M12 9H14"
194
+ stroke="currentColor"
195
+ strokeWidth="1"
196
+ opacity="0.5"
197
+ />
198
+ </svg>
199
+ </button>
200
+ </div>
201
+ );
202
+ }
@@ -0,0 +1,243 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ColorPicker — Full-featured HSL color picker with hex/RGB display.
5
+ * Used in the Global Styles palette editor and inline block editors.
6
+ */
7
+
8
+ import { useState, useRef, useCallback, useEffect } from "react";
9
+ import { hexToHSL, hsvToHex, hexToHSV, isValidHex } from "../../lib/color-utils";
10
+
11
+ // ─── ColorPicker Component ───
12
+
13
+ interface ColorPickerProps {
14
+ color: string;
15
+ onChange: (hex: string) => void;
16
+ onClose?: () => void;
17
+ /** Label for the confirm button */
18
+ confirmLabel?: string;
19
+ /** If true, shows a compact version (no confirm/cancel buttons, live onChange) */
20
+ compact?: boolean;
21
+ }
22
+
23
+ export default function ColorPicker({
24
+ color,
25
+ onChange,
26
+ onClose,
27
+ confirmLabel = "Confirm",
28
+ compact = false,
29
+ }: ColorPickerProps) {
30
+ const initHsv = hexToHSV(isValidHex(color) ? color : "#ffffff");
31
+ const [hue, setHue] = useState(initHsv.h);
32
+ const [sat, setSat] = useState(initHsv.s);
33
+ const [val, setVal] = useState(initHsv.v);
34
+ const [hex, setHex] = useState(isValidHex(color) ? color : "#ffffff");
35
+ const canvasRef = useRef<HTMLDivElement>(null);
36
+ const hueRef = useRef<HTMLDivElement>(null);
37
+ const draggingCanvas = useRef(false);
38
+ const draggingHue = useRef(false);
39
+
40
+ // Sync hex from HSV
41
+ const updateHex = useCallback((h: number, s: number, v: number) => {
42
+ const newHex = hsvToHex(h, s, v);
43
+ setHex(newHex);
44
+ if (compact) onChange(newHex);
45
+ }, [compact, onChange]);
46
+
47
+ // Canvas (saturation + value) interaction
48
+ const handleCanvasMove = useCallback((e: React.MouseEvent | MouseEvent) => {
49
+ const rect = canvasRef.current?.getBoundingClientRect();
50
+ if (!rect) return;
51
+ const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
52
+ const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
53
+ const s = Math.round(x * 100);
54
+ const v = Math.round((1 - y) * 100);
55
+ setSat(s);
56
+ setVal(v);
57
+ updateHex(hue, s, v);
58
+ }, [hue, updateHex]);
59
+
60
+ // Hue slider interaction
61
+ const handleHueMove = useCallback((e: React.MouseEvent | MouseEvent) => {
62
+ const rect = hueRef.current?.getBoundingClientRect();
63
+ if (!rect) return;
64
+ const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
65
+ const h = Math.round(x * 360);
66
+ setHue(h);
67
+ updateHex(h, sat, val);
68
+ }, [sat, val, updateHex]);
69
+
70
+ // Global mouse up
71
+ useEffect(() => {
72
+ const handleUp = () => {
73
+ draggingCanvas.current = false;
74
+ draggingHue.current = false;
75
+ };
76
+ const handleMove = (e: MouseEvent) => {
77
+ if (draggingCanvas.current) handleCanvasMove(e);
78
+ if (draggingHue.current) handleHueMove(e);
79
+ };
80
+ window.addEventListener("mouseup", handleUp);
81
+ window.addEventListener("mousemove", handleMove);
82
+ return () => {
83
+ window.removeEventListener("mouseup", handleUp);
84
+ window.removeEventListener("mousemove", handleMove);
85
+ };
86
+ }, [handleCanvasMove, handleHueMove]);
87
+
88
+ const handleHexInput = (v: string) => {
89
+ setHex(v);
90
+ if (isValidHex(v)) {
91
+ const hsv = hexToHSV(v);
92
+ setHue(hsv.h);
93
+ setSat(hsv.s);
94
+ setVal(hsv.v);
95
+ if (compact) onChange(v);
96
+ }
97
+ };
98
+
99
+ // Keyboard handlers for canvas (saturation/value) and hue slider
100
+ const handleCanvasKeyDown = useCallback((e: React.KeyboardEvent) => {
101
+ const step = e.shiftKey ? 10 : 2;
102
+ let newSat = sat;
103
+ let newVal = val;
104
+ switch (e.key) {
105
+ case "ArrowRight": newSat = Math.min(100, sat + step); break;
106
+ case "ArrowLeft": newSat = Math.max(0, sat - step); break;
107
+ case "ArrowUp": newVal = Math.min(100, val + step); break;
108
+ case "ArrowDown": newVal = Math.max(0, val - step); break;
109
+ default: return;
110
+ }
111
+ e.preventDefault();
112
+ setSat(newSat);
113
+ setVal(newVal);
114
+ updateHex(hue, newSat, newVal);
115
+ }, [sat, val, hue, updateHex]);
116
+
117
+ const handleHueKeyDown = useCallback((e: React.KeyboardEvent) => {
118
+ const step = e.shiftKey ? 20 : 5;
119
+ let newHue = hue;
120
+ switch (e.key) {
121
+ case "ArrowRight": newHue = Math.min(360, hue + step); break;
122
+ case "ArrowLeft": newHue = Math.max(0, hue - step); break;
123
+ default: return;
124
+ }
125
+ e.preventDefault();
126
+ setHue(newHue);
127
+ updateHex(newHue, sat, val);
128
+ }, [hue, sat, val, updateHex]);
129
+
130
+ const r = parseInt(hex.slice(1, 3), 16) || 0;
131
+ const g = parseInt(hex.slice(3, 5), 16) || 0;
132
+ const b = parseInt(hex.slice(5, 7), 16) || 0;
133
+
134
+ return (
135
+ <div className="bg-white rounded-2xl p-4 w-[260px] shadow-xl border border-neutral-200">
136
+ {/* Saturation/Value canvas */}
137
+ <div
138
+ ref={canvasRef}
139
+ role="slider"
140
+ tabIndex={0}
141
+ aria-label="Color saturation and brightness"
142
+ aria-valuetext={`Saturation ${sat}%, Brightness ${val}%`}
143
+ className="w-full h-[160px] rounded-xl cursor-crosshair relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
144
+ style={{
145
+ background: `linear-gradient(to bottom, transparent, #000), linear-gradient(to right, #fff, hsl(${hue}, 100%, 50%))`,
146
+ }}
147
+ onMouseDown={(e) => { draggingCanvas.current = true; handleCanvasMove(e); }}
148
+ onKeyDown={handleCanvasKeyDown}
149
+ >
150
+ <div
151
+ className="absolute w-4 h-4 rounded-full border-2 border-white pointer-events-none"
152
+ style={{
153
+ left: `${sat}%`,
154
+ top: `${100 - val}%`,
155
+ transform: "translate(-50%, -50%)",
156
+ boxShadow: "0 0 0 1px rgba(0,0,0,0.3), 0 2px 6px rgba(0,0,0,0.3)",
157
+ }}
158
+ />
159
+ </div>
160
+
161
+ {/* Hue slider */}
162
+ <div
163
+ ref={hueRef}
164
+ role="slider"
165
+ tabIndex={0}
166
+ aria-label="Color hue"
167
+ aria-valuemin={0}
168
+ aria-valuemax={360}
169
+ aria-valuenow={hue}
170
+ aria-valuetext={`Hue ${hue} degrees`}
171
+ className="w-full h-3.5 rounded-full mt-3 cursor-pointer relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
172
+ style={{
173
+ background: "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
174
+ }}
175
+ onMouseDown={(e) => { draggingHue.current = true; handleHueMove(e); }}
176
+ onKeyDown={handleHueKeyDown}
177
+ >
178
+ <div
179
+ className="absolute w-4 h-4 rounded-full border-2 border-white pointer-events-none"
180
+ style={{
181
+ left: `${(hue / 360) * 100}%`,
182
+ top: "50%",
183
+ transform: "translate(-50%, -50%)",
184
+ background: `hsl(${hue}, 100%, 50%)`,
185
+ boxShadow: "0 1px 3px rgba(0,0,0,0.4)",
186
+ }}
187
+ />
188
+ </div>
189
+
190
+ {/* Hex input */}
191
+ <div className="flex items-center gap-2.5 mt-3.5">
192
+ <div
193
+ className="w-8 h-8 rounded-lg border border-neutral-200 shrink-0"
194
+ style={{ background: hex }}
195
+ />
196
+ <input
197
+ value={hex.toUpperCase()}
198
+ onChange={(e) => handleHexInput(e.target.value)}
199
+ className="flex-1 bg-neutral-50 border border-neutral-200 rounded-lg px-2.5 py-1.5 text-neutral-900 text-xs font-mono outline-none focus:border-[#076bff] focus:ring-2 focus:ring-[#076bff]/10 transition-colors"
200
+ />
201
+ </div>
202
+
203
+ {/* RGB readout */}
204
+ <div className="flex gap-2 mt-2.5">
205
+ {[
206
+ { label: "R", val: r },
207
+ { label: "G", val: g },
208
+ { label: "B", val: b },
209
+ ].map(({ label, val: v }) => (
210
+ <div key={label} className="flex-1">
211
+ <div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-0.5">{label}</div>
212
+ <div className="bg-neutral-50 border border-neutral-200 rounded-md px-2 py-1 text-neutral-600 text-[11px] text-center font-mono">
213
+ {v}
214
+ </div>
215
+ </div>
216
+ ))}
217
+ </div>
218
+
219
+ {/* Buttons (non-compact mode) */}
220
+ {!compact && (
221
+ <div className="flex gap-2 mt-3.5">
222
+ {onClose && (
223
+ <button
224
+ onClick={onClose}
225
+ className="flex-1 py-2 rounded-lg border border-neutral-200 bg-transparent text-neutral-500 text-xs font-mono cursor-pointer hover:border-neutral-300 hover:text-neutral-700 transition-colors"
226
+ >
227
+ Cancel
228
+ </button>
229
+ )}
230
+ <button
231
+ onClick={() => { onChange(hex); onClose?.(); }}
232
+ className="flex-1 py-2 rounded-lg border-none bg-[#076bff] text-white text-xs font-mono font-semibold cursor-pointer hover:bg-[#0559d4] transition-colors"
233
+ >
234
+ {confirmLabel}
235
+ </button>
236
+ </div>
237
+ )}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ // ─── Export helpers ───
243
+ export { isValidHex, hexToHSL, hexToHSV, hsvToHex };