@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.
Files changed (117) hide show
  1. package/app/admin/assets/page.tsx +6 -6
  2. package/app/admin/database/page.tsx +302 -302
  3. package/app/admin/error.tsx +53 -53
  4. package/app/admin/layout.tsx +320 -320
  5. package/app/admin/navigation/page.tsx +255 -255
  6. package/app/admin/pages/[slug]/page.tsx +6 -6
  7. package/app/admin/pages/page.tsx +11 -11
  8. package/app/admin/projects/page.tsx +14 -14
  9. package/app/admin/setup/page.tsx +1 -1
  10. package/app/admin/styles/page.tsx +1 -1
  11. package/components/admin/MetadataEditor.tsx +6 -6
  12. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  13. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  14. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  15. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  16. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  17. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  18. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  19. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  20. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  22. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  23. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  24. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  25. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  26. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  27. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  28. package/components/admin/styles/ColorsEditor.tsx +2 -2
  29. package/components/admin/styles/FontsEditor.tsx +6 -6
  30. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  31. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  32. package/components/admin/styles/TypographyEditor.tsx +6 -6
  33. package/components/admin/styles/shared.tsx +68 -68
  34. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  35. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  36. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  37. package/components/builder/BlockCardIcons.tsx +316 -316
  38. package/components/builder/BlockTypePicker.tsx +1 -1
  39. package/components/builder/BubbleIcons.tsx +90 -0
  40. package/components/builder/BuilderCanvas.tsx +2 -0
  41. package/components/builder/CanvasMinimap.tsx +2 -2
  42. package/components/builder/CoverSectionCanvas.tsx +363 -363
  43. package/components/builder/DeviceFrame.tsx +1 -1
  44. package/components/builder/DndWrapper.tsx +3 -3
  45. package/components/builder/InsertionLines.tsx +1 -1
  46. package/components/builder/SectionCardIcons.tsx +421 -320
  47. package/components/builder/SectionEditorBar.tsx +1 -1
  48. package/components/builder/SectionTypePicker.tsx +4 -4
  49. package/components/builder/SectionV2Canvas.tsx +1 -1
  50. package/components/builder/SectionV2Column.tsx +69 -67
  51. package/components/builder/SortableBlock.tsx +93 -73
  52. package/components/builder/SortableRow.tsx +27 -26
  53. package/components/builder/VirtualAssetGrid.tsx +2 -2
  54. package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
  55. package/components/builder/blockStyles.tsx +192 -185
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  57. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  58. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  59. package/components/builder/color-picker/HueSlider.tsx +124 -124
  60. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  61. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  62. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  63. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  64. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  65. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  66. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  67. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  68. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  69. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  70. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  71. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  72. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  73. package/components/builder/editors/StaggerSettings.tsx +109 -109
  74. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  75. package/components/builder/editors/TextStylePicker.tsx +1 -1
  76. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  77. package/components/builder/editors/index.ts +11 -10
  78. package/components/builder/editors/shared.tsx +6 -6
  79. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  80. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  81. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  82. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  83. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  84. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  85. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  86. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  87. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  88. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  89. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  90. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  91. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  92. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  93. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  94. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  95. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  96. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  97. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  98. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  99. package/lib/animation/enter-types.ts +1 -0
  100. package/lib/animation/hover-effect-presets.ts +210 -210
  101. package/lib/animation/hover-effect-types.ts +1 -0
  102. package/lib/builder/block-registrations.ts +468 -417
  103. package/lib/builder/constants.ts +111 -111
  104. package/lib/builder/store-sections.ts +2 -2
  105. package/lib/builder/types-slices.ts +414 -414
  106. package/lib/builder/types.ts +4 -1
  107. package/lib/config/index.ts +27 -27
  108. package/lib/sanity/types.ts +98 -1
  109. package/lib/version.ts +1 -1
  110. package/package.json +1 -1
  111. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  112. package/sanity/schemas/blocks/index.ts +12 -11
  113. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  114. package/sanity/schemas/index.ts +120 -117
  115. package/styles/admin.css +85 -85
  116. package/styles/animations.css +237 -237
  117. 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-[#076bff]"
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-[#076bff] focus:ring-2 focus:ring-[#076bff]/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
+ "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-[#076bff]/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
- }
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
+ }