@morphika/andami 0.5.1 → 0.5.3

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 (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. 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,75 @@
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
+ import { BubbleTooltip } from "../BubbleIcons";
14
+
15
+ /** Check if the EyeDropper API is available */
16
+ function isEyeDropperSupported(): boolean {
17
+ return typeof window !== "undefined" && "EyeDropper" in window;
18
+ }
19
+
20
+ export default function EyedropperButton({
21
+ onColorPicked,
22
+ }: EyedropperButtonProps) {
23
+ const supported = isEyeDropperSupported();
24
+ const [picking, setPicking] = useState(false);
25
+
26
+ const handleClick = useCallback(async () => {
27
+ if (!supported || picking) return;
28
+ setPicking(true);
29
+ try {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ const dropper = new (window as any).EyeDropper();
32
+ const result = await dropper.open();
33
+ if (result?.sRGBHex) {
34
+ onColorPicked(result.sRGBHex.toLowerCase());
35
+ }
36
+ } catch {
37
+ // User cancelled or API error — silently ignore
38
+ } finally {
39
+ setPicking(false);
40
+ }
41
+ }, [supported, picking, onColorPicked]);
42
+
43
+ const label = supported
44
+ ? "Pick a color from screen"
45
+ : "Eyedropper not supported in this browser";
46
+ return (
47
+ <button
48
+ type="button"
49
+ onClick={handleClick}
50
+ disabled={!supported}
51
+ aria-label={label}
52
+ className={`group/bb relative 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
+ <BubbleTooltip>{label}</BubbleTooltip>
73
+ </button>
74
+ );
75
+ }