@morphika/andami 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/(site)/[slug]/page.tsx +2 -2
- package/app/(site)/layout.tsx +1 -0
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +2 -2
- package/app/admin/layout.tsx +2 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/settings/route.ts +40 -15
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +6 -3
- package/components/admin/index.ts +7 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
- package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
- package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
- package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
- package/components/admin/nav-builder/index.ts +2 -0
- package/components/blocks/BlockRenderer.tsx +65 -13
- package/components/blocks/ButtonBlockRenderer.tsx +29 -6
- package/components/blocks/CoverBlockRenderer.tsx +36 -14
- package/components/blocks/ImageBlockRenderer.tsx +5 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
- package/components/blocks/PageRenderer.tsx +4 -2
- package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
- package/components/blocks/SectionRenderer.tsx +9 -8
- package/components/blocks/SectionV2Renderer.tsx +8 -8
- package/components/blocks/SpacerBlockRenderer.tsx +4 -2
- package/components/blocks/TextBlockRenderer.tsx +9 -4
- package/components/builder/BuilderCanvas.tsx +10 -4
- package/components/builder/ColorPicker.tsx +51 -243
- package/components/builder/ColorSwatchPicker.tsx +214 -274
- package/components/builder/DndWrapper.tsx +5 -2
- package/components/builder/SectionV2Canvas.tsx +15 -4
- package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
- package/components/builder/color-picker/AlphaSlider.tsx +141 -0
- package/components/builder/color-picker/AngleControl.tsx +138 -0
- package/components/builder/color-picker/ColorInputs.tsx +105 -0
- package/components/builder/color-picker/EyedropperButton.tsx +74 -0
- package/components/builder/color-picker/GradientBar.tsx +222 -0
- package/components/builder/color-picker/GradientPreview.tsx +53 -0
- package/components/builder/color-picker/HueSlider.tsx +124 -0
- package/components/builder/color-picker/MeshCanvas.tsx +172 -0
- package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
- package/components/builder/color-picker/MeshPointList.tsx +200 -0
- package/components/builder/color-picker/PositionControl.tsx +158 -0
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
- package/components/builder/color-picker/StopEditor.tsx +178 -0
- package/components/builder/color-picker/SwatchBar.tsx +93 -0
- package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
- package/components/builder/color-picker/index.ts +62 -0
- package/components/builder/color-picker/types.ts +115 -0
- package/components/builder/color-picker/utils.ts +138 -0
- package/components/builder/editors/CoverBlockEditor.tsx +86 -32
- package/components/builder/editors/ProjectGridEditor.tsx +51 -4
- package/components/builder/hooks/useColumnDrag.ts +25 -27
- package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
- package/components/builder/settings-panel/LayoutTab.tsx +382 -310
- package/components/builder/settings-panel/PageSettings.tsx +6 -4
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
- package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
- package/components/ui/Navbar.tsx +95 -25
- package/components/ui/PortfolioTracker.tsx +3 -3
- package/lib/assets.ts +1 -1
- package/lib/auth.ts +1 -1
- package/lib/builder/gradient-presets.ts +128 -0
- package/lib/builder/layout-styles.ts +16 -10
- package/lib/builder/serializer.ts +1 -0
- package/lib/builder/store-blocks.ts +48 -61
- package/lib/builder/store-helpers.ts +31 -14
- package/lib/builder/store.ts +59 -41
- package/lib/builder/types.ts +14 -0
- package/lib/color-utils.ts +200 -0
- package/lib/revalidate.ts +2 -2
- package/lib/sanity/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- package/package.json +8 -2
- package/sanity/schemas/siteSettings.ts +34 -0
- package/styles/base.css +3 -3
- package/app/globals.css +0 -7
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Picker v2 — Barrel exports.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1: Solid color picker with modal UI.
|
|
5
|
+
* Phase 3: Gradient components (GradientBar, GradientPreview, AngleControl,
|
|
6
|
+
* PositionControl, MeshCanvas, MeshPointList, StopEditor).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { default as UnifiedColorPicker } from "./UnifiedColorPicker";
|
|
10
|
+
export { default as SaturationCanvas } from "./SaturationCanvas";
|
|
11
|
+
export { default as HueSlider } from "./HueSlider";
|
|
12
|
+
export { default as AlphaSlider } from "./AlphaSlider";
|
|
13
|
+
export { default as ColorInputs } from "./ColorInputs";
|
|
14
|
+
export { default as EyedropperButton } from "./EyedropperButton";
|
|
15
|
+
export { default as SwatchBar } from "./SwatchBar";
|
|
16
|
+
|
|
17
|
+
// Phase 3: Gradient components
|
|
18
|
+
export { default as GradientBar } from "./GradientBar";
|
|
19
|
+
export { default as GradientPreview } from "./GradientPreview";
|
|
20
|
+
export { default as AngleControl } from "./AngleControl";
|
|
21
|
+
export { default as PositionControl } from "./PositionControl";
|
|
22
|
+
export { default as MeshCanvas } from "./MeshCanvas";
|
|
23
|
+
export { default as MeshPointList } from "./MeshPointList";
|
|
24
|
+
export { default as MeshPointEditor } from "./MeshPointEditor";
|
|
25
|
+
export { default as StopEditor } from "./StopEditor";
|
|
26
|
+
|
|
27
|
+
// Types
|
|
28
|
+
export type {
|
|
29
|
+
ColorFormat,
|
|
30
|
+
HSV,
|
|
31
|
+
HSL,
|
|
32
|
+
RGB,
|
|
33
|
+
GradientStop,
|
|
34
|
+
MeshPoint,
|
|
35
|
+
LinearGradient,
|
|
36
|
+
RadialGradient,
|
|
37
|
+
MeshGradient,
|
|
38
|
+
GradientValue,
|
|
39
|
+
ColorField,
|
|
40
|
+
UnifiedColorPickerProps,
|
|
41
|
+
SaturationCanvasProps,
|
|
42
|
+
HueSliderProps,
|
|
43
|
+
AlphaSliderProps,
|
|
44
|
+
ColorInputsProps,
|
|
45
|
+
EyedropperButtonProps,
|
|
46
|
+
SwatchBarProps,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
// Utils
|
|
50
|
+
export {
|
|
51
|
+
hexToRGB,
|
|
52
|
+
rgbToHex,
|
|
53
|
+
hslToHex,
|
|
54
|
+
formatColorValue,
|
|
55
|
+
parseColorInput,
|
|
56
|
+
clamp,
|
|
57
|
+
hexToHSL,
|
|
58
|
+
hexToHSV,
|
|
59
|
+
hsvToHex,
|
|
60
|
+
hexToRgba,
|
|
61
|
+
isValidHex,
|
|
62
|
+
} from "./utils";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Picker v2 — Type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Gradient/ColorField types are canonical in lib/sanity/types.ts.
|
|
5
|
+
* Re-exported here for convenience within the color-picker module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ColorSwatch } from "../../../lib/sanity/types";
|
|
9
|
+
|
|
10
|
+
// Re-export canonical gradient types from lib/sanity/types
|
|
11
|
+
export type {
|
|
12
|
+
GradientStop,
|
|
13
|
+
MeshPoint,
|
|
14
|
+
LinearGradient,
|
|
15
|
+
RadialGradient,
|
|
16
|
+
MeshGradient,
|
|
17
|
+
GradientValue,
|
|
18
|
+
ColorField,
|
|
19
|
+
} from "../../../lib/sanity/types";
|
|
20
|
+
|
|
21
|
+
// ─── Color Formats ───
|
|
22
|
+
|
|
23
|
+
/** Supported color input/display formats */
|
|
24
|
+
export type ColorFormat = "hex" | "rgb" | "hsl";
|
|
25
|
+
|
|
26
|
+
// ─── HSV / HSL / RGB ───
|
|
27
|
+
|
|
28
|
+
export interface HSV {
|
|
29
|
+
h: number; // 0-360
|
|
30
|
+
s: number; // 0-100
|
|
31
|
+
v: number; // 0-100
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HSL {
|
|
35
|
+
h: number; // 0-360
|
|
36
|
+
s: number; // 0-100
|
|
37
|
+
l: number; // 0-100
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RGB {
|
|
41
|
+
r: number; // 0-255
|
|
42
|
+
g: number; // 0-255
|
|
43
|
+
b: number; // 0-255
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Component Props ───
|
|
47
|
+
|
|
48
|
+
import type { ColorField } from "../../../lib/sanity/types";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Props for the unified color picker modal.
|
|
52
|
+
* Phase 3: value and onChange use ColorField (string | GradientValue).
|
|
53
|
+
*/
|
|
54
|
+
export interface UnifiedColorPickerProps {
|
|
55
|
+
/** Current color value — hex string or GradientValue */
|
|
56
|
+
value: ColorField;
|
|
57
|
+
/** Callback when color is confirmed */
|
|
58
|
+
onChange: (value: ColorField) => void;
|
|
59
|
+
/** Close modal */
|
|
60
|
+
onClose: () => void;
|
|
61
|
+
/** Palette swatches from global styles */
|
|
62
|
+
swatches?: ColorSwatch[];
|
|
63
|
+
/** Label for the confirm button */
|
|
64
|
+
confirmLabel?: string;
|
|
65
|
+
/** Alpha value 0-1 (used for solid tab display) */
|
|
66
|
+
alpha?: number;
|
|
67
|
+
/** Callback when alpha changes */
|
|
68
|
+
onAlphaChange?: (alpha: number) => void;
|
|
69
|
+
/** Allow gradient tabs? false = only Solid tab visible (default false) */
|
|
70
|
+
allowGradients?: boolean;
|
|
71
|
+
/** Live preview callback — called throttled during drag interactions (Phase 4).
|
|
72
|
+
* Does NOT fire on confirm — use onChange for that. */
|
|
73
|
+
onPreview?: (value: ColorField) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SaturationCanvasProps {
|
|
77
|
+
hue: number;
|
|
78
|
+
saturation: number;
|
|
79
|
+
value: number;
|
|
80
|
+
onChange: (saturation: number, value: number) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface HueSliderProps {
|
|
84
|
+
hue: number;
|
|
85
|
+
onChange: (hue: number) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AlphaSliderProps {
|
|
89
|
+
/** Current hex color (for the gradient display) */
|
|
90
|
+
color: string;
|
|
91
|
+
/** Alpha value 0-1 */
|
|
92
|
+
alpha: number;
|
|
93
|
+
onChange: (alpha: number) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ColorInputsProps {
|
|
97
|
+
hex: string;
|
|
98
|
+
onHexChange: (hex: string) => void;
|
|
99
|
+
alpha?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface EyedropperButtonProps {
|
|
103
|
+
onColorPicked: (hex: string) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SwatchBarProps {
|
|
107
|
+
/** Current hex value (for active state) */
|
|
108
|
+
value: string;
|
|
109
|
+
/** Callback on swatch click */
|
|
110
|
+
onSelect: (hex: string) => void;
|
|
111
|
+
/** User palette swatches */
|
|
112
|
+
swatches?: ColorSwatch[];
|
|
113
|
+
/** Current hex to potentially add to palette */
|
|
114
|
+
currentColor?: string;
|
|
115
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Picker v2 — Utility functions.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports core conversions from lib/color-utils.ts and adds
|
|
5
|
+
* picker-specific helpers (HSV↔HSL, RGB↔hex, format display).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
hexToHSL,
|
|
10
|
+
hexToHSV,
|
|
11
|
+
hsvToHex,
|
|
12
|
+
hexToRgba,
|
|
13
|
+
isValidHex,
|
|
14
|
+
} from "../../../lib/color-utils";
|
|
15
|
+
|
|
16
|
+
import type { HSV, HSL, RGB, ColorFormat } from "./types";
|
|
17
|
+
|
|
18
|
+
// ─── Re-exports ───
|
|
19
|
+
export { hexToHSL, hexToHSV, hsvToHex, hexToRgba, isValidHex };
|
|
20
|
+
|
|
21
|
+
// ─── RGB helpers ───
|
|
22
|
+
|
|
23
|
+
/** Parse a hex string (#RRGGBB) into RGB components. */
|
|
24
|
+
export function hexToRGB(hex: string): RGB {
|
|
25
|
+
const clean = hex.replace("#", "");
|
|
26
|
+
return {
|
|
27
|
+
r: parseInt(clean.slice(0, 2), 16) || 0,
|
|
28
|
+
g: parseInt(clean.slice(2, 4), 16) || 0,
|
|
29
|
+
b: parseInt(clean.slice(4, 6), 16) || 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Convert RGB components to hex string (#rrggbb). */
|
|
34
|
+
export function rgbToHex(r: number, g: number, b: number): string {
|
|
35
|
+
const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));
|
|
36
|
+
const toH = (n: number) => clamp(n).toString(16).padStart(2, "0");
|
|
37
|
+
return `#${toH(r)}${toH(g)}${toH(b)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── HSL helpers ───
|
|
41
|
+
|
|
42
|
+
/** Convert HSL to hex. */
|
|
43
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
44
|
+
s /= 100;
|
|
45
|
+
l /= 100;
|
|
46
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
47
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
48
|
+
const m = l - c / 2;
|
|
49
|
+
let r = 0,
|
|
50
|
+
g = 0,
|
|
51
|
+
b = 0;
|
|
52
|
+
if (h < 60) {
|
|
53
|
+
r = c;
|
|
54
|
+
g = x;
|
|
55
|
+
} else if (h < 120) {
|
|
56
|
+
r = x;
|
|
57
|
+
g = c;
|
|
58
|
+
} else if (h < 180) {
|
|
59
|
+
g = c;
|
|
60
|
+
b = x;
|
|
61
|
+
} else if (h < 240) {
|
|
62
|
+
g = x;
|
|
63
|
+
b = c;
|
|
64
|
+
} else if (h < 300) {
|
|
65
|
+
r = x;
|
|
66
|
+
b = c;
|
|
67
|
+
} else {
|
|
68
|
+
r = c;
|
|
69
|
+
b = x;
|
|
70
|
+
}
|
|
71
|
+
return rgbToHex(
|
|
72
|
+
Math.round((r + m) * 255),
|
|
73
|
+
Math.round((g + m) * 255),
|
|
74
|
+
Math.round((b + m) * 255)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Format display ───
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format a hex color for display in the given format.
|
|
82
|
+
* Returns the string to show in the color input.
|
|
83
|
+
*/
|
|
84
|
+
export function formatColorValue(hex: string, format: ColorFormat): string {
|
|
85
|
+
if (!isValidHex(hex)) return hex;
|
|
86
|
+
switch (format) {
|
|
87
|
+
case "hex":
|
|
88
|
+
return hex.toUpperCase();
|
|
89
|
+
case "rgb": {
|
|
90
|
+
const { r, g, b } = hexToRGB(hex);
|
|
91
|
+
return `${r}, ${g}, ${b}`;
|
|
92
|
+
}
|
|
93
|
+
case "hsl": {
|
|
94
|
+
const { h, s, l } = hexToHSL(hex);
|
|
95
|
+
return `${h}, ${s}%, ${l}%`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a color input string in the given format back to hex.
|
|
102
|
+
* Returns null if the input is not valid for the format.
|
|
103
|
+
*/
|
|
104
|
+
export function parseColorInput(
|
|
105
|
+
input: string,
|
|
106
|
+
format: ColorFormat
|
|
107
|
+
): string | null {
|
|
108
|
+
const trimmed = input.trim();
|
|
109
|
+
switch (format) {
|
|
110
|
+
case "hex": {
|
|
111
|
+
let v = trimmed;
|
|
112
|
+
if (!v.startsWith("#")) v = "#" + v;
|
|
113
|
+
return isValidHex(v) ? v.toLowerCase() : null;
|
|
114
|
+
}
|
|
115
|
+
case "rgb": {
|
|
116
|
+
const parts = trimmed.split(/[,\s]+/).map((s) => parseInt(s, 10));
|
|
117
|
+
if (parts.length !== 3 || parts.some((n) => isNaN(n) || n < 0 || n > 255))
|
|
118
|
+
return null;
|
|
119
|
+
return rgbToHex(parts[0], parts[1], parts[2]);
|
|
120
|
+
}
|
|
121
|
+
case "hsl": {
|
|
122
|
+
const cleaned = trimmed.replace(/%/g, "");
|
|
123
|
+
const parts = cleaned.split(/[,\s]+/).map((s) => parseFloat(s));
|
|
124
|
+
if (parts.length !== 3) return null;
|
|
125
|
+
const [h, s, l] = parts;
|
|
126
|
+
if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
|
|
127
|
+
if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) return null;
|
|
128
|
+
return hslToHex(h, s, l);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clamp a number to [min, max].
|
|
135
|
+
*/
|
|
136
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
137
|
+
return Math.max(min, Math.min(max, value));
|
|
138
|
+
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
SELECT_CLASS,
|
|
16
16
|
} from "./shared";
|
|
17
17
|
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
18
|
+
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
19
|
+
import type { ColorField } from "../../../lib/sanity/types";
|
|
18
20
|
|
|
19
21
|
interface Props {
|
|
20
22
|
block: CoverBlock;
|
|
@@ -354,41 +356,93 @@ export default function CoverBlockEditor({ block }: Props) {
|
|
|
354
356
|
|
|
355
357
|
{/* ========== OVERLAY ========== */}
|
|
356
358
|
<SettingsSection title="Cover Effects">
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
359
|
+
{/* Toggle: Custom gradient vs Preset overlay */}
|
|
360
|
+
<SettingsField label="Overlay Mode">
|
|
361
|
+
<div className="flex gap-1">
|
|
362
|
+
{(["preset", "custom"] as const).map((mode) => {
|
|
363
|
+
const isActive = mode === "custom" ? !!block.overlay_gradient : !block.overlay_gradient;
|
|
364
|
+
return (
|
|
365
|
+
<button
|
|
366
|
+
key={mode}
|
|
367
|
+
onClick={() => {
|
|
368
|
+
if (mode === "custom" && !block.overlay_gradient) {
|
|
369
|
+
// Switch to custom: initialize with a dark semi-transparent solid
|
|
370
|
+
update({ overlay_gradient: "#00000080" });
|
|
371
|
+
} else if (mode === "preset" && block.overlay_gradient) {
|
|
372
|
+
// Switch to preset: clear overlay_gradient
|
|
373
|
+
update({ overlay_gradient: undefined });
|
|
374
|
+
}
|
|
375
|
+
}}
|
|
376
|
+
className={`flex-1 rounded border py-1 text-[10px] capitalize transition-colors ${
|
|
377
|
+
isActive
|
|
378
|
+
? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
|
|
379
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
380
|
+
}`}
|
|
381
|
+
>
|
|
382
|
+
{mode === "preset" ? "Preset" : "Custom"}
|
|
383
|
+
</button>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</div>
|
|
373
387
|
</SettingsField>
|
|
374
388
|
|
|
375
|
-
{
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
max={100}
|
|
382
|
-
value={block.overlay_opacity ?? 50}
|
|
389
|
+
{/* Preset overlay controls */}
|
|
390
|
+
{!block.overlay_gradient && (
|
|
391
|
+
<>
|
|
392
|
+
<SettingsField label="Overlay">
|
|
393
|
+
<select
|
|
394
|
+
value={block.overlay || "dark"}
|
|
383
395
|
onChange={(e) =>
|
|
384
|
-
update({
|
|
396
|
+
update({
|
|
397
|
+
overlay: e.target.value as CoverBlock["overlay"],
|
|
398
|
+
})
|
|
385
399
|
}
|
|
386
|
-
className=
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
400
|
+
className={SELECT_CLASS}
|
|
401
|
+
>
|
|
402
|
+
<option value="none">None</option>
|
|
403
|
+
<option value="dark">Dark</option>
|
|
404
|
+
<option value="light">Light</option>
|
|
405
|
+
<option value="gradient-bottom">Gradient (Bottom)</option>
|
|
406
|
+
<option value="gradient-top">Gradient (Top)</option>
|
|
407
|
+
</select>
|
|
408
|
+
</SettingsField>
|
|
409
|
+
|
|
410
|
+
{block.overlay && block.overlay !== "none" && (
|
|
411
|
+
<SettingsField label="Opacity">
|
|
412
|
+
<div className="flex items-center gap-2">
|
|
413
|
+
<input
|
|
414
|
+
type="range"
|
|
415
|
+
min={0}
|
|
416
|
+
max={100}
|
|
417
|
+
value={block.overlay_opacity ?? 50}
|
|
418
|
+
onChange={(e) =>
|
|
419
|
+
update({ overlay_opacity: parseInt(e.target.value, 10) })
|
|
420
|
+
}
|
|
421
|
+
className="flex-1 accent-[#076bff]"
|
|
422
|
+
/>
|
|
423
|
+
<span className="text-xs text-neutral-500 w-8 text-right">
|
|
424
|
+
{block.overlay_opacity ?? 50}%
|
|
425
|
+
</span>
|
|
426
|
+
</div>
|
|
427
|
+
</SettingsField>
|
|
428
|
+
)}
|
|
429
|
+
</>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{/* Custom overlay gradient (Phase 4) */}
|
|
433
|
+
{block.overlay_gradient && (
|
|
434
|
+
<SettingsField label="Overlay Color">
|
|
435
|
+
<ColorSwatchPicker
|
|
436
|
+
value={(() => {
|
|
437
|
+
const parsed = parseColorField(block.overlay_gradient);
|
|
438
|
+
return typeof parsed === "string" ? parsed : parsed;
|
|
439
|
+
})()}
|
|
440
|
+
onChange={(val: ColorField) => {
|
|
441
|
+
update({ overlay_gradient: serializeColorField(val) });
|
|
442
|
+
}}
|
|
443
|
+
swatches={paletteSwatches}
|
|
444
|
+
allowGradients
|
|
445
|
+
/>
|
|
392
446
|
</SettingsField>
|
|
393
447
|
)}
|
|
394
448
|
</SettingsSection>
|
|
@@ -472,7 +526,7 @@ export default function CoverBlockEditor({ block }: Props) {
|
|
|
472
526
|
<SettingsField label="Text Color">
|
|
473
527
|
<ColorSwatchPicker
|
|
474
528
|
value={block.text_color || ""}
|
|
475
|
-
onChange={(hex) => update({ text_color: hex || "#ffffff" })}
|
|
529
|
+
onChange={(hex) => update({ text_color: (typeof hex === "string" ? hex : "#ffffff") || "#ffffff" })}
|
|
476
530
|
swatches={paletteSwatches}
|
|
477
531
|
/>
|
|
478
532
|
</SettingsField>
|
|
@@ -232,6 +232,53 @@ function CardRatioChips({
|
|
|
232
232
|
);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// ============================================
|
|
236
|
+
// Section title icons (small, inline — matching BlockLayoutTab)
|
|
237
|
+
// ============================================
|
|
238
|
+
|
|
239
|
+
function GridSectionIcon() {
|
|
240
|
+
return (
|
|
241
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
242
|
+
<rect x="1.5" y="1.5" width="4.5" height="4.5" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.6" />
|
|
243
|
+
<rect x="8" y="1.5" width="4.5" height="4.5" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.6" />
|
|
244
|
+
<rect x="1.5" y="8" width="4.5" height="4.5" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.6" />
|
|
245
|
+
<rect x="8" y="8" width="4.5" height="4.5" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.6" />
|
|
246
|
+
</svg>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function AppearanceSectionIcon() {
|
|
251
|
+
return (
|
|
252
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
253
|
+
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.5" />
|
|
254
|
+
<path d="M7 2 A5 5 0 0 1 7 12 Z" fill="currentColor" opacity="0.3" />
|
|
255
|
+
<circle cx="7" cy="7" r="1.5" fill="currentColor" opacity="0.6" />
|
|
256
|
+
</svg>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function VideoSectionIcon() {
|
|
261
|
+
return (
|
|
262
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
263
|
+
<rect x="1.5" y="3" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.5" />
|
|
264
|
+
<path d="M10 6 L12.5 4.5 L12.5 9.5 L10 8 Z" fill="currentColor" opacity="0.5" />
|
|
265
|
+
</svg>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ProjectsSectionIcon() {
|
|
270
|
+
return (
|
|
271
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
272
|
+
<rect x="1.5" y="2" width="11" height="3" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.5" />
|
|
273
|
+
<rect x="1.5" y="7" width="11" height="3" rx="1" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.5" />
|
|
274
|
+
<circle cx="4" cy="3.5" r="0.8" fill="currentColor" opacity="0.6" />
|
|
275
|
+
<circle cx="4" cy="8.5" r="0.8" fill="currentColor" opacity="0.6" />
|
|
276
|
+
<rect x="6" y="3" width="4" height="1" rx="0.5" fill="currentColor" opacity="0.4" />
|
|
277
|
+
<rect x="6" y="8" width="4" height="1" rx="0.5" fill="currentColor" opacity="0.4" />
|
|
278
|
+
</svg>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
235
282
|
// ============================================
|
|
236
283
|
// Main Editor
|
|
237
284
|
// ============================================
|
|
@@ -409,7 +456,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
409
456
|
{isResponsive && <ViewportBadge />}
|
|
410
457
|
|
|
411
458
|
{/* ─── Grid Settings ─── */}
|
|
412
|
-
<SettingsSection title="Grid">
|
|
459
|
+
<SettingsSection title="Grid" icon={<GridSectionIcon />}>
|
|
413
460
|
<SettingsField label="Columns">
|
|
414
461
|
<RangeSlider
|
|
415
462
|
value={effectiveColumns}
|
|
@@ -450,7 +497,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
450
497
|
</SettingsSection>
|
|
451
498
|
|
|
452
499
|
{/* ─── Appearance ─── */}
|
|
453
|
-
<SettingsSection title="Appearance">
|
|
500
|
+
<SettingsSection title="Appearance" icon={<AppearanceSectionIcon />}>
|
|
454
501
|
<SettingsField label="Hover">
|
|
455
502
|
<SegmentedControl
|
|
456
503
|
options={HOVER_EFFECT_OPTIONS}
|
|
@@ -477,7 +524,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
477
524
|
</SettingsSection>
|
|
478
525
|
|
|
479
526
|
{/* ─── Video ─── */}
|
|
480
|
-
<SettingsSection title="Video">
|
|
527
|
+
<SettingsSection title="Video" icon={<VideoSectionIcon />}>
|
|
481
528
|
<SettingsField label="Mode">
|
|
482
529
|
<SegmentedControl
|
|
483
530
|
options={VIDEO_MODE_OPTIONS}
|
|
@@ -488,7 +535,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
488
535
|
</SettingsSection>
|
|
489
536
|
|
|
490
537
|
{/* ─── Projects ─── */}
|
|
491
|
-
<SettingsSection title={`Projects (${(block.projects || []).length})`}>
|
|
538
|
+
<SettingsSection title={`Projects (${(block.projects || []).length})`} icon={<ProjectsSectionIcon />}>
|
|
492
539
|
{(block.projects || []).length === 0 ? (
|
|
493
540
|
<p className="text-xs text-neutral-400 py-2">
|
|
494
541
|
No projects selected. Add projects below.
|
|
@@ -177,7 +177,11 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
177
177
|
const [insertBetween, setInsertBetween] = useState<InsertBetween | null>(null);
|
|
178
178
|
const [overlayPosition, setOverlayPosition] = useState<{ x: number; y: number } | null>(null);
|
|
179
179
|
|
|
180
|
-
// --- Mutable ref for drag state
|
|
180
|
+
// --- Mutable ref for drag state ---
|
|
181
|
+
// RC-002 fix: Store actions are read directly from useBuilderStore.getState()
|
|
182
|
+
// at execution time instead of being synced via useEffect. This eliminates
|
|
183
|
+
// the stale closure window between render and effect, where rapid
|
|
184
|
+
// click+drag could execute stale action references.
|
|
181
185
|
const dragRef = useRef({
|
|
182
186
|
sectionKey: "",
|
|
183
187
|
columnKey: "",
|
|
@@ -186,26 +190,18 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
186
190
|
startX: 0,
|
|
187
191
|
startY: 0,
|
|
188
192
|
draggedEl: null as HTMLElement | null, // pointer-events guard
|
|
189
|
-
// Store actions — updated every render via useEffect
|
|
190
|
-
swapColumnV2: null as ((s: string, d: string, t: string) => void) | null,
|
|
191
|
-
moveColumnToGapV2: null as ((s: string, c: string, r: number, col: number, sp: number) => void) | null,
|
|
192
|
-
moveColumnV2: null as ((s: string, c: string, r: number, col: number) => void) | null,
|
|
193
|
-
updateSectionV2Responsive: null as ((s: string, r: PageSectionV2["responsive"]) => void) | null,
|
|
194
193
|
});
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
dragRef.current.moveColumnV2 = moveColumnV2;
|
|
207
|
-
dragRef.current.updateSectionV2Responsive = updateSectionV2Responsive;
|
|
208
|
-
});
|
|
195
|
+
/** Read a fresh store action at call time — never stale */
|
|
196
|
+
const getActions = () => {
|
|
197
|
+
const s = useBuilderStore.getState();
|
|
198
|
+
return {
|
|
199
|
+
swapColumnV2: s.swapColumnV2,
|
|
200
|
+
moveColumnToGapV2: s.moveColumnToGapV2,
|
|
201
|
+
moveColumnV2: s.moveColumnV2,
|
|
202
|
+
updateSectionV2Responsive: s.updateSectionV2Responsive,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
209
205
|
|
|
210
206
|
// --- Stable mousemove handler (empty deps — delegates to dragRef) ---
|
|
211
207
|
const stableMouseMove = useCallback((e: MouseEvent) => {
|
|
@@ -373,23 +369,25 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
373
369
|
}
|
|
374
370
|
}
|
|
375
371
|
|
|
376
|
-
// Execute the drop action
|
|
372
|
+
// Execute the drop action — read actions fresh from store (RC-002)
|
|
377
373
|
if (finalTarget) {
|
|
378
|
-
const
|
|
374
|
+
const storeState = useBuilderStore.getState();
|
|
375
|
+
const activeViewport = storeState.activeViewport;
|
|
379
376
|
const isResponsive = activeViewport !== "desktop";
|
|
377
|
+
const actions = getActions();
|
|
380
378
|
|
|
381
379
|
if (finalTarget.type === "swap" && finalTarget.columnKey) {
|
|
382
380
|
if (!isResponsive) {
|
|
383
|
-
|
|
381
|
+
actions.swapColumnV2(sectionKey, columnKey, finalTarget.columnKey);
|
|
384
382
|
} else {
|
|
385
383
|
executeResponsiveSwap(
|
|
386
384
|
sectionKey, columnKey, finalTarget.columnKey, activeViewport,
|
|
387
|
-
|
|
385
|
+
actions.updateSectionV2Responsive
|
|
388
386
|
);
|
|
389
387
|
}
|
|
390
388
|
} else if (finalTarget.type === "gap") {
|
|
391
389
|
if (!isResponsive) {
|
|
392
|
-
|
|
390
|
+
actions.moveColumnToGapV2(
|
|
393
391
|
sectionKey, columnKey,
|
|
394
392
|
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!
|
|
395
393
|
);
|
|
@@ -398,12 +396,12 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
398
396
|
sectionKey, columnKey,
|
|
399
397
|
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!,
|
|
400
398
|
activeViewport,
|
|
401
|
-
|
|
399
|
+
actions.updateSectionV2Responsive
|
|
402
400
|
);
|
|
403
401
|
}
|
|
404
402
|
} else if (finalTarget.type === "insert") {
|
|
405
403
|
if (!isResponsive) {
|
|
406
|
-
|
|
404
|
+
actions.moveColumnV2(
|
|
407
405
|
sectionKey, columnKey,
|
|
408
406
|
finalTarget.insertRow!, finalTarget.insertCol!
|
|
409
407
|
);
|
|
@@ -412,7 +410,7 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
412
410
|
sectionKey, columnKey,
|
|
413
411
|
finalTarget.insertRow!, finalTarget.insertCol!,
|
|
414
412
|
activeViewport,
|
|
415
|
-
|
|
413
|
+
actions.updateSectionV2Responsive
|
|
416
414
|
);
|
|
417
415
|
}
|
|
418
416
|
}
|