@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,124 +1,124 @@
1
- "use client";
2
-
3
- /**
4
- * HueSlider — Horizontal rainbow hue selector (0–360).
5
- *
6
- * Mouse + touch + keyboard support.
7
- */
8
-
9
- import { useRef, useCallback, useEffect } from "react";
10
- import type { HueSliderProps } from "./types";
11
- import { clamp } from "./utils";
12
-
13
- export default function HueSlider({ hue, onChange }: HueSliderProps) {
14
- const trackRef = useRef<HTMLDivElement>(null);
15
- const dragging = useRef(false);
16
-
17
- const computeFromX = useCallback(
18
- (clientX: number) => {
19
- const rect = trackRef.current?.getBoundingClientRect();
20
- if (!rect) return;
21
- const x = clamp((clientX - rect.left) / rect.width, 0, 1);
22
- onChange(Math.round(x * 360));
23
- },
24
- [onChange]
25
- );
26
-
27
- const handleMouseDown = useCallback(
28
- (e: React.MouseEvent) => {
29
- e.preventDefault();
30
- dragging.current = true;
31
- computeFromX(e.clientX);
32
- },
33
- [computeFromX]
34
- );
35
-
36
- const handleTouchStart = useCallback(
37
- (e: React.TouchEvent) => {
38
- e.preventDefault();
39
- dragging.current = true;
40
- computeFromX(e.touches[0].clientX);
41
- },
42
- [computeFromX]
43
- );
44
-
45
- const handleTouchMove = useCallback(
46
- (e: React.TouchEvent) => {
47
- if (!dragging.current) return;
48
- e.preventDefault();
49
- computeFromX(e.touches[0].clientX);
50
- },
51
- [computeFromX]
52
- );
53
-
54
- useEffect(() => {
55
- const handleMove = (e: MouseEvent) => {
56
- if (!dragging.current) return;
57
- computeFromX(e.clientX);
58
- };
59
- const handleUp = () => {
60
- dragging.current = false;
61
- };
62
- window.addEventListener("mousemove", handleMove);
63
- window.addEventListener("mouseup", handleUp);
64
- window.addEventListener("touchend", handleUp);
65
- return () => {
66
- window.removeEventListener("mousemove", handleMove);
67
- window.removeEventListener("mouseup", handleUp);
68
- window.removeEventListener("touchend", handleUp);
69
- };
70
- }, [computeFromX]);
71
-
72
- const handleKeyDown = useCallback(
73
- (e: React.KeyboardEvent) => {
74
- const step = e.shiftKey ? 20 : 5;
75
- let newHue = hue;
76
- switch (e.key) {
77
- case "ArrowRight":
78
- newHue = clamp(hue + step, 0, 360);
79
- break;
80
- case "ArrowLeft":
81
- newHue = clamp(hue - step, 0, 360);
82
- break;
83
- default:
84
- return;
85
- }
86
- e.preventDefault();
87
- onChange(newHue);
88
- },
89
- [hue, onChange]
90
- );
91
-
92
- return (
93
- <div
94
- ref={trackRef}
95
- role="slider"
96
- tabIndex={0}
97
- aria-label="Color hue"
98
- aria-valuemin={0}
99
- aria-valuemax={360}
100
- aria-valuenow={hue}
101
- aria-valuetext={`Hue ${hue} degrees`}
102
- className="w-full h-3.5 rounded-full cursor-pointer relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
103
- style={{
104
- background:
105
- "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
106
- }}
107
- onMouseDown={handleMouseDown}
108
- onTouchStart={handleTouchStart}
109
- onTouchMove={handleTouchMove}
110
- onKeyDown={handleKeyDown}
111
- >
112
- <div
113
- className="absolute w-[18px] h-[18px] rounded-full border-[2.5px] border-white pointer-events-none"
114
- style={{
115
- left: `${(hue / 360) * 100}%`,
116
- top: "50%",
117
- transform: "translate(-50%, -50%)",
118
- background: `hsl(${hue}, 100%, 50%)`,
119
- boxShadow: "0 1px 4px rgba(0,0,0,0.4)",
120
- }}
121
- />
122
- </div>
123
- );
124
- }
1
+ "use client";
2
+
3
+ /**
4
+ * HueSlider — Horizontal rainbow hue selector (0–360).
5
+ *
6
+ * Mouse + touch + keyboard support.
7
+ */
8
+
9
+ import { useRef, useCallback, useEffect } from "react";
10
+ import type { HueSliderProps } from "./types";
11
+ import { clamp } from "./utils";
12
+
13
+ export default function HueSlider({ hue, onChange }: HueSliderProps) {
14
+ const trackRef = useRef<HTMLDivElement>(null);
15
+ const dragging = useRef(false);
16
+
17
+ const computeFromX = useCallback(
18
+ (clientX: number) => {
19
+ const rect = trackRef.current?.getBoundingClientRect();
20
+ if (!rect) return;
21
+ const x = clamp((clientX - rect.left) / rect.width, 0, 1);
22
+ onChange(Math.round(x * 360));
23
+ },
24
+ [onChange]
25
+ );
26
+
27
+ const handleMouseDown = useCallback(
28
+ (e: React.MouseEvent) => {
29
+ e.preventDefault();
30
+ dragging.current = true;
31
+ computeFromX(e.clientX);
32
+ },
33
+ [computeFromX]
34
+ );
35
+
36
+ const handleTouchStart = useCallback(
37
+ (e: React.TouchEvent) => {
38
+ e.preventDefault();
39
+ dragging.current = true;
40
+ computeFromX(e.touches[0].clientX);
41
+ },
42
+ [computeFromX]
43
+ );
44
+
45
+ const handleTouchMove = useCallback(
46
+ (e: React.TouchEvent) => {
47
+ if (!dragging.current) return;
48
+ e.preventDefault();
49
+ computeFromX(e.touches[0].clientX);
50
+ },
51
+ [computeFromX]
52
+ );
53
+
54
+ useEffect(() => {
55
+ const handleMove = (e: MouseEvent) => {
56
+ if (!dragging.current) return;
57
+ computeFromX(e.clientX);
58
+ };
59
+ const handleUp = () => {
60
+ dragging.current = false;
61
+ };
62
+ window.addEventListener("mousemove", handleMove);
63
+ window.addEventListener("mouseup", handleUp);
64
+ window.addEventListener("touchend", handleUp);
65
+ return () => {
66
+ window.removeEventListener("mousemove", handleMove);
67
+ window.removeEventListener("mouseup", handleUp);
68
+ window.removeEventListener("touchend", handleUp);
69
+ };
70
+ }, [computeFromX]);
71
+
72
+ const handleKeyDown = useCallback(
73
+ (e: React.KeyboardEvent) => {
74
+ const step = e.shiftKey ? 20 : 5;
75
+ let newHue = hue;
76
+ switch (e.key) {
77
+ case "ArrowRight":
78
+ newHue = clamp(hue + step, 0, 360);
79
+ break;
80
+ case "ArrowLeft":
81
+ newHue = clamp(hue - step, 0, 360);
82
+ break;
83
+ default:
84
+ return;
85
+ }
86
+ e.preventDefault();
87
+ onChange(newHue);
88
+ },
89
+ [hue, onChange]
90
+ );
91
+
92
+ return (
93
+ <div
94
+ ref={trackRef}
95
+ role="slider"
96
+ tabIndex={0}
97
+ aria-label="Color hue"
98
+ aria-valuemin={0}
99
+ aria-valuemax={360}
100
+ aria-valuenow={hue}
101
+ aria-valuetext={`Hue ${hue} degrees`}
102
+ className="w-full h-3.5 rounded-full cursor-pointer relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#3580f9]"
103
+ style={{
104
+ background:
105
+ "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
106
+ }}
107
+ onMouseDown={handleMouseDown}
108
+ onTouchStart={handleTouchStart}
109
+ onTouchMove={handleTouchMove}
110
+ onKeyDown={handleKeyDown}
111
+ >
112
+ <div
113
+ className="absolute w-[18px] h-[18px] rounded-full border-[2.5px] border-white pointer-events-none"
114
+ style={{
115
+ left: `${(hue / 360) * 100}%`,
116
+ top: "50%",
117
+ transform: "translate(-50%, -50%)",
118
+ background: `hsl(${hue}, 100%, 50%)`,
119
+ boxShadow: "0 1px 4px rgba(0,0,0,0.4)",
120
+ }}
121
+ />
122
+ </div>
123
+ );
124
+ }
@@ -1,142 +1,142 @@
1
- "use client";
2
-
3
- /**
4
- * SaturationCanvas — HSV saturation/brightness 2D canvas.
5
- *
6
- * Mouse + touch + keyboard support.
7
- * Renders a hue-tinted gradient with white→color horizontally
8
- * and transparent→black vertically.
9
- */
10
-
11
- import { useRef, useCallback, useEffect } from "react";
12
- import type { SaturationCanvasProps } from "./types";
13
- import { clamp } from "./utils";
14
-
15
- export default function SaturationCanvas({
16
- hue,
17
- saturation,
18
- value,
19
- onChange,
20
- }: SaturationCanvasProps) {
21
- const canvasRef = useRef<HTMLDivElement>(null);
22
- const dragging = useRef(false);
23
-
24
- const computeFromEvent = useCallback(
25
- (clientX: number, clientY: number) => {
26
- const rect = canvasRef.current?.getBoundingClientRect();
27
- if (!rect) return;
28
- const x = clamp((clientX - rect.left) / rect.width, 0, 1);
29
- const y = clamp((clientY - rect.top) / rect.height, 0, 1);
30
- onChange(Math.round(x * 100), Math.round((1 - y) * 100));
31
- },
32
- [onChange]
33
- );
34
-
35
- // Mouse events
36
- const handleMouseDown = useCallback(
37
- (e: React.MouseEvent) => {
38
- e.preventDefault();
39
- dragging.current = true;
40
- computeFromEvent(e.clientX, e.clientY);
41
- },
42
- [computeFromEvent]
43
- );
44
-
45
- // Touch events
46
- const handleTouchStart = useCallback(
47
- (e: React.TouchEvent) => {
48
- e.preventDefault();
49
- dragging.current = true;
50
- const t = e.touches[0];
51
- computeFromEvent(t.clientX, t.clientY);
52
- },
53
- [computeFromEvent]
54
- );
55
-
56
- const handleTouchMove = useCallback(
57
- (e: React.TouchEvent) => {
58
- if (!dragging.current) return;
59
- e.preventDefault();
60
- const t = e.touches[0];
61
- computeFromEvent(t.clientX, t.clientY);
62
- },
63
- [computeFromEvent]
64
- );
65
-
66
- // Global mouse move/up
67
- useEffect(() => {
68
- const handleMove = (e: MouseEvent) => {
69
- if (!dragging.current) return;
70
- computeFromEvent(e.clientX, e.clientY);
71
- };
72
- const handleUp = () => {
73
- dragging.current = false;
74
- };
75
- window.addEventListener("mousemove", handleMove);
76
- window.addEventListener("mouseup", handleUp);
77
- window.addEventListener("touchend", handleUp);
78
- return () => {
79
- window.removeEventListener("mousemove", handleMove);
80
- window.removeEventListener("mouseup", handleUp);
81
- window.removeEventListener("touchend", handleUp);
82
- };
83
- }, [computeFromEvent]);
84
-
85
- // Keyboard
86
- const handleKeyDown = useCallback(
87
- (e: React.KeyboardEvent) => {
88
- const step = e.shiftKey ? 10 : 2;
89
- let newSat = saturation;
90
- let newVal = value;
91
- switch (e.key) {
92
- case "ArrowRight":
93
- newSat = clamp(saturation + step, 0, 100);
94
- break;
95
- case "ArrowLeft":
96
- newSat = clamp(saturation - step, 0, 100);
97
- break;
98
- case "ArrowUp":
99
- newVal = clamp(value + step, 0, 100);
100
- break;
101
- case "ArrowDown":
102
- newVal = clamp(value - step, 0, 100);
103
- break;
104
- default:
105
- return;
106
- }
107
- e.preventDefault();
108
- onChange(newSat, newVal);
109
- },
110
- [saturation, value, onChange]
111
- );
112
-
113
- return (
114
- <div
115
- ref={canvasRef}
116
- role="slider"
117
- tabIndex={0}
118
- aria-label="Color saturation and brightness"
119
- aria-valuetext={`Saturation ${saturation}%, Brightness ${value}%`}
120
- className="w-full h-[220px] rounded-xl cursor-crosshair relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
121
- style={{
122
- background: `linear-gradient(to bottom, transparent, #000), linear-gradient(to right, #fff, hsl(${hue}, 100%, 50%))`,
123
- }}
124
- onMouseDown={handleMouseDown}
125
- onTouchStart={handleTouchStart}
126
- onTouchMove={handleTouchMove}
127
- onKeyDown={handleKeyDown}
128
- >
129
- {/* Cursor indicator */}
130
- <div
131
- className="absolute w-5 h-5 rounded-full border-[2.5px] border-white pointer-events-none"
132
- style={{
133
- left: `${saturation}%`,
134
- top: `${100 - value}%`,
135
- transform: "translate(-50%, -50%)",
136
- boxShadow:
137
- "0 0 0 1px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4)",
138
- }}
139
- />
140
- </div>
141
- );
142
- }
1
+ "use client";
2
+
3
+ /**
4
+ * SaturationCanvas — HSV saturation/brightness 2D canvas.
5
+ *
6
+ * Mouse + touch + keyboard support.
7
+ * Renders a hue-tinted gradient with white→color horizontally
8
+ * and transparent→black vertically.
9
+ */
10
+
11
+ import { useRef, useCallback, useEffect } from "react";
12
+ import type { SaturationCanvasProps } from "./types";
13
+ import { clamp } from "./utils";
14
+
15
+ export default function SaturationCanvas({
16
+ hue,
17
+ saturation,
18
+ value,
19
+ onChange,
20
+ }: SaturationCanvasProps) {
21
+ const canvasRef = useRef<HTMLDivElement>(null);
22
+ const dragging = useRef(false);
23
+
24
+ const computeFromEvent = useCallback(
25
+ (clientX: number, clientY: number) => {
26
+ const rect = canvasRef.current?.getBoundingClientRect();
27
+ if (!rect) return;
28
+ const x = clamp((clientX - rect.left) / rect.width, 0, 1);
29
+ const y = clamp((clientY - rect.top) / rect.height, 0, 1);
30
+ onChange(Math.round(x * 100), Math.round((1 - y) * 100));
31
+ },
32
+ [onChange]
33
+ );
34
+
35
+ // Mouse events
36
+ const handleMouseDown = useCallback(
37
+ (e: React.MouseEvent) => {
38
+ e.preventDefault();
39
+ dragging.current = true;
40
+ computeFromEvent(e.clientX, e.clientY);
41
+ },
42
+ [computeFromEvent]
43
+ );
44
+
45
+ // Touch events
46
+ const handleTouchStart = useCallback(
47
+ (e: React.TouchEvent) => {
48
+ e.preventDefault();
49
+ dragging.current = true;
50
+ const t = e.touches[0];
51
+ computeFromEvent(t.clientX, t.clientY);
52
+ },
53
+ [computeFromEvent]
54
+ );
55
+
56
+ const handleTouchMove = useCallback(
57
+ (e: React.TouchEvent) => {
58
+ if (!dragging.current) return;
59
+ e.preventDefault();
60
+ const t = e.touches[0];
61
+ computeFromEvent(t.clientX, t.clientY);
62
+ },
63
+ [computeFromEvent]
64
+ );
65
+
66
+ // Global mouse move/up
67
+ useEffect(() => {
68
+ const handleMove = (e: MouseEvent) => {
69
+ if (!dragging.current) return;
70
+ computeFromEvent(e.clientX, e.clientY);
71
+ };
72
+ const handleUp = () => {
73
+ dragging.current = false;
74
+ };
75
+ window.addEventListener("mousemove", handleMove);
76
+ window.addEventListener("mouseup", handleUp);
77
+ window.addEventListener("touchend", handleUp);
78
+ return () => {
79
+ window.removeEventListener("mousemove", handleMove);
80
+ window.removeEventListener("mouseup", handleUp);
81
+ window.removeEventListener("touchend", handleUp);
82
+ };
83
+ }, [computeFromEvent]);
84
+
85
+ // Keyboard
86
+ const handleKeyDown = useCallback(
87
+ (e: React.KeyboardEvent) => {
88
+ const step = e.shiftKey ? 10 : 2;
89
+ let newSat = saturation;
90
+ let newVal = value;
91
+ switch (e.key) {
92
+ case "ArrowRight":
93
+ newSat = clamp(saturation + step, 0, 100);
94
+ break;
95
+ case "ArrowLeft":
96
+ newSat = clamp(saturation - step, 0, 100);
97
+ break;
98
+ case "ArrowUp":
99
+ newVal = clamp(value + step, 0, 100);
100
+ break;
101
+ case "ArrowDown":
102
+ newVal = clamp(value - step, 0, 100);
103
+ break;
104
+ default:
105
+ return;
106
+ }
107
+ e.preventDefault();
108
+ onChange(newSat, newVal);
109
+ },
110
+ [saturation, value, onChange]
111
+ );
112
+
113
+ return (
114
+ <div
115
+ ref={canvasRef}
116
+ role="slider"
117
+ tabIndex={0}
118
+ aria-label="Color saturation and brightness"
119
+ aria-valuetext={`Saturation ${saturation}%, Brightness ${value}%`}
120
+ className="w-full h-[220px] rounded-xl cursor-crosshair relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#3580f9]"
121
+ style={{
122
+ background: `linear-gradient(to bottom, transparent, #000), linear-gradient(to right, #fff, hsl(${hue}, 100%, 50%))`,
123
+ }}
124
+ onMouseDown={handleMouseDown}
125
+ onTouchStart={handleTouchStart}
126
+ onTouchMove={handleTouchMove}
127
+ onKeyDown={handleKeyDown}
128
+ >
129
+ {/* Cursor indicator */}
130
+ <div
131
+ className="absolute w-5 h-5 rounded-full border-[2.5px] border-white pointer-events-none"
132
+ style={{
133
+ left: `${saturation}%`,
134
+ top: `${100 - value}%`,
135
+ transform: "translate(-50%, -50%)",
136
+ boxShadow:
137
+ "0 0 0 1px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4)",
138
+ }}
139
+ />
140
+ </div>
141
+ );
142
+ }