Skip to content

Commit d51c6c8

Browse files
committed
feat: implement color control focus feature with context and highlight animation
- Added SectionContext to manage expanded state for ControlSection components. - Enhanced ColorPicker to support programmatic focus and highlight animations. - Introduced useColorControlFocus hook for managing color control references. - Updated ColorPreview to utilize new focus functionality for color items. - Added name prop to ColorPicker for identifying color controls in the focus store.
1 parent b51c54d commit d51c6c8

File tree

9 files changed

+414
-66
lines changed

9 files changed

+414
-66
lines changed

components/editor/color-picker.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
import React, { useState, useEffect, useMemo } from "react";
1+
import React, { useState, useEffect, useMemo, useRef, useContext } from "react";
22
import { Label } from "@/components/ui/label";
33
import { ColorPickerProps } from "@/types";
44
import { debounce } from "@/utils/debounce";
5+
import { useColorControlFocus } from "@/store/color-control-focus-store";
6+
import { cn } from "@/lib/utils";
7+
import { SectionContext } from "./section-context";
58

6-
const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
9+
const ColorPicker = ({ color, onChange, label, name }: ColorPickerProps) => {
710
const [isOpen, setIsOpen] = useState(false);
811
const [localColor, setLocalColor] = useState(color);
12+
const [shouldAnimate, setShouldAnimate] = useState(false);
13+
const rootRef = useRef<HTMLDivElement>(null);
14+
const sectionCtx = useContext(SectionContext);
15+
const { registerColor, unregisterColor, highlightTarget } = useColorControlFocus();
16+
17+
// Register/unregister this color control with the focus store
18+
useEffect(() => {
19+
if (!name) return;
20+
registerColor(name, rootRef.current);
21+
return () => unregisterColor(name);
22+
}, [name, registerColor, unregisterColor]);
923

1024
// Update localColor if the prop changes externally
1125
useEffect(() => {
@@ -31,9 +45,39 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
3145
};
3246
}, [debouncedOnChange]);
3347

48+
const isHighlighted = name && highlightTarget === name;
49+
50+
useEffect(() => {
51+
if (isHighlighted) {
52+
// Trigger animation
53+
setShouldAnimate(true);
54+
55+
sectionCtx?.setIsExpanded(true);
56+
setTimeout(
57+
() => {
58+
rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
59+
},
60+
sectionCtx?.isExpanded ? 0 : 100
61+
);
62+
63+
// Reset animation after it completes
64+
const timer = setTimeout(() => {
65+
setShouldAnimate(false);
66+
}, 1000); // Duration should match the animation duration
67+
68+
return () => clearTimeout(timer);
69+
}
70+
}, [isHighlighted, sectionCtx]);
71+
3472
return (
35-
<div className="mb-3">
36-
<div className="flex items-center justify-between mb-1.5">
73+
<div
74+
ref={rootRef}
75+
className={cn(
76+
"mb-3 transition-all duration-300",
77+
shouldAnimate && "bg-border/50 -m-1.5 mb-1.5 rounded-sm p-1.5"
78+
)}
79+
>
80+
<div className="mb-1.5 flex items-center justify-between">
3781
<Label
3882
htmlFor={`color-${label.replace(/\s+/g, "-").toLowerCase()}`}
3983
className="text-xs font-medium"
@@ -43,7 +87,7 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
4387
</div>
4488
<div className="flex items-center gap-1">
4589
<div
46-
className="h-8 w-8 border cursor-pointer overflow-hidden relative flex items-center justify-center rounded"
90+
className="relative flex h-8 w-8 cursor-pointer items-center justify-center overflow-hidden rounded border"
4791
style={{ backgroundColor: localColor }}
4892
onClick={() => setIsOpen(!isOpen)}
4993
>
@@ -52,14 +96,14 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
5296
id={`color-${label.replace(/\s+/g, "-").toLowerCase()}`}
5397
value={localColor}
5498
onChange={handleColorChange}
55-
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
99+
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
56100
/>
57101
</div>
58102
<input
59103
type="text"
60104
value={localColor}
61105
onChange={handleColorChange}
62-
className="flex-1 h-8 px-2 text-sm rounded bg-input/25 border border-border/20"
106+
className="bg-input/25 border-border/20 h-8 flex-1 rounded border px-2 text-sm"
63107
/>
64108
</div>
65109
</div>

components/editor/control-section.tsx

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,44 @@ import React, { useState } from "react";
22
import { ChevronDown, ChevronUp } from "lucide-react";
33
import { cn } from "@/lib/utils";
44
import { ControlSectionProps } from "@/types";
5+
import { SectionContext } from "./section-context";
56

6-
const ControlSection = ({
7-
title,
8-
children,
9-
expanded = false,
10-
className,
11-
id,
12-
}: ControlSectionProps) => {
7+
const ControlSection = ({ title, children, expanded = false, className }: ControlSectionProps) => {
138
const [isExpanded, setIsExpanded] = useState(expanded);
149

1510
return (
16-
<div
17-
id={id}
18-
className={cn("mb-4 border rounded-lg overflow-hidden", className)}
11+
<SectionContext.Provider
12+
value={{
13+
isExpanded,
14+
setIsExpanded,
15+
toggleExpanded: () => setIsExpanded((prev) => !prev),
16+
}}
1917
>
20-
<div
21-
className="flex items-center justify-between p-3 cursor-pointer bg-background hover:bg-muted"
22-
onClick={() => setIsExpanded(!isExpanded)}
23-
>
24-
<h3 className="font-medium text-sm">{title}</h3>
25-
<button
26-
type="button"
27-
className="text-muted-foreground hover:text-foreground transition-colors"
28-
aria-label={isExpanded ? "Collapse section" : "Expand section"}
18+
<div id={id} className={cn("mb-4 overflow-hidden rounded-lg border", className)}>
19+
<div
20+
className="bg-background hover:bg-muted flex cursor-pointer items-center justify-between p-3"
21+
onClick={() => setIsExpanded(!isExpanded)}
2922
>
30-
{isExpanded ? (
31-
<ChevronUp className="h-4 w-4" />
32-
) : (
33-
<ChevronDown className="h-4 w-4" />
34-
)}
35-
</button>
36-
</div>
23+
<h3 className="text-sm font-medium">{title}</h3>
24+
<button
25+
type="button"
26+
className="text-muted-foreground hover:text-foreground transition-colors"
27+
aria-label={isExpanded ? "Collapse section" : "Expand section"}
28+
>
29+
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
30+
</button>
31+
</div>
3732

38-
<div
39-
className={cn(
40-
"overflow-hidden transition-all duration-200",
41-
isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"
42-
)}
43-
>
44-
<div className="p-3 bg-background border-t">{children}</div>
33+
<div
34+
className={cn(
35+
"overflow-hidden transition-all duration-200",
36+
isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"
37+
)}
38+
>
39+
<div className="bg-background border-t p-3">{children}</div>
40+
</div>
4541
</div>
46-
</div>
42+
</SectionContext.Provider>
4743
);
4844
};
4945

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext } from "react";
2+
3+
interface SectionContextType {
4+
/** Whether the parent ControlSection is currently expanded */
5+
isExpanded: boolean;
6+
/** Set the expanded state explicitly */
7+
setIsExpanded: (expanded: boolean) => void;
8+
/** Helper to toggle the expanded state */
9+
toggleExpanded: () => void;
10+
}
11+
12+
/**
13+
* Context that allows descendants of a ControlSection to query or mutate
14+
* the expanded / collapsed state of their parent section.
15+
*/
16+
export const SectionContext = createContext<SectionContextType | undefined>(undefined);

components/editor/shadow-control.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const ShadowControl: React.FC<ShadowControlProps> = ({
2525
<div className="space-y-4">
2626
<div>
2727
<ColorPicker
28+
name="shadow-color"
2829
color={shadowColor}
2930
onChange={(color) => onChange("shadow-color", color)}
3031
label="Shadow Color"

0 commit comments

Comments
 (0)