@morphika/andami 0.2.12 → 0.2.14

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 (58) hide show
  1. package/README.md +2 -1
  2. package/app/admin/pages/[slug]/page.tsx +39 -2
  3. package/components/blocks/BlockRenderer.tsx +0 -7
  4. package/components/blocks/CoverSectionRenderer.tsx +295 -0
  5. package/components/blocks/ImageBlockRenderer.tsx +12 -10
  6. package/components/blocks/PageRenderer.tsx +13 -9
  7. package/components/blocks/VideoBlockRenderer.tsx +11 -6
  8. package/components/builder/BlockLivePreview.tsx +0 -5
  9. package/components/builder/BlockTypePicker.tsx +0 -1
  10. package/components/builder/ColorSwatchPicker.tsx +2 -2
  11. package/components/builder/CoverRowResizeHandle.tsx +180 -0
  12. package/components/builder/CoverSectionCanvas.tsx +260 -0
  13. package/components/builder/ReadOnlyFrame.tsx +127 -3
  14. package/components/builder/SectionTypePicker.tsx +29 -0
  15. package/components/builder/SectionV2Canvas.tsx +4 -1
  16. package/components/builder/SectionV2Column.tsx +15 -20
  17. package/components/builder/SettingsPanel.tsx +14 -0
  18. package/components/builder/SortableRow.tsx +7 -21
  19. package/components/builder/blockStyles.tsx +13 -14
  20. package/components/builder/editors/ImageBlockEditor.tsx +1 -0
  21. package/components/builder/editors/VideoBlockEditor.tsx +1 -0
  22. package/components/builder/editors/index.ts +0 -1
  23. package/components/builder/index.ts +1 -0
  24. package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
  25. package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
  26. package/components/builder/live-preview/RichTextEditor.tsx +23 -2
  27. package/components/builder/live-preview/index.ts +0 -1
  28. package/components/builder/settings-panel/BlockSettings.tsx +0 -7
  29. package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
  30. package/components/builder/settings-panel/index.ts +1 -0
  31. package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
  32. package/lib/animation/enter-types.ts +0 -1
  33. package/lib/animation/hover-effect-types.ts +0 -1
  34. package/lib/builder/defaults.ts +43 -22
  35. package/lib/builder/serializer/normalizers.ts +34 -1
  36. package/lib/builder/serializer/serializers.ts +39 -2
  37. package/lib/builder/store-blocks.ts +11 -3
  38. package/lib/builder/store-cover.ts +220 -0
  39. package/lib/builder/store-helpers.ts +81 -4
  40. package/lib/builder/store-sections.ts +12 -2
  41. package/lib/builder/store.ts +11 -2
  42. package/lib/builder/types.ts +15 -2
  43. package/lib/sanity/queries.ts +18 -4
  44. package/lib/sanity/types.ts +81 -45
  45. package/lib/version.ts +1 -1
  46. package/package.json +1 -1
  47. package/sanity/schemas/blocks/imageBlock.ts +1 -0
  48. package/sanity/schemas/blocks/index.ts +1 -2
  49. package/sanity/schemas/blocks/videoBlock.ts +1 -0
  50. package/sanity/schemas/index.ts +5 -3
  51. package/sanity/schemas/objects/coverSection.ts +317 -0
  52. package/sanity/schemas/objects/parallaxSlide.ts +0 -1
  53. package/sanity/schemas/page.ts +1 -1
  54. package/sanity/schemas/pageSectionV2.ts +0 -1
  55. package/components/blocks/CoverBlockRenderer.tsx +0 -261
  56. package/components/builder/editors/CoverBlockEditor.tsx +0 -550
  57. package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
  58. package/sanity/schemas/blocks/coverBlock.ts +0 -229
@@ -1,261 +0,0 @@
1
- "use client";
2
-
3
- import type { CoverBlock } from "../../lib/sanity/types";
4
- import { useAssetUrl } from "../../lib/contexts/AssetContext";
5
- import { handleImageRetry, handleVideoRetry } from "../../lib/asset-retry";
6
- import { parseColorField, colorToCSS, isGradient } from "../../lib/color-utils";
7
-
8
- function getOverlayStyle(
9
- overlay: CoverBlock["overlay"],
10
- opacity: number
11
- ): React.CSSProperties | null {
12
- const alpha = opacity / 100;
13
- switch (overlay) {
14
- case "dark":
15
- return { backgroundColor: `rgba(0,0,0,${alpha})` };
16
- case "light":
17
- return { backgroundColor: `rgba(255,255,255,${alpha})` };
18
- case "gradient-bottom":
19
- return {
20
- background: `linear-gradient(to top, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
21
- };
22
- case "gradient-top":
23
- return {
24
- background: `linear-gradient(to bottom, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
25
- };
26
- default:
27
- return null;
28
- }
29
- }
30
-
31
- function getAlignItems(v: CoverBlock["content_align_v"]): string {
32
- switch (v) {
33
- case "top":
34
- return "flex-start";
35
- case "bottom":
36
- return "flex-end";
37
- default:
38
- return "center";
39
- }
40
- }
41
-
42
- function getJustify(h: CoverBlock["content_align_h"]): string {
43
- switch (h) {
44
- case "left":
45
- return "flex-start";
46
- case "right":
47
- return "flex-end";
48
- default:
49
- return "center";
50
- }
51
- }
52
-
53
- function getTextAlign(
54
- h: CoverBlock["content_align_h"]
55
- ): "left" | "center" | "right" {
56
- return h || "center";
57
- }
58
-
59
- export default function CoverBlockRenderer({
60
- block,
61
- }: {
62
- block: CoverBlock;
63
- }) {
64
- const height =
65
- block.height === "custom" && block.custom_height
66
- ? block.custom_height
67
- : block.height || "100vh";
68
-
69
- const mobileHeight =
70
- block.mobile_height && block.mobile_height !== "same"
71
- ? block.mobile_height
72
- : null;
73
-
74
- // Phase 4: custom overlay_gradient takes precedence over hardcoded presets
75
- const overlayStyle: React.CSSProperties | null = (() => {
76
- if (block.overlay_gradient) {
77
- const parsed = parseColorField(block.overlay_gradient);
78
- if (isGradient(parsed)) {
79
- return { backgroundImage: colorToCSS(parsed) };
80
- }
81
- // Solid color overlay from gradient field
82
- return { backgroundColor: colorToCSS(parsed) };
83
- }
84
- return getOverlayStyle(block.overlay ?? "none", block.overlay_opacity ?? 50);
85
- })();
86
-
87
- const resolveAsset = useAssetUrl();
88
- const mediaSrc = block.media_path ? resolveAsset(block.media_path) : undefined;
89
- const posterSrc = block.video_poster
90
- ? resolveAsset(block.video_poster)
91
- : undefined;
92
- const isVideo = block.media_type === "video";
93
-
94
- // BLK-010: Validate objectFit against allowed CSS values
95
- const allowedObjectFit = new Set(["cover", "contain", "none", "fill", "scale-down"]);
96
- const objectFit = allowedObjectFit.has(block.background_size || "") ? block.background_size! : "cover";
97
- const objectPosition = block.background_position || "center center";
98
-
99
- // Guard: text color must be a solid hex string. Fallback if gradient slips through.
100
- const rawTextColor = block.text_color || "#ffffff";
101
- const textColor = typeof rawTextColor === "string" ? rawTextColor : "#ffffff";
102
-
103
- const ctaStyleClasses: Record<string, string> = {
104
- primary:
105
- "bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
106
- secondary:
107
- "bg-brand-dark text-white hover:bg-neutral-800",
108
- outline:
109
- "border-2 border-current bg-transparent hover:bg-white/10",
110
- text: "underline underline-offset-4 hover:opacity-70",
111
- };
112
-
113
- // BLK-002: Sanitize _key and mobileHeight before CSS interpolation
114
- // _key: allow only alphanumeric, hyphens, underscores (strip anything else)
115
- const safeKey = block._key?.replace(/[^a-zA-Z0-9_-]/g, "") || "";
116
- // mobileHeight: must match valid CSS height (number+unit or CSS keyword)
117
- const safeMobileHeight =
118
- mobileHeight && /^(\d+(\.\d+)?(px|vh|vw|em|rem|%|svh|dvh)|auto|inherit)$/i.test(mobileHeight)
119
- ? mobileHeight
120
- : null;
121
-
122
- return (
123
- <>
124
- {/* Mobile height override via inline style tag */}
125
- {safeMobileHeight && safeKey && (
126
- <style
127
- dangerouslySetInnerHTML={{
128
- __html: `@media(max-width:767px){.cover-block-${safeKey}{height:${safeMobileHeight}!important;min-height:${safeMobileHeight}!important;}}`,
129
- }}
130
- />
131
- )}
132
-
133
- <section
134
- className={`cover-block-${safeKey} relative flex overflow-hidden`}
135
- style={{
136
- height,
137
- minHeight: height,
138
- alignItems: getAlignItems(block.content_align_v),
139
- justifyContent: getJustify(block.content_align_h),
140
- color: textColor,
141
- }}
142
- >
143
- {/* Media layer — uses <img> instead of background-image for error recovery */}
144
- {mediaSrc && !isVideo && (
145
- /* eslint-disable-next-line @next/next/no-img-element */
146
- <img
147
- src={mediaSrc}
148
- alt={block.headline || ""}
149
- onError={handleImageRetry}
150
- className="absolute inset-0 h-full w-full"
151
- style={{
152
- objectFit: objectFit as React.CSSProperties["objectFit"],
153
- objectPosition,
154
- }}
155
- />
156
- )}
157
-
158
- {mediaSrc && isVideo && (
159
- <video
160
- src={mediaSrc}
161
- poster={posterSrc}
162
- autoPlay
163
- loop
164
- muted
165
- playsInline
166
- onError={handleVideoRetry}
167
- className="absolute inset-0 h-full w-full"
168
- style={{
169
- objectFit: objectFit as React.CSSProperties["objectFit"],
170
- objectPosition,
171
- }}
172
- />
173
- )}
174
-
175
- {/* Fallback: poster only if no media_path but video_poster exists */}
176
- {!mediaSrc && posterSrc && (
177
- /* eslint-disable-next-line @next/next/no-img-element */
178
- <img
179
- src={posterSrc}
180
- alt=""
181
- onError={handleImageRetry}
182
- className="absolute inset-0 h-full w-full"
183
- style={{ objectFit: "cover", objectPosition: "center" }}
184
- />
185
- )}
186
-
187
- {/* No media fallback */}
188
- {!mediaSrc && !posterSrc && (
189
- <div className="absolute inset-0 bg-brand-dark" />
190
- )}
191
-
192
- {/* Overlay */}
193
- {overlayStyle && <div className="absolute inset-0" style={overlayStyle} />}
194
-
195
- {/* Content */}
196
- <div
197
- className="relative z-10 flex flex-col gap-4 py-12"
198
- style={{
199
- maxWidth: block.content_max_width || "800px",
200
- textAlign: getTextAlign(block.content_align_h),
201
- width: "100%",
202
- paddingLeft: "var(--grid-padding, 24px)",
203
- paddingRight: "var(--grid-padding, 24px)",
204
- }}
205
- >
206
- {block.headline && (
207
- <h1 className="font-sans text-4xl uppercase tracking-widest md:text-6xl lg:text-7xl">
208
- {block.headline}
209
- </h1>
210
- )}
211
-
212
- {block.subheadline && (
213
- <p className="font-sans text-sm uppercase tracking-wider opacity-80 md:text-base">
214
- {block.subheadline}
215
- </p>
216
- )}
217
-
218
- {block.cta_button?.text && block.cta_button.url && (
219
- <div>
220
- <a
221
- href={block.cta_button.url}
222
- target={
223
- block.cta_button.target_blank ? "_blank" : undefined
224
- }
225
- rel={
226
- block.cta_button.target_blank
227
- ? "noopener noreferrer"
228
- : undefined
229
- }
230
- className={`inline-block px-6 py-3 font-sans text-sm uppercase tracking-wider transition ${
231
- ctaStyleClasses[block.cta_button.style || "primary"]
232
- }`}
233
- >
234
- {block.cta_button.text}
235
- </a>
236
- </div>
237
- )}
238
- </div>
239
-
240
- {/* Scroll indicator */}
241
- {block.show_scroll_indicator && (
242
- <div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 animate-bounce">
243
- <svg
244
- width="24"
245
- height="24"
246
- viewBox="0 0 24 24"
247
- fill="none"
248
- stroke="currentColor"
249
- strokeWidth="2"
250
- strokeLinecap="round"
251
- strokeLinejoin="round"
252
- aria-hidden="true"
253
- >
254
- <path d="M12 5v14M5 12l7 7 7-7" />
255
- </svg>
256
- </div>
257
- )}
258
- </section>
259
- </>
260
- );
261
- }