@morphika/andami 0.5.1 → 0.5.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.
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -320
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -286
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -316
- package/components/builder/BlockTypePicker.tsx +1 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -363
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +69 -67
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
- package/components/builder/blockStyles.tsx +192 -185
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -242
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +6 -6
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +468 -417
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +4 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +98 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -69
- package/sanity/schemas/blocks/index.ts +12 -11
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -117
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AlphaSlider — Transparency slider with checkerboard pattern.
|
|
5
|
-
*
|
|
6
|
-
* Shows a gradient from transparent to the current color over a
|
|
7
|
-
* checker background. Mouse + touch + keyboard support.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useRef, useCallback, useEffect } from "react";
|
|
11
|
-
import type { AlphaSliderProps } from "./types";
|
|
12
|
-
import { clamp } from "./utils";
|
|
13
|
-
|
|
14
|
-
export default function AlphaSlider({
|
|
15
|
-
color,
|
|
16
|
-
alpha,
|
|
17
|
-
onChange,
|
|
18
|
-
}: AlphaSliderProps) {
|
|
19
|
-
const trackRef = useRef<HTMLDivElement>(null);
|
|
20
|
-
const dragging = useRef(false);
|
|
21
|
-
|
|
22
|
-
const computeFromX = useCallback(
|
|
23
|
-
(clientX: number) => {
|
|
24
|
-
const rect = trackRef.current?.getBoundingClientRect();
|
|
25
|
-
if (!rect) return;
|
|
26
|
-
const x = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
27
|
-
onChange(Math.round(x * 100) / 100);
|
|
28
|
-
},
|
|
29
|
-
[onChange]
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const handleMouseDown = useCallback(
|
|
33
|
-
(e: React.MouseEvent) => {
|
|
34
|
-
e.preventDefault();
|
|
35
|
-
dragging.current = true;
|
|
36
|
-
computeFromX(e.clientX);
|
|
37
|
-
},
|
|
38
|
-
[computeFromX]
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const handleTouchStart = useCallback(
|
|
42
|
-
(e: React.TouchEvent) => {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
dragging.current = true;
|
|
45
|
-
computeFromX(e.touches[0].clientX);
|
|
46
|
-
},
|
|
47
|
-
[computeFromX]
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const handleTouchMove = useCallback(
|
|
51
|
-
(e: React.TouchEvent) => {
|
|
52
|
-
if (!dragging.current) return;
|
|
53
|
-
e.preventDefault();
|
|
54
|
-
computeFromX(e.touches[0].clientX);
|
|
55
|
-
},
|
|
56
|
-
[computeFromX]
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
const handleMove = (e: MouseEvent) => {
|
|
61
|
-
if (!dragging.current) return;
|
|
62
|
-
computeFromX(e.clientX);
|
|
63
|
-
};
|
|
64
|
-
const handleUp = () => {
|
|
65
|
-
dragging.current = false;
|
|
66
|
-
};
|
|
67
|
-
window.addEventListener("mousemove", handleMove);
|
|
68
|
-
window.addEventListener("mouseup", handleUp);
|
|
69
|
-
window.addEventListener("touchend", handleUp);
|
|
70
|
-
return () => {
|
|
71
|
-
window.removeEventListener("mousemove", handleMove);
|
|
72
|
-
window.removeEventListener("mouseup", handleUp);
|
|
73
|
-
window.removeEventListener("touchend", handleUp);
|
|
74
|
-
};
|
|
75
|
-
}, [computeFromX]);
|
|
76
|
-
|
|
77
|
-
const handleKeyDown = useCallback(
|
|
78
|
-
(e: React.KeyboardEvent) => {
|
|
79
|
-
const step = e.shiftKey ? 0.1 : 0.01;
|
|
80
|
-
let newAlpha = alpha;
|
|
81
|
-
switch (e.key) {
|
|
82
|
-
case "ArrowRight":
|
|
83
|
-
newAlpha = clamp(alpha + step, 0, 1);
|
|
84
|
-
break;
|
|
85
|
-
case "ArrowLeft":
|
|
86
|
-
newAlpha = clamp(alpha - step, 0, 1);
|
|
87
|
-
break;
|
|
88
|
-
default:
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
onChange(Math.round(newAlpha * 100) / 100);
|
|
93
|
-
},
|
|
94
|
-
[alpha, onChange]
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div
|
|
99
|
-
ref={trackRef}
|
|
100
|
-
role="slider"
|
|
101
|
-
tabIndex={0}
|
|
102
|
-
aria-label="Color opacity"
|
|
103
|
-
aria-valuemin={0}
|
|
104
|
-
aria-valuemax={100}
|
|
105
|
-
aria-valuenow={Math.round(alpha * 100)}
|
|
106
|
-
aria-valuetext={`Opacity ${Math.round(alpha * 100)}%`}
|
|
107
|
-
className="w-full h-3.5 rounded-full cursor-pointer relative select-none overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#
|
|
108
|
-
style={{
|
|
109
|
-
backgroundImage:
|
|
110
|
-
"linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%), linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%)",
|
|
111
|
-
backgroundSize: "8px 8px",
|
|
112
|
-
backgroundPosition: "0 0, 4px 4px",
|
|
113
|
-
backgroundColor: "#f5f5f5",
|
|
114
|
-
}}
|
|
115
|
-
onMouseDown={handleMouseDown}
|
|
116
|
-
onTouchStart={handleTouchStart}
|
|
117
|
-
onTouchMove={handleTouchMove}
|
|
118
|
-
onKeyDown={handleKeyDown}
|
|
119
|
-
>
|
|
120
|
-
{/* Color gradient overlay */}
|
|
121
|
-
<div
|
|
122
|
-
className="absolute inset-0 rounded-full"
|
|
123
|
-
style={{
|
|
124
|
-
background: `linear-gradient(to right, transparent, ${color})`,
|
|
125
|
-
}}
|
|
126
|
-
/>
|
|
127
|
-
{/* Thumb */}
|
|
128
|
-
<div
|
|
129
|
-
className="absolute w-[18px] h-[18px] rounded-full border-[2.5px] border-white pointer-events-none"
|
|
130
|
-
style={{
|
|
131
|
-
left: `${alpha * 100}%`,
|
|
132
|
-
top: "50%",
|
|
133
|
-
transform: "translate(-50%, -50%)",
|
|
134
|
-
background: color,
|
|
135
|
-
opacity: alpha,
|
|
136
|
-
boxShadow: "0 1px 4px rgba(0,0,0,0.4)",
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AlphaSlider — Transparency slider with checkerboard pattern.
|
|
5
|
+
*
|
|
6
|
+
* Shows a gradient from transparent to the current color over a
|
|
7
|
+
* checker background. Mouse + touch + keyboard support.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useRef, useCallback, useEffect } from "react";
|
|
11
|
+
import type { AlphaSliderProps } from "./types";
|
|
12
|
+
import { clamp } from "./utils";
|
|
13
|
+
|
|
14
|
+
export default function AlphaSlider({
|
|
15
|
+
color,
|
|
16
|
+
alpha,
|
|
17
|
+
onChange,
|
|
18
|
+
}: AlphaSliderProps) {
|
|
19
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const dragging = useRef(false);
|
|
21
|
+
|
|
22
|
+
const computeFromX = useCallback(
|
|
23
|
+
(clientX: number) => {
|
|
24
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
25
|
+
if (!rect) return;
|
|
26
|
+
const x = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
27
|
+
onChange(Math.round(x * 100) / 100);
|
|
28
|
+
},
|
|
29
|
+
[onChange]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const handleMouseDown = useCallback(
|
|
33
|
+
(e: React.MouseEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
dragging.current = true;
|
|
36
|
+
computeFromX(e.clientX);
|
|
37
|
+
},
|
|
38
|
+
[computeFromX]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const handleTouchStart = useCallback(
|
|
42
|
+
(e: React.TouchEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
dragging.current = true;
|
|
45
|
+
computeFromX(e.touches[0].clientX);
|
|
46
|
+
},
|
|
47
|
+
[computeFromX]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleTouchMove = useCallback(
|
|
51
|
+
(e: React.TouchEvent) => {
|
|
52
|
+
if (!dragging.current) return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
computeFromX(e.touches[0].clientX);
|
|
55
|
+
},
|
|
56
|
+
[computeFromX]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleMove = (e: MouseEvent) => {
|
|
61
|
+
if (!dragging.current) return;
|
|
62
|
+
computeFromX(e.clientX);
|
|
63
|
+
};
|
|
64
|
+
const handleUp = () => {
|
|
65
|
+
dragging.current = false;
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener("mousemove", handleMove);
|
|
68
|
+
window.addEventListener("mouseup", handleUp);
|
|
69
|
+
window.addEventListener("touchend", handleUp);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("mousemove", handleMove);
|
|
72
|
+
window.removeEventListener("mouseup", handleUp);
|
|
73
|
+
window.removeEventListener("touchend", handleUp);
|
|
74
|
+
};
|
|
75
|
+
}, [computeFromX]);
|
|
76
|
+
|
|
77
|
+
const handleKeyDown = useCallback(
|
|
78
|
+
(e: React.KeyboardEvent) => {
|
|
79
|
+
const step = e.shiftKey ? 0.1 : 0.01;
|
|
80
|
+
let newAlpha = alpha;
|
|
81
|
+
switch (e.key) {
|
|
82
|
+
case "ArrowRight":
|
|
83
|
+
newAlpha = clamp(alpha + step, 0, 1);
|
|
84
|
+
break;
|
|
85
|
+
case "ArrowLeft":
|
|
86
|
+
newAlpha = clamp(alpha - step, 0, 1);
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
onChange(Math.round(newAlpha * 100) / 100);
|
|
93
|
+
},
|
|
94
|
+
[alpha, onChange]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
ref={trackRef}
|
|
100
|
+
role="slider"
|
|
101
|
+
tabIndex={0}
|
|
102
|
+
aria-label="Color opacity"
|
|
103
|
+
aria-valuemin={0}
|
|
104
|
+
aria-valuemax={100}
|
|
105
|
+
aria-valuenow={Math.round(alpha * 100)}
|
|
106
|
+
aria-valuetext={`Opacity ${Math.round(alpha * 100)}%`}
|
|
107
|
+
className="w-full h-3.5 rounded-full cursor-pointer relative select-none overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#3580f9]"
|
|
108
|
+
style={{
|
|
109
|
+
backgroundImage:
|
|
110
|
+
"linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%), linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%)",
|
|
111
|
+
backgroundSize: "8px 8px",
|
|
112
|
+
backgroundPosition: "0 0, 4px 4px",
|
|
113
|
+
backgroundColor: "#f5f5f5",
|
|
114
|
+
}}
|
|
115
|
+
onMouseDown={handleMouseDown}
|
|
116
|
+
onTouchStart={handleTouchStart}
|
|
117
|
+
onTouchMove={handleTouchMove}
|
|
118
|
+
onKeyDown={handleKeyDown}
|
|
119
|
+
>
|
|
120
|
+
{/* Color gradient overlay */}
|
|
121
|
+
<div
|
|
122
|
+
className="absolute inset-0 rounded-full"
|
|
123
|
+
style={{
|
|
124
|
+
background: `linear-gradient(to right, transparent, ${color})`,
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
{/* Thumb */}
|
|
128
|
+
<div
|
|
129
|
+
className="absolute w-[18px] h-[18px] rounded-full border-[2.5px] border-white pointer-events-none"
|
|
130
|
+
style={{
|
|
131
|
+
left: `${alpha * 100}%`,
|
|
132
|
+
top: "50%",
|
|
133
|
+
transform: "translate(-50%, -50%)",
|
|
134
|
+
background: color,
|
|
135
|
+
opacity: alpha,
|
|
136
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.4)",
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ColorInputs — Color preview + editable input + format toggle (HEX/RGB/HSL).
|
|
5
|
-
*
|
|
6
|
-
* Shows:
|
|
7
|
-
* - A square color preview with checker background (for alpha)
|
|
8
|
-
* - An editable text input whose format changes with the toggle
|
|
9
|
-
* - A format toggle button cycling HEX → RGB → HSL
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useState, useCallback, useEffect } from "react";
|
|
13
|
-
import type { ColorFormat, ColorInputsProps } from "./types";
|
|
14
|
-
import { formatColorValue, parseColorInput, isValidHex } from "./utils";
|
|
15
|
-
|
|
16
|
-
export default function ColorInputs({
|
|
17
|
-
hex,
|
|
18
|
-
onHexChange,
|
|
19
|
-
alpha = 1,
|
|
20
|
-
}: ColorInputsProps) {
|
|
21
|
-
const [format, setFormat] = useState<ColorFormat>("hex");
|
|
22
|
-
const [inputValue, setInputValue] = useState(
|
|
23
|
-
formatColorValue(hex, "hex")
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
// Sync input display when hex changes externally (e.g. from canvas drag)
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (isValidHex(hex)) {
|
|
29
|
-
setInputValue(formatColorValue(hex, format));
|
|
30
|
-
}
|
|
31
|
-
}, [hex, format]);
|
|
32
|
-
|
|
33
|
-
const handleFormatToggle = useCallback(() => {
|
|
34
|
-
const formats: ColorFormat[] = ["hex", "rgb", "hsl"];
|
|
35
|
-
const nextIndex = (formats.indexOf(format) + 1) % formats.length;
|
|
36
|
-
const nextFormat = formats[nextIndex];
|
|
37
|
-
setFormat(nextFormat);
|
|
38
|
-
if (isValidHex(hex)) {
|
|
39
|
-
setInputValue(formatColorValue(hex, nextFormat));
|
|
40
|
-
}
|
|
41
|
-
}, [format, hex]);
|
|
42
|
-
|
|
43
|
-
const handleInputChange = useCallback(
|
|
44
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
-
const raw = e.target.value;
|
|
46
|
-
setInputValue(raw);
|
|
47
|
-
const parsed = parseColorInput(raw, format);
|
|
48
|
-
if (parsed) {
|
|
49
|
-
onHexChange(parsed);
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
[format, onHexChange]
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const handleInputBlur = useCallback(() => {
|
|
56
|
-
// Reset to current valid value on blur
|
|
57
|
-
if (isValidHex(hex)) {
|
|
58
|
-
setInputValue(formatColorValue(hex, format));
|
|
59
|
-
}
|
|
60
|
-
}, [hex, format]);
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="flex items-center gap-2">
|
|
64
|
-
{/* Color preview square */}
|
|
65
|
-
<div
|
|
66
|
-
className="w-10 h-10 rounded-[10px] border border-neutral-200 shrink-0 relative overflow-hidden"
|
|
67
|
-
>
|
|
68
|
-
{/* Checker background for alpha visibility */}
|
|
69
|
-
<div
|
|
70
|
-
className="absolute inset-0"
|
|
71
|
-
style={{
|
|
72
|
-
backgroundImage:
|
|
73
|
-
"linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%), linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%)",
|
|
74
|
-
backgroundSize: "8px 8px",
|
|
75
|
-
backgroundPosition: "0 0, 4px 4px",
|
|
76
|
-
backgroundColor: "#f5f5f5",
|
|
77
|
-
}}
|
|
78
|
-
/>
|
|
79
|
-
{/* Color fill */}
|
|
80
|
-
<div
|
|
81
|
-
className="absolute inset-0"
|
|
82
|
-
style={{ background: hex, opacity: alpha }}
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
{/* Editable input */}
|
|
87
|
-
<input
|
|
88
|
-
value={inputValue}
|
|
89
|
-
onChange={handleInputChange}
|
|
90
|
-
onBlur={handleInputBlur}
|
|
91
|
-
spellCheck={false}
|
|
92
|
-
className="flex-1 bg-neutral-50 border border-neutral-200 rounded-[10px] px-3 py-2.5 text-neutral-900 text-sm font-mono outline-none focus:border-[#
|
|
93
|
-
/>
|
|
94
|
-
|
|
95
|
-
{/* Format toggle */}
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
onClick={handleFormatToggle}
|
|
99
|
-
className="bg-neutral-50 border border-neutral-200 rounded-[10px] px-3 py-2.5 text-neutral-400 text-[11px] uppercase tracking-wide cursor-pointer hover:text-neutral-600 hover:border-neutral-300 transition-colors font-sans"
|
|
100
|
-
>
|
|
101
|
-
{format.toUpperCase()}
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColorInputs — Color preview + editable input + format toggle (HEX/RGB/HSL).
|
|
5
|
+
*
|
|
6
|
+
* Shows:
|
|
7
|
+
* - A square color preview with checker background (for alpha)
|
|
8
|
+
* - An editable text input whose format changes with the toggle
|
|
9
|
+
* - A format toggle button cycling HEX → RGB → HSL
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useEffect } from "react";
|
|
13
|
+
import type { ColorFormat, ColorInputsProps } from "./types";
|
|
14
|
+
import { formatColorValue, parseColorInput, isValidHex } from "./utils";
|
|
15
|
+
|
|
16
|
+
export default function ColorInputs({
|
|
17
|
+
hex,
|
|
18
|
+
onHexChange,
|
|
19
|
+
alpha = 1,
|
|
20
|
+
}: ColorInputsProps) {
|
|
21
|
+
const [format, setFormat] = useState<ColorFormat>("hex");
|
|
22
|
+
const [inputValue, setInputValue] = useState(
|
|
23
|
+
formatColorValue(hex, "hex")
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Sync input display when hex changes externally (e.g. from canvas drag)
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (isValidHex(hex)) {
|
|
29
|
+
setInputValue(formatColorValue(hex, format));
|
|
30
|
+
}
|
|
31
|
+
}, [hex, format]);
|
|
32
|
+
|
|
33
|
+
const handleFormatToggle = useCallback(() => {
|
|
34
|
+
const formats: ColorFormat[] = ["hex", "rgb", "hsl"];
|
|
35
|
+
const nextIndex = (formats.indexOf(format) + 1) % formats.length;
|
|
36
|
+
const nextFormat = formats[nextIndex];
|
|
37
|
+
setFormat(nextFormat);
|
|
38
|
+
if (isValidHex(hex)) {
|
|
39
|
+
setInputValue(formatColorValue(hex, nextFormat));
|
|
40
|
+
}
|
|
41
|
+
}, [format, hex]);
|
|
42
|
+
|
|
43
|
+
const handleInputChange = useCallback(
|
|
44
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
const raw = e.target.value;
|
|
46
|
+
setInputValue(raw);
|
|
47
|
+
const parsed = parseColorInput(raw, format);
|
|
48
|
+
if (parsed) {
|
|
49
|
+
onHexChange(parsed);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[format, onHexChange]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const handleInputBlur = useCallback(() => {
|
|
56
|
+
// Reset to current valid value on blur
|
|
57
|
+
if (isValidHex(hex)) {
|
|
58
|
+
setInputValue(formatColorValue(hex, format));
|
|
59
|
+
}
|
|
60
|
+
}, [hex, format]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
{/* Color preview square */}
|
|
65
|
+
<div
|
|
66
|
+
className="w-10 h-10 rounded-[10px] border border-neutral-200 shrink-0 relative overflow-hidden"
|
|
67
|
+
>
|
|
68
|
+
{/* Checker background for alpha visibility */}
|
|
69
|
+
<div
|
|
70
|
+
className="absolute inset-0"
|
|
71
|
+
style={{
|
|
72
|
+
backgroundImage:
|
|
73
|
+
"linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%), linear-gradient(45deg, #d4d4d4 25%, transparent 25%, transparent 75%, #d4d4d4 75%)",
|
|
74
|
+
backgroundSize: "8px 8px",
|
|
75
|
+
backgroundPosition: "0 0, 4px 4px",
|
|
76
|
+
backgroundColor: "#f5f5f5",
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
{/* Color fill */}
|
|
80
|
+
<div
|
|
81
|
+
className="absolute inset-0"
|
|
82
|
+
style={{ background: hex, opacity: alpha }}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Editable input */}
|
|
87
|
+
<input
|
|
88
|
+
value={inputValue}
|
|
89
|
+
onChange={handleInputChange}
|
|
90
|
+
onBlur={handleInputBlur}
|
|
91
|
+
spellCheck={false}
|
|
92
|
+
className="flex-1 bg-neutral-50 border border-neutral-200 rounded-[10px] px-3 py-2.5 text-neutral-900 text-sm font-mono outline-none focus:border-[#3580f9] focus:ring-2 focus:ring-[#3580f9]/10 transition-colors"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
{/* Format toggle */}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={handleFormatToggle}
|
|
99
|
+
className="bg-neutral-50 border border-neutral-200 rounded-[10px] px-3 py-2.5 text-neutral-400 text-[11px] uppercase tracking-wide cursor-pointer hover:text-neutral-600 hover:border-neutral-300 transition-colors font-sans"
|
|
100
|
+
>
|
|
101
|
+
{format.toUpperCase()}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* EyedropperButton — Uses the browser's EyeDropper API to pick a color
|
|
5
|
-
* from anywhere on screen. Gracefully disabled in unsupported browsers.
|
|
6
|
-
*
|
|
7
|
-
* Supported: Chrome, Edge, Arc, Brave (Chromium-based).
|
|
8
|
-
* Not supported: Firefox, Safari.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { useState, useCallback } from "react";
|
|
12
|
-
import type { EyedropperButtonProps } from "./types";
|
|
13
|
-
|
|
14
|
-
/** Check if the EyeDropper API is available */
|
|
15
|
-
function isEyeDropperSupported(): boolean {
|
|
16
|
-
return typeof window !== "undefined" && "EyeDropper" in window;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default function EyedropperButton({
|
|
20
|
-
onColorPicked,
|
|
21
|
-
}: EyedropperButtonProps) {
|
|
22
|
-
const supported = isEyeDropperSupported();
|
|
23
|
-
const [picking, setPicking] = useState(false);
|
|
24
|
-
|
|
25
|
-
const handleClick = useCallback(async () => {
|
|
26
|
-
if (!supported || picking) return;
|
|
27
|
-
setPicking(true);
|
|
28
|
-
try {
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
-
const dropper = new (window as any).EyeDropper();
|
|
31
|
-
const result = await dropper.open();
|
|
32
|
-
if (result?.sRGBHex) {
|
|
33
|
-
onColorPicked(result.sRGBHex.toLowerCase());
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
// User cancelled or API error — silently ignore
|
|
37
|
-
} finally {
|
|
38
|
-
setPicking(false);
|
|
39
|
-
}
|
|
40
|
-
}, [supported, picking, onColorPicked]);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<button
|
|
44
|
-
type="button"
|
|
45
|
-
onClick={handleClick}
|
|
46
|
-
disabled={!supported}
|
|
47
|
-
title={
|
|
48
|
-
supported
|
|
49
|
-
? "Pick a color from screen"
|
|
50
|
-
: "Eyedropper not supported in this browser"
|
|
51
|
-
}
|
|
52
|
-
className={`w-10 h-10 rounded-[10px] border shrink-0 flex items-center justify-center transition-colors ${
|
|
53
|
-
supported
|
|
54
|
-
? "border-neutral-200 bg-neutral-50 text-neutral-500 cursor-pointer hover:border-neutral-300 hover:text-neutral-700"
|
|
55
|
-
: "border-neutral-100 bg-neutral-50 text-neutral-300 cursor-not-allowed"
|
|
56
|
-
} ${picking ? "ring-2 ring-[#
|
|
57
|
-
>
|
|
58
|
-
<svg
|
|
59
|
-
width="18"
|
|
60
|
-
height="18"
|
|
61
|
-
viewBox="0 0 24 24"
|
|
62
|
-
fill="none"
|
|
63
|
-
stroke="currentColor"
|
|
64
|
-
strokeWidth="2"
|
|
65
|
-
strokeLinecap="round"
|
|
66
|
-
strokeLinejoin="round"
|
|
67
|
-
>
|
|
68
|
-
<path d="m2 22 1-1h3l9-9" />
|
|
69
|
-
<path d="M3 21v-3l9-9" />
|
|
70
|
-
<path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3L15 6" />
|
|
71
|
-
</svg>
|
|
72
|
-
</button>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EyedropperButton — Uses the browser's EyeDropper API to pick a color
|
|
5
|
+
* from anywhere on screen. Gracefully disabled in unsupported browsers.
|
|
6
|
+
*
|
|
7
|
+
* Supported: Chrome, Edge, Arc, Brave (Chromium-based).
|
|
8
|
+
* Not supported: Firefox, Safari.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useCallback } from "react";
|
|
12
|
+
import type { EyedropperButtonProps } from "./types";
|
|
13
|
+
|
|
14
|
+
/** Check if the EyeDropper API is available */
|
|
15
|
+
function isEyeDropperSupported(): boolean {
|
|
16
|
+
return typeof window !== "undefined" && "EyeDropper" in window;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function EyedropperButton({
|
|
20
|
+
onColorPicked,
|
|
21
|
+
}: EyedropperButtonProps) {
|
|
22
|
+
const supported = isEyeDropperSupported();
|
|
23
|
+
const [picking, setPicking] = useState(false);
|
|
24
|
+
|
|
25
|
+
const handleClick = useCallback(async () => {
|
|
26
|
+
if (!supported || picking) return;
|
|
27
|
+
setPicking(true);
|
|
28
|
+
try {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const dropper = new (window as any).EyeDropper();
|
|
31
|
+
const result = await dropper.open();
|
|
32
|
+
if (result?.sRGBHex) {
|
|
33
|
+
onColorPicked(result.sRGBHex.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// User cancelled or API error — silently ignore
|
|
37
|
+
} finally {
|
|
38
|
+
setPicking(false);
|
|
39
|
+
}
|
|
40
|
+
}, [supported, picking, onColorPicked]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={handleClick}
|
|
46
|
+
disabled={!supported}
|
|
47
|
+
title={
|
|
48
|
+
supported
|
|
49
|
+
? "Pick a color from screen"
|
|
50
|
+
: "Eyedropper not supported in this browser"
|
|
51
|
+
}
|
|
52
|
+
className={`w-10 h-10 rounded-[10px] border shrink-0 flex items-center justify-center transition-colors ${
|
|
53
|
+
supported
|
|
54
|
+
? "border-neutral-200 bg-neutral-50 text-neutral-500 cursor-pointer hover:border-neutral-300 hover:text-neutral-700"
|
|
55
|
+
: "border-neutral-100 bg-neutral-50 text-neutral-300 cursor-not-allowed"
|
|
56
|
+
} ${picking ? "ring-2 ring-[#3580f9]/30" : ""}`}
|
|
57
|
+
>
|
|
58
|
+
<svg
|
|
59
|
+
width="18"
|
|
60
|
+
height="18"
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="2"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
>
|
|
68
|
+
<path d="m2 22 1-1h3l9-9" />
|
|
69
|
+
<path d="M3 21v-3l9-9" />
|
|
70
|
+
<path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3L15 6" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|