@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,126 @@
1
+ /**
2
+ * Hover Effect System — Type definitions
3
+ *
4
+ * Unified hover effects that combine CSS-based hovers (scale-up, lift, etc.)
5
+ * and shader-based hovers (ripple, rgb-shift, pixelate) into one type system.
6
+ *
7
+ * Block-level only — no cascade. Each block type has a curated set of
8
+ * available hover presets.
9
+ *
10
+ * Session 116 — Animation UX Refactor.
11
+ */
12
+
13
+ import type { ContentBlock } from "../../lib/sanity/types";
14
+
15
+ type BlockType = ContentBlock["_type"];
16
+
17
+ // ── Easing ─────────────────────────────────────────────────────────
18
+
19
+ export type HoverEasing =
20
+ | "ease"
21
+ | "ease-in"
22
+ | "ease-out"
23
+ | "ease-in-out"
24
+ | "linear";
25
+
26
+ // ── Shader smoothness ──────────────────────────────────────────────
27
+
28
+ export type ShaderSmoothness = "snappy" | "normal" | "smooth";
29
+
30
+ // ── CSS-based hover presets ────────────────────────────────────────
31
+
32
+ export type CSSHoverPreset =
33
+ | "none"
34
+ | "scale-up"
35
+ | "scale-down"
36
+ | "lift"
37
+ | "tilt-3d"
38
+ | "color-shift"
39
+ | "blur-reveal"
40
+ | "border-glow";
41
+
42
+ // ── Shader-based hover presets ─────────────────────────────────────
43
+
44
+ export type ShaderHoverPreset =
45
+ | "ripple"
46
+ | "rgb-shift"
47
+ | "pixelate";
48
+
49
+ /**
50
+ * Union of ALL hover presets — CSS-based + shader-based.
51
+ * Used as the field type in HoverEffectConfig.
52
+ */
53
+ export type HoverPreset = CSSHoverPreset | ShaderHoverPreset;
54
+
55
+ // ── Per-block-type preset registry ─────────────────────────────────
56
+
57
+ /**
58
+ * Which hover presets are available for each block type.
59
+ * Empty array = no hover effects available for that block type.
60
+ *
61
+ * Based on the registry table in ANIMATION-UX-ROADMAP.md.
62
+ */
63
+ export const BLOCK_HOVER_PRESETS: Record<BlockType, readonly HoverPreset[]> = {
64
+ textBlock: [], // text hovers feel wrong
65
+ imageBlock: ["scale-up", "lift", "tilt-3d", "ripple", "rgb-shift", "pixelate"],
66
+ imageGridBlock: ["tilt-3d"],
67
+ videoBlock: [], // video has play/pause interaction
68
+ buttonBlock: ["scale-up", "lift", "border-glow"],
69
+ coverBlock: ["scale-up", "lift", "ripple", "rgb-shift", "pixelate"],
70
+ spacerBlock: [], // invisible
71
+ projectGridBlock: ["scale-up", "lift"], // per-card effect
72
+ };
73
+
74
+ // ── Hover effect config ────────────────────────────────────────────
75
+
76
+ /**
77
+ * Unified hover effect config. Replaces both HoverAnimationConfig
78
+ * and ShaderEffectConfig.
79
+ *
80
+ * Stored on blocks only (no cascade — hover is block-level).
81
+ * Shader-specific fields are only relevant when preset is a ShaderHoverPreset.
82
+ */
83
+ export interface HoverEffectConfig {
84
+ preset?: HoverPreset;
85
+ duration?: number; // ms, default 300 (CSS transitions)
86
+ easing?: HoverEasing; // default "ease-out"
87
+ // Shader-specific (only when preset is ripple/rgb-shift/pixelate):
88
+ shader_speed?: number; // 0.5–3.0, default 1.0
89
+ shader_smoothness?: ShaderSmoothness; // default "normal"
90
+ }
91
+
92
+ /**
93
+ * Fully resolved config — all fields present.
94
+ */
95
+ export interface ResolvedHoverEffectConfig {
96
+ preset: HoverPreset;
97
+ duration: number;
98
+ easing: HoverEasing;
99
+ shader_speed: number;
100
+ shader_smoothness: ShaderSmoothness;
101
+ }
102
+
103
+ // ── Defaults ───────────────────────────────────────────────────────
104
+
105
+ export const HOVER_EFFECT_DEFAULTS: ResolvedHoverEffectConfig = {
106
+ preset: "none",
107
+ duration: 300,
108
+ easing: "ease-out",
109
+ shader_speed: 1.0,
110
+ shader_smoothness: "normal",
111
+ };
112
+
113
+ // ── Resolved shader config (used by ShaderCanvas / HoverAnimationWrapper) ──
114
+ // Moved here from lib/shader/types.ts in Session 122.
115
+
116
+ /**
117
+ * Fully resolved shader effect config — all fields guaranteed.
118
+ * Used internally by ShaderCanvas and HoverAnimationWrapper.
119
+ */
120
+ export interface ResolvedShaderEffectConfig {
121
+ preset: ShaderHoverPreset | "none";
122
+ trigger: "hover" | "scroll";
123
+ intensity: number;
124
+ speed: number;
125
+ smoothing: number; // Resolved from ShaderSmoothness → lerp factor
126
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Client-side asset retry utilities with cache busting and visual feedback.
3
+ *
4
+ * When an image/video fails to load, the browser may have cached the
5
+ * 302 redirect from /api/assets/... pointing to a stale link.
6
+ * Simply re-setting `img.src = img.src` would reuse that cached redirect.
7
+ *
8
+ * Cache busting appends `?_r=N&t=<timestamp>` to force the browser to
9
+ * make a fresh request to the API route, which generates a new link.
10
+ *
11
+ * Visual feedback: after exhausting retries, elements get a `data-asset-failed`
12
+ * attribute that CSS uses to show a subtle placeholder instead of the browser's
13
+ * broken image icon. A manual retry is available via click/tap.
14
+ */
15
+
16
+ /** Max retry attempts for media that fails to load */
17
+ export const MAX_RETRIES = 3;
18
+
19
+ /** Data attribute to track retry count */
20
+ export const RETRY_ATTR = "data-retries";
21
+
22
+ /** Data attribute set when all retries are exhausted */
23
+ export const FAILED_ATTR = "data-asset-failed";
24
+
25
+ /** Backoff delays in ms for each retry (exponential: 1s, 2s, 4s) */
26
+ const RETRY_DELAYS = [1000, 2000, 4000];
27
+
28
+ /**
29
+ * Strip any existing retry query params from a URL, then append fresh ones.
30
+ * This ensures each retry gets a unique URL that bypasses the browser cache.
31
+ */
32
+ export function cacheBustUrl(url: string, retryCount: number): string {
33
+ // Remove existing retry params
34
+ const base = url.replace(/[?&](_r|t)=[^&]*/g, "").replace(/\?$/, "");
35
+ const separator = base.includes("?") ? "&" : "?";
36
+ return `${base}${separator}_r=${retryCount}&t=${Date.now()}`;
37
+ }
38
+
39
+ /**
40
+ * Mark an element as failed and show visual placeholder.
41
+ * The element becomes clickable to trigger a manual retry.
42
+ */
43
+ function markAsFailed(el: HTMLImageElement | HTMLVideoElement) {
44
+ el.setAttribute(FAILED_ATTR, "1");
45
+
46
+ // For images: make the broken icon invisible and show a CSS placeholder via parent
47
+ if (el instanceof HTMLImageElement) {
48
+ // Store original src for manual retry
49
+ if (!el.dataset.originalSrc) {
50
+ el.dataset.originalSrc = el.src.replace(/[?&](_r|t)=[^&]*/g, "").replace(/\?$/, "");
51
+ }
52
+ // Set up manual retry on click
53
+ el.style.cursor = "pointer";
54
+ el.title = "Tap to retry loading";
55
+ el.removeEventListener("click", manualRetryHandler);
56
+ el.addEventListener("click", manualRetryHandler);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Manual retry handler — resets retry counter and tries again with fresh cache bust.
62
+ */
63
+ function manualRetryHandler(e: Event) {
64
+ const img = e.currentTarget as HTMLImageElement;
65
+ img.removeAttribute(FAILED_ATTR);
66
+ img.removeAttribute(RETRY_ATTR);
67
+ img.style.cursor = "";
68
+ img.title = "";
69
+ img.src = cacheBustUrl(img.dataset.originalSrc || img.src, 0);
70
+ }
71
+
72
+ /**
73
+ * Generic onError handler for <img> elements with cache-busting retry.
74
+ * Use directly as `onError={handleImageRetry}` or wrap in useCallback.
75
+ *
76
+ * After MAX_RETRIES, sets `data-asset-failed` attribute for CSS-based
77
+ * visual feedback and enables click-to-retry.
78
+ */
79
+ export function handleImageRetry(e: React.SyntheticEvent<HTMLImageElement>) {
80
+ const img = e.currentTarget;
81
+ const retries = parseInt(img.getAttribute(RETRY_ATTR) || "0", 10);
82
+ if (retries < MAX_RETRIES) {
83
+ const next = retries + 1;
84
+ img.setAttribute(RETRY_ATTR, String(next));
85
+ const delay = RETRY_DELAYS[retries] ?? RETRY_DELAYS[RETRY_DELAYS.length - 1];
86
+ setTimeout(() => {
87
+ img.src = cacheBustUrl(img.src, next);
88
+ }, delay);
89
+ } else {
90
+ markAsFailed(img);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Generic onError handler for <video> elements with cache-busting retry.
96
+ */
97
+ export function handleVideoRetry(e: React.SyntheticEvent<HTMLVideoElement>) {
98
+ const video = e.currentTarget;
99
+ const retries = parseInt(video.getAttribute(RETRY_ATTR) || "0", 10);
100
+ if (retries < MAX_RETRIES) {
101
+ const next = retries + 1;
102
+ video.setAttribute(RETRY_ATTR, String(next));
103
+ const delay = RETRY_DELAYS[retries] ?? RETRY_DELAYS[RETRY_DELAYS.length - 1];
104
+ setTimeout(() => {
105
+ video.src = cacheBustUrl(video.src, next);
106
+ video.load();
107
+ }, delay);
108
+ } else {
109
+ markAsFailed(video);
110
+ }
111
+ }
package/lib/assets.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Asset URL resolution — provider-aware.
3
+ *
4
+ * Assets are stored as relative paths in Sanity (e.g. "projects/hero.jpg").
5
+ * This module resolves them to full URLs based on the active storage provider.
6
+ *
7
+ * Resolution strategies:
8
+ *
9
+ * 1. **R2 direct URL (fastest):** If `NEXT_PUBLIC_R2_BUCKET_URL` is set,
10
+ * assets resolve directly to the R2 CDN URL. No proxy, no API call.
11
+ * This is baked into the client bundle at build time.
12
+ *
13
+ * 2. **Proxy fallback (runtime switching):** If no R2 env var, assets go
14
+ * through `/api/assets/[...path]` which checks the active provider at
15
+ * runtime and redirects accordingly. This supports provider switching
16
+ * without redeploying.
17
+ *
18
+ * @example
19
+ * assetUrl("projects/house-of-delights/cover.jpg")
20
+ * // R2 direct: "https://assets.example.com/projects/house-of-delights/cover.jpg"
21
+ * // Or proxy: "/api/assets/projects/house-of-delights/cover.jpg"
22
+ */
23
+
24
+ import { logger } from "../lib/logger";
25
+
26
+ /**
27
+ * Resolve a relative asset path to a full URL (public site).
28
+ *
29
+ * Priority:
30
+ * 1. NEXT_PUBLIC_R2_BUCKET_URL → direct R2 CDN URL (zero latency)
31
+ * 2. NEXT_PUBLIC_ASSET_BASE_URL → custom base (e.g. "/api/assets")
32
+ * 3. "/api/assets" → default proxy route
33
+ */
34
+ export function assetUrl(path: string | undefined | null): string {
35
+ if (!path) {
36
+ if (process.env.NODE_ENV === "development") {
37
+ logger.warn("[Assets]", "assetUrl() called with empty path");
38
+ }
39
+ return "/placeholder-asset.svg";
40
+ }
41
+
42
+ // #7: Normalize path — strip all leading slashes to prevent double-slash URLs
43
+ const cleanPath = path.replace(/^\/+/, "");
44
+
45
+ // R2 direct mode: when env var is set, skip proxy entirely.
46
+ // This is the zero-latency path — URL resolves to R2 CDN directly.
47
+ const r2Base = process.env.NEXT_PUBLIC_R2_BUCKET_URL;
48
+ if (r2Base) {
49
+ return `${r2Base.replace(/\/+$/, "")}/${cleanPath}`;
50
+ }
51
+
52
+ // Proxy mode: route through /api/assets which handles provider detection
53
+ // at runtime (supports provider switching without env var changes).
54
+ const base = process.env.NEXT_PUBLIC_ASSET_BASE_URL;
55
+ const resolvedBase = base || "/api/assets";
56
+ return `${resolvedBase.replace(/\/+$/, "")}/${cleanPath}`;
57
+ }
58
+
59
+ /**
60
+ * Resolve a relative asset path via the admin proxy.
61
+ *
62
+ * In the editor (/admin), images load through `/api/admin/assets/file?path=...`
63
+ * which handles auth + provider detection internally (R2 direct URL redirect).
64
+ */
65
+ export function adminAssetUrl(path: string | undefined | null): string {
66
+ if (!path) return "/placeholder-asset.svg";
67
+ return `/api/admin/assets/file?path=${encodeURIComponent(path)}`;
68
+ }
69
+
70
+ /**
71
+ * Resolve a relative asset path to its thumbnail URL (public).
72
+ *
73
+ * Thumbnails live in `_thumbs/{original_path}` with .jpg extension.
74
+ * The base URL resolution is provider-aware (same as assetUrl).
75
+ */
76
+ export function thumbUrl(path: string | undefined | null): string {
77
+ if (!path) return assetUrl(path);
78
+ // #40: Case-insensitive extension replacement for thumbnails (.JPG, .PNG, etc.)
79
+ const thumbPath = `_thumbs/${path.replace(/\.[^.]+$/i, ".jpg")}`;
80
+ return assetUrl(thumbPath);
81
+ }
82
+
83
+ /**
84
+ * Resolve a relative asset path to its thumbnail URL (admin proxy).
85
+ *
86
+ * Same as thumbUrl but routes through the admin proxy for builder use.
87
+ */
88
+ export function adminThumbUrl(path: string | undefined | null): string {
89
+ if (!path) return adminAssetUrl(path);
90
+ const thumbPath = `_thumbs/${path.replace(/\.[^.]+$/, ".jpg")}`;
91
+ return adminAssetUrl(thumbPath);
92
+ }
package/lib/audit.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Structured audit logging for admin operations.
3
+ *
4
+ * Logs admin actions with consistent format to server console.
5
+ * Future: can be extended to write to a persistent store
6
+ * (e.g., Sanity document, external logging service).
7
+ */
8
+
9
+ export interface AuditLogEntry {
10
+ /** Action performed: 'page.create', 'settings.update', 'r2.connect', etc. */
11
+ action: string;
12
+ /** Timestamp of the action */
13
+ timestamp: string;
14
+ /** Additional context about the action */
15
+ details?: Record<string, unknown>;
16
+ }
17
+
18
+ /**
19
+ * Log an admin action to the server console with structured format.
20
+ *
21
+ * Usage:
22
+ * auditLog("page.create", { slug: "about", page_type: "about" });
23
+ * auditLog("settings.update", { section: "navigation" });
24
+ * auditLog("r2.connect", { bucket: "my-assets" });
25
+ */
26
+ export function auditLog(action: string, details?: Record<string, unknown>): void {
27
+ const entry: AuditLogEntry = {
28
+ action,
29
+ timestamp: new Date().toISOString(),
30
+ ...(details ? { details } : {}),
31
+ };
32
+
33
+ // Structured JSON log — easily parseable by log aggregation tools
34
+ console.log(`[AUDIT] ${JSON.stringify(entry)}`);
35
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Admin token generation & validation using HMAC-SHA256.
3
+ * Uses Web Crypto API (works in Edge Runtime, Node.js, and browsers).
4
+ *
5
+ * Security: Tokens include a timestamp nonce and use a dedicated server-side
6
+ * secret (ADMIN_TOKEN_SECRET). Tokens expire after 24 hours.
7
+ *
8
+ * Used by: middleware.ts, lib/auth.ts, app/api/admin/auth/route.ts
9
+ */
10
+
11
+ const encoder = new TextEncoder();
12
+
13
+ /** Token validity period: 24 hours */
14
+ const TOKEN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
15
+
16
+ /**
17
+ * Derive the HMAC signing key from server-side secrets.
18
+ * Uses ADMIN_TOKEN_SECRET (preferred) with password as fallback component.
19
+ */
20
+ async function getSigningKey(password: string): Promise<CryptoKey> {
21
+ const tokenSecret = process.env.ADMIN_TOKEN_SECRET || "";
22
+ if (!tokenSecret) {
23
+ console.warn(
24
+ "[Security] ADMIN_TOKEN_SECRET is not set. Token security is degraded. " +
25
+ "Set this env var for production. Generate one with: openssl rand -base64 32"
26
+ );
27
+ }
28
+ // Combine dedicated secret + password for key material
29
+ const keyMaterial = `${tokenSecret}:${password}`;
30
+ return crypto.subtle.importKey(
31
+ "raw",
32
+ encoder.encode(keyMaterial),
33
+ { name: "HMAC", hash: "SHA-256" },
34
+ false,
35
+ ["sign", "verify"]
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Sign a payload string with HMAC-SHA256 and return hex.
41
+ */
42
+ async function hmacSign(key: CryptoKey, payload: string): Promise<string> {
43
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
44
+ return Array.from(new Uint8Array(signature))
45
+ .map((b) => b.toString(16).padStart(2, "0"))
46
+ .join("");
47
+ }
48
+
49
+ /**
50
+ * Generate a new admin token with embedded timestamp nonce.
51
+ * Format: `{timestamp}.{hmac_hex}`
52
+ */
53
+ export async function generateAdminToken(password: string): Promise<string> {
54
+ const timestamp = Date.now().toString();
55
+ const key = await getSigningKey(password);
56
+ const signature = await hmacSign(key, `admin_session:${timestamp}`);
57
+ return `${timestamp}.${signature}`;
58
+ }
59
+
60
+ /**
61
+ * Validate an admin token: check format, expiration, and HMAC signature.
62
+ * Returns true if the token is valid and not expired.
63
+ */
64
+ export async function validateAdminToken(token: string): Promise<boolean> {
65
+ const pw = process.env.ADMIN_PASSWORD;
66
+ if (!pw) return false;
67
+
68
+ // Parse token format: timestamp.signature
69
+ const dotIndex = token.indexOf(".");
70
+ if (dotIndex === -1) return false;
71
+
72
+ const timestamp = token.substring(0, dotIndex);
73
+ const providedSignature = token.substring(dotIndex + 1);
74
+
75
+ // Validate timestamp is a number
76
+ const tokenTime = parseInt(timestamp, 10);
77
+ if (isNaN(tokenTime)) return false;
78
+
79
+ // Check token expiration (24h)
80
+ const age = Date.now() - tokenTime;
81
+ if (age < 0 || age > TOKEN_MAX_AGE_MS) return false;
82
+
83
+ // Recompute expected signature and compare
84
+ const key = await getSigningKey(pw);
85
+ const expectedSignature = await hmacSign(key, `admin_session:${timestamp}`);
86
+
87
+ // Constant-time comparison to prevent timing attacks
88
+ if (providedSignature.length !== expectedSignature.length) return false;
89
+ let mismatch = 0;
90
+ for (let i = 0; i < expectedSignature.length; i++) {
91
+ mismatch |= providedSignature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
92
+ }
93
+ return mismatch === 0;
94
+ }
package/lib/auth.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { cookies } from "next/headers";
2
+ import { validateAdminToken } from "../lib/auth-token";
3
+
4
+ /**
5
+ * Verify that the current request is authenticated as admin.
6
+ * For use in server-side API routes and Server Components.
7
+ */
8
+ export async function isAdminAuthenticated(): Promise<boolean> {
9
+ const cookieStore = await cookies();
10
+ const token = cookieStore.get("admin_token")?.value;
11
+ if (!token) return false;
12
+ return validateAdminToken(token);
13
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Helper utilities for the cascade engine.
3
+ *
4
+ * Extracted from store.ts to reduce duplication and enable unit testing.
5
+ * The main pattern here is mapping cascade engine output (lightweight position objects)
6
+ * back to full SectionColumn objects (preserving blocks).
7
+ */
8
+
9
+ import type { SectionColumn, ContentBlock } from "../../lib/sanity/types";
10
+ import type { CascadeColumn } from "./cascade";
11
+
12
+ // Re-export so existing consumers that import from cascade-helpers still work
13
+ export type { CascadeColumn } from "./cascade";
14
+
15
+ /**
16
+ * Map cascade engine output back to full SectionColumn objects,
17
+ * preserving blocks from the original columns.
18
+ *
19
+ * The cascade engine only deals with positions (_key, grid_column, grid_row, span).
20
+ * This function re-attaches the blocks from the original columns by key lookup.
21
+ * New columns (not in the block map) get empty block arrays.
22
+ */
23
+ export function applyBlocksToColumns(
24
+ cascadeResult: CascadeColumn[],
25
+ originalColumns: SectionColumn[]
26
+ ): SectionColumn[] {
27
+ const blockMap = new Map<string, ContentBlock[]>(
28
+ originalColumns.map((c) => [c._key, c.blocks])
29
+ );
30
+
31
+ return cascadeResult.map((cc) => ({
32
+ _key: cc._key,
33
+ grid_column: cc.grid_column,
34
+ grid_row: cc.grid_row,
35
+ span: cc.span,
36
+ blocks: blockMap.get(cc._key) || [],
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * Extract lightweight cascade columns from full SectionColumns.
42
+ * Used as input to cascade engine functions.
43
+ */
44
+ export function toCascadeColumns(columns: SectionColumn[]): CascadeColumn[] {
45
+ return columns.map((c) => ({
46
+ _key: c._key,
47
+ grid_column: c.grid_column,
48
+ grid_row: c.grid_row,
49
+ span: c.span,
50
+ }));
51
+ }