@hunterchen/canvas 0.6.0 → 0.8.0
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/README.md +239 -9
- package/dist/components/canvas/backgrounds.d.ts +4 -4
- package/dist/components/canvas/backgrounds.d.ts.map +1 -1
- package/dist/components/canvas/backgrounds.js +7 -7
- package/dist/components/canvas/backgrounds.js.map +1 -1
- package/dist/components/canvas/canvas.d.ts +5 -1
- package/dist/components/canvas/canvas.d.ts.map +1 -1
- package/dist/components/canvas/canvas.js +16 -16
- package/dist/components/canvas/canvas.js.map +1 -1
- package/dist/components/canvas/navbar/index.d.ts +4 -2
- package/dist/components/canvas/navbar/index.d.ts.map +1 -1
- package/dist/components/canvas/navbar/index.js +56 -11
- package/dist/components/canvas/navbar/index.js.map +1 -1
- package/dist/components/canvas/navbar/single-button.d.ts +10 -1
- package/dist/components/canvas/navbar/single-button.d.ts.map +1 -1
- package/dist/components/canvas/navbar/single-button.js +95 -15
- package/dist/components/canvas/navbar/single-button.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/canvas.d.ts.map +1 -1
- package/dist/lib/canvas.js +3 -3
- package/dist/lib/canvas.js.map +1 -1
- package/dist/lib/constants.d.ts +2 -2
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +2 -2
- package/dist/lib/constants.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/components/canvas/backgrounds.tsx +7 -7
- package/src/components/canvas/canvas.tsx +30 -15
- package/src/components/canvas/navbar/index.tsx +91 -15
- package/src/components/canvas/navbar/single-button.tsx +210 -56
- package/src/index.ts +5 -0
- package/src/lib/canvas.ts +4 -4
- package/src/lib/constants.ts +2 -2
- package/src/styles.css +15 -13
- package/src/types/index.ts +91 -0
|
@@ -32,8 +32,8 @@ export interface DefaultCanvasBackgroundProps {
|
|
|
32
32
|
children?: ReactNode;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** The default canvas gradient (neutral gray) */
|
|
36
|
-
export const DEFAULT_CANVAS_GRADIENT = `radial-gradient(ellipse ${canvasWidth}px ${canvasHeight}px at ${canvasWidth / 2}px ${canvasHeight}px, #
|
|
35
|
+
/** The default canvas gradient (neutral light gray) */
|
|
36
|
+
export const DEFAULT_CANVAS_GRADIENT = `radial-gradient(ellipse ${canvasWidth}px ${canvasHeight}px at ${canvasWidth / 2}px ${canvasHeight}px, #fafafa 0%, #f5f5f5 41%, #e5e5e5 59%, #fafafa 90%)`;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Default canvas background with gradient, dots, and noise filter.
|
|
@@ -42,7 +42,7 @@ export const DEFAULT_CANVAS_GRADIENT = `radial-gradient(ellipse ${canvasWidth}px
|
|
|
42
42
|
export const DefaultCanvasBackground: React.FC<DefaultCanvasBackgroundProps> = ({
|
|
43
43
|
gradientStyle,
|
|
44
44
|
showDots = true,
|
|
45
|
-
dotColor = "#
|
|
45
|
+
dotColor = "#a3a3a3",
|
|
46
46
|
dotSize = 1.5,
|
|
47
47
|
dotSpacing = 22,
|
|
48
48
|
dotOpacity = 0.35,
|
|
@@ -117,7 +117,7 @@ export interface DefaultWrapperBackgroundProps {
|
|
|
117
117
|
* Default wrapper/intro background gradient.
|
|
118
118
|
*/
|
|
119
119
|
export const DefaultWrapperBackground: React.FC<DefaultWrapperBackgroundProps> = ({
|
|
120
|
-
gradient = "linear-gradient(to top, #
|
|
120
|
+
gradient = "linear-gradient(to top, #f5f5f5 0%, #fafafa 50%, #ffffff 100%)",
|
|
121
121
|
className,
|
|
122
122
|
style,
|
|
123
123
|
}) => {
|
|
@@ -197,9 +197,9 @@ export const DefaultIntroContent: React.FC<DefaultIntroContentProps> = ({
|
|
|
197
197
|
);
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
// Default gradient values for export (neutral grays)
|
|
200
|
+
// Default gradient values for export (neutral light grays)
|
|
201
201
|
export const DEFAULT_INTRO_GRADIENT =
|
|
202
|
-
"linear-gradient(to top, #
|
|
202
|
+
"linear-gradient(to top, #f5f5f5 0%, #fafafa 50%, #ffffff 100%)";
|
|
203
203
|
|
|
204
204
|
export const DEFAULT_CANVAS_BOX_GRADIENT =
|
|
205
|
-
"radial-gradient(130.38% 95% at 50.03% 97.25%, #
|
|
205
|
+
"radial-gradient(130.38% 95% at 50.03% 97.25%, #f5f5f5 0%, #fafafa 48.09%, #ffffff 100%)";
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
STAGE2_TRANSITION,
|
|
36
36
|
MOUSE_WHEEL_ZOOM_SENSITIVITY,
|
|
37
37
|
TRACKPAD_ZOOM_SENSITIVITY,
|
|
38
|
+
DEFAULT_CANVAS_WIDTH,
|
|
39
|
+
DEFAULT_CANVAS_HEIGHT,
|
|
38
40
|
} from "../../lib/constants";
|
|
39
41
|
import useWindowDimensions from "../../hooks/useWindowDimensions";
|
|
40
42
|
import Navbar from "./navbar";
|
|
@@ -42,6 +44,7 @@ import Toolbar from "./toolbar";
|
|
|
42
44
|
import type {
|
|
43
45
|
CanvasSection,
|
|
44
46
|
NavItem,
|
|
47
|
+
NavbarConfig,
|
|
45
48
|
SectionCoordinates,
|
|
46
49
|
ToolbarConfig,
|
|
47
50
|
} from "../../types";
|
|
@@ -54,6 +57,10 @@ interface Props {
|
|
|
54
57
|
homeCoordinates: SectionCoordinates;
|
|
55
58
|
children: React.ReactNode;
|
|
56
59
|
|
|
60
|
+
// Optional height and with params, if omitted sizing will be 6000x4000
|
|
61
|
+
canvasWidth?:number;
|
|
62
|
+
canvasHeight?:number;
|
|
63
|
+
|
|
57
64
|
// Navbar data (optional). If omitted, navbar is hidden.
|
|
58
65
|
/** Array of navigation items for the navbar. If omitted, navbar is hidden. */
|
|
59
66
|
navItems?: NavItem[];
|
|
@@ -83,6 +90,10 @@ interface Props {
|
|
|
83
90
|
// ============== Toolbar Customization ==============
|
|
84
91
|
/** Toolbar customization options */
|
|
85
92
|
toolbarConfig?: ToolbarConfig;
|
|
93
|
+
|
|
94
|
+
// ============== Navbar Customization ==============
|
|
95
|
+
/** Navbar customization options */
|
|
96
|
+
navbarConfig?: NavbarConfig;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
const stopAllMotion = (
|
|
@@ -109,6 +120,9 @@ const Canvas: FC<Props> = ({
|
|
|
109
120
|
canvasBackground,
|
|
110
121
|
wrapperBackground,
|
|
111
122
|
toolbarConfig,
|
|
123
|
+
navbarConfig,
|
|
124
|
+
canvasHeight,
|
|
125
|
+
canvasWidth,
|
|
112
126
|
}) => {
|
|
113
127
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
114
128
|
|
|
@@ -116,8 +130,8 @@ const Canvas: FC<Props> = ({
|
|
|
116
130
|
|
|
117
131
|
const hasNavbar = Boolean(navItems && navItems.length > 0);
|
|
118
132
|
|
|
119
|
-
const sceneWidth = canvasWidth;
|
|
120
|
-
const sceneHeight = canvasHeight;
|
|
133
|
+
const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;
|
|
134
|
+
const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
|
|
121
135
|
|
|
122
136
|
const MIN_ZOOM = MIN_ZOOMS[getScreenSizeEnum(windowWidth)];
|
|
123
137
|
|
|
@@ -176,13 +190,13 @@ const Canvas: FC<Props> = ({
|
|
|
176
190
|
// Precompute final stage1 scale and offsets (snapshot dimensions once on mount)
|
|
177
191
|
const stage1Targets = useMemo(() => {
|
|
178
192
|
const finalScale = Math.max(
|
|
179
|
-
(windowWidth || 0) /
|
|
180
|
-
(windowHeight || 0) /
|
|
193
|
+
(windowWidth || 0) / sceneWidth,
|
|
194
|
+
(windowHeight || 0) / sceneHeight
|
|
181
195
|
);
|
|
182
|
-
const endX = (windowWidth -
|
|
183
|
-
const endY = (windowHeight -
|
|
196
|
+
const endX = (windowWidth - sceneWidth * finalScale) / 2;
|
|
197
|
+
const endY = (windowHeight - sceneHeight * finalScale) / 2;
|
|
184
198
|
return { finalScale, endX, endY };
|
|
185
|
-
}, [windowWidth, windowHeight]);
|
|
199
|
+
}, [windowWidth, windowHeight, sceneWidth, sceneHeight]);
|
|
186
200
|
|
|
187
201
|
// Replace direct motion values with derived transforms during stage1
|
|
188
202
|
const derivedScale = useTransform(
|
|
@@ -406,8 +420,8 @@ const Canvas: FC<Props> = ({
|
|
|
406
420
|
|
|
407
421
|
let newZoom = initialZoom * (currentDistance / initialDistance);
|
|
408
422
|
newZoom = Math.max(
|
|
409
|
-
(window.innerWidth /
|
|
410
|
-
(window.innerHeight /
|
|
423
|
+
(window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
|
|
424
|
+
(window.innerHeight / sceneHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas
|
|
411
425
|
Math.min(newZoom, 10),
|
|
412
426
|
MIN_ZOOM // Ensure zoom is not less than MIN_ZOOM
|
|
413
427
|
);
|
|
@@ -517,8 +531,8 @@ const Canvas: FC<Props> = ({
|
|
|
517
531
|
MAX_ZOOM
|
|
518
532
|
),
|
|
519
533
|
MIN_ZOOM,
|
|
520
|
-
(window.innerWidth /
|
|
521
|
-
(window.innerHeight /
|
|
534
|
+
(window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
|
|
535
|
+
(window.innerHeight / sceneHeight) * ZOOM_BOUND // Ensure zoom is at least the height of the canvas
|
|
522
536
|
);
|
|
523
537
|
|
|
524
538
|
const rect = viewportRef.current?.getBoundingClientRect();
|
|
@@ -636,13 +650,14 @@ const Canvas: FC<Props> = ({
|
|
|
636
650
|
config={toolbarConfig}
|
|
637
651
|
/>
|
|
638
652
|
)}
|
|
639
|
-
{hasNavbar && navItems
|
|
653
|
+
{hasNavbar && navItems && !navbarConfig?.hidden && (
|
|
640
654
|
<Navbar
|
|
641
655
|
panToOffset={handlePanToOffset}
|
|
642
656
|
onReset={onResetViewAndItems}
|
|
643
657
|
items={navItems}
|
|
658
|
+
config={navbarConfig}
|
|
644
659
|
/>
|
|
645
|
-
)
|
|
660
|
+
)}
|
|
646
661
|
</>
|
|
647
662
|
)}
|
|
648
663
|
<div
|
|
@@ -666,8 +681,8 @@ const Canvas: FC<Props> = ({
|
|
|
666
681
|
animate={{ opacity: 1 }}
|
|
667
682
|
transition={{ duration: 0.3, ease: "easeIn" }}
|
|
668
683
|
style={{
|
|
669
|
-
width: `${
|
|
670
|
-
height: `${
|
|
684
|
+
width: `${sceneWidth}px`,
|
|
685
|
+
height: `${sceneHeight}px`,
|
|
671
686
|
x,
|
|
672
687
|
y,
|
|
673
688
|
scale,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { motion, useMotionValueEvent } from "framer-motion";
|
|
2
2
|
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
|
3
3
|
import SingleButton from "./single-button";
|
|
4
|
-
import type { NavItem } from "../../../types";
|
|
4
|
+
import type { NavItem, NavbarConfig, NavbarPosition } from "../../../types";
|
|
5
5
|
import { useCanvasContext } from "../../../contexts/CanvasContext";
|
|
6
6
|
import useWindowDimensions from "../../../hooks/useWindowDimensions";
|
|
7
7
|
import { usePerformanceMode } from "../../../hooks/usePerformanceMode";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
RESPONSIVE_ZOOM_MAP,
|
|
14
14
|
NAVBAR_DEBOUNCE_MS,
|
|
15
15
|
} from "../../../lib/constants";
|
|
16
|
+
import { cn } from "../../../lib/utils";
|
|
16
17
|
|
|
17
18
|
interface NavbarProps {
|
|
18
19
|
panToOffset: (
|
|
@@ -23,12 +24,50 @@ interface NavbarProps {
|
|
|
23
24
|
onReset: () => void;
|
|
24
25
|
/** Array of navigation items defining sections, their icons, and coordinates */
|
|
25
26
|
items: NavItem[];
|
|
27
|
+
/** Navbar configuration options */
|
|
28
|
+
config?: NavbarConfig;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
const positionStyles: Record<NavbarPosition, React.CSSProperties> = {
|
|
32
|
+
top: {
|
|
33
|
+
top: "1rem",
|
|
34
|
+
bottom: "auto",
|
|
35
|
+
left: "50%",
|
|
36
|
+
transform: "translateX(-50%)",
|
|
37
|
+
},
|
|
38
|
+
bottom: {
|
|
39
|
+
bottom: "1rem",
|
|
40
|
+
top: "auto",
|
|
41
|
+
left: "50%",
|
|
42
|
+
transform: "translateX(-50%)",
|
|
43
|
+
},
|
|
44
|
+
left: {
|
|
45
|
+
left: "1rem",
|
|
46
|
+
right: "auto",
|
|
47
|
+
top: "50%",
|
|
48
|
+
transform: "translateY(-50%)",
|
|
49
|
+
},
|
|
50
|
+
right: {
|
|
51
|
+
right: "1rem",
|
|
52
|
+
left: "auto",
|
|
53
|
+
top: "50%",
|
|
54
|
+
transform: "translateY(-50%)",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Responsive position adjustments (mobile vs desktop)
|
|
59
|
+
const responsivePositionClasses: Record<NavbarPosition, string> = {
|
|
60
|
+
top: "top-12 md:top-4",
|
|
61
|
+
bottom: "bottom-12 md:bottom-4",
|
|
62
|
+
left: "left-4",
|
|
63
|
+
right: "right-4",
|
|
64
|
+
};
|
|
65
|
+
|
|
28
66
|
export default function Navbar({
|
|
29
67
|
panToOffset,
|
|
30
68
|
onReset,
|
|
31
69
|
items,
|
|
70
|
+
config = {},
|
|
32
71
|
}: NavbarProps) {
|
|
33
72
|
const { x, y, scale, animationStage, setNextTargetSection } =
|
|
34
73
|
useCanvasContext();
|
|
@@ -50,6 +89,20 @@ export default function Navbar({
|
|
|
50
89
|
// Derive debounce duration from performance mode
|
|
51
90
|
const debounceMs = NAVBAR_DEBOUNCE_MS[mode] ?? 0;
|
|
52
91
|
|
|
92
|
+
// Extract config values
|
|
93
|
+
const {
|
|
94
|
+
display = "icons",
|
|
95
|
+
position = "bottom",
|
|
96
|
+
className,
|
|
97
|
+
style,
|
|
98
|
+
buttonConfig,
|
|
99
|
+
tooltipConfig,
|
|
100
|
+
gap,
|
|
101
|
+
padding,
|
|
102
|
+
} = config;
|
|
103
|
+
|
|
104
|
+
const isVertical = position === "left" || position === "right";
|
|
105
|
+
|
|
53
106
|
// Find the home section from items
|
|
54
107
|
const homeItem = useMemo(() => items.find((item) => item.isHome), [items]);
|
|
55
108
|
|
|
@@ -141,24 +194,43 @@ export default function Navbar({
|
|
|
141
194
|
};
|
|
142
195
|
}, [handlePan, animationStage, homeItem]);
|
|
143
196
|
|
|
197
|
+
// Compute container styles (positioning only)
|
|
198
|
+
const containerStyle: React.CSSProperties = {
|
|
199
|
+
position: "fixed",
|
|
200
|
+
zIndex: 1000,
|
|
201
|
+
pointerEvents: "auto",
|
|
202
|
+
display: "flex",
|
|
203
|
+
justifyContent: "center",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
...positionStyles[position],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Compute inner container styles (visual styling)
|
|
209
|
+
const innerStyle: React.CSSProperties = {
|
|
210
|
+
...(gap !== undefined && { gap: `${gap}px` }),
|
|
211
|
+
...(padding !== undefined && { padding: `${padding}px` }),
|
|
212
|
+
...(isVertical && { flexDirection: "column" }),
|
|
213
|
+
...style,
|
|
214
|
+
};
|
|
215
|
+
|
|
144
216
|
return (
|
|
145
217
|
<div
|
|
146
|
-
className=
|
|
147
|
-
style={
|
|
148
|
-
position: "fixed",
|
|
149
|
-
left: "50%",
|
|
150
|
-
transform: "translateX(-50%)",
|
|
151
|
-
zIndex: 1000,
|
|
152
|
-
pointerEvents: "auto",
|
|
153
|
-
display: "flex",
|
|
154
|
-
justifyContent: "center",
|
|
155
|
-
alignItems: "center",
|
|
156
|
-
}}
|
|
218
|
+
className={responsivePositionClasses[position]}
|
|
219
|
+
style={containerStyle}
|
|
157
220
|
>
|
|
158
221
|
{/* padding to prevent edge bug */}
|
|
159
|
-
<div className="px-4 md:px-8">
|
|
160
|
-
<motion.div
|
|
161
|
-
|
|
222
|
+
<div className={isVertical ? "py-4 md:py-8" : "px-4 md:px-8"}>
|
|
223
|
+
<motion.div
|
|
224
|
+
className={cn(
|
|
225
|
+
"flex select-none items-center justify-center gap-1 rounded-[10px] border p-1 shadow-[0_6px_12px_rgba(0,0,0,0.08)]",
|
|
226
|
+
!style?.backgroundColor && "bg-white",
|
|
227
|
+
!style?.borderColor && "border-neutral-200",
|
|
228
|
+
isVertical && "flex-col",
|
|
229
|
+
className,
|
|
230
|
+
)}
|
|
231
|
+
style={innerStyle}
|
|
232
|
+
>
|
|
233
|
+
<div className={cn("flex items-center gap-1", isVertical && "flex-col")}>
|
|
162
234
|
{items.map((item) => (
|
|
163
235
|
<SingleButton
|
|
164
236
|
key={item.id}
|
|
@@ -167,6 +239,10 @@ export default function Navbar({
|
|
|
167
239
|
onClick={() => handlePan(item)}
|
|
168
240
|
isPushed={expandedButton === item.id}
|
|
169
241
|
onDebouncedClick={handleDebouncedClick}
|
|
242
|
+
displayMode={display}
|
|
243
|
+
buttonConfig={buttonConfig}
|
|
244
|
+
tooltipConfig={tooltipConfig}
|
|
245
|
+
isVertical={isVertical}
|
|
170
246
|
/>
|
|
171
247
|
))}
|
|
172
248
|
</div>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import * as LucideIcons from "lucide-react";
|
|
3
3
|
import { AnimatePresence, motion } from "framer-motion";
|
|
4
|
+
import type {
|
|
5
|
+
NavbarDisplayMode,
|
|
6
|
+
NavbarButtonConfig,
|
|
7
|
+
NavbarTooltipConfig,
|
|
8
|
+
} from "../../../types";
|
|
9
|
+
import { cn } from "../../../lib/utils";
|
|
4
10
|
|
|
5
11
|
interface SingleButtonProps {
|
|
6
12
|
label: string;
|
|
@@ -10,6 +16,14 @@ interface SingleButtonProps {
|
|
|
10
16
|
isPushed: boolean;
|
|
11
17
|
link?: string;
|
|
12
18
|
onDebouncedClick?: (callback: () => void) => void;
|
|
19
|
+
/** Display mode for the button */
|
|
20
|
+
displayMode?: NavbarDisplayMode;
|
|
21
|
+
/** Button styling configuration */
|
|
22
|
+
buttonConfig?: NavbarButtonConfig;
|
|
23
|
+
/** Tooltip configuration */
|
|
24
|
+
tooltipConfig?: NavbarTooltipConfig;
|
|
25
|
+
/** Whether the navbar is in vertical layout */
|
|
26
|
+
isVertical?: boolean;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
export default function SingleButton({
|
|
@@ -19,17 +33,48 @@ export default function SingleButton({
|
|
|
19
33
|
isPushed,
|
|
20
34
|
link,
|
|
21
35
|
onDebouncedClick,
|
|
36
|
+
displayMode = "icons",
|
|
37
|
+
buttonConfig = {},
|
|
38
|
+
tooltipConfig = {},
|
|
39
|
+
isVertical = false,
|
|
22
40
|
}: SingleButtonProps) {
|
|
23
41
|
const [isHovered, setIsHovered] = useState(false);
|
|
24
42
|
const [showTag, setShowTag] = useState(false);
|
|
25
43
|
const [copiedEmail, setCopiedEmail] = useState(false);
|
|
44
|
+
|
|
26
45
|
const isLucideIconName = typeof icon === "string";
|
|
27
46
|
const IconComponent = isLucideIconName
|
|
28
47
|
? (LucideIcons[icon as keyof typeof LucideIcons] as LucideIcons.LucideIcon | undefined)
|
|
29
48
|
: icon;
|
|
30
|
-
const TagDelay = 100;
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
// Extract config values
|
|
51
|
+
const {
|
|
52
|
+
className: buttonClassName,
|
|
53
|
+
style: buttonStyle,
|
|
54
|
+
activeClassName,
|
|
55
|
+
activeStyle,
|
|
56
|
+
hoverClassName,
|
|
57
|
+
hoverStyle,
|
|
58
|
+
iconClassName,
|
|
59
|
+
iconSize = 20,
|
|
60
|
+
labelClassName,
|
|
61
|
+
labelStyle,
|
|
62
|
+
} = buttonConfig;
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
disabled: tooltipDisabled = false,
|
|
66
|
+
className: tooltipClassName,
|
|
67
|
+
style: tooltipStyle,
|
|
68
|
+
delay: tooltipDelay = 100,
|
|
69
|
+
} = tooltipConfig;
|
|
70
|
+
|
|
71
|
+
// Determine what to show based on display mode
|
|
72
|
+
const showIcon = displayMode !== "labels";
|
|
73
|
+
const allowExpand = displayMode === "icons"; // Only expand on active in icons mode
|
|
74
|
+
const showTooltip = (displayMode === "icons" || displayMode === "compact") && !tooltipDisabled;
|
|
75
|
+
|
|
76
|
+
// Validate icon component for modes that need it
|
|
77
|
+
if (showIcon && !IconComponent) {
|
|
33
78
|
throw new Error(
|
|
34
79
|
"A valid 'icon' prop is required (Lucide icon name or custom icon component).",
|
|
35
80
|
);
|
|
@@ -38,10 +83,10 @@ export default function SingleButton({
|
|
|
38
83
|
useEffect(() => {
|
|
39
84
|
let timeoutId: NodeJS.Timeout;
|
|
40
85
|
|
|
41
|
-
if (isHovered) {
|
|
86
|
+
if (isHovered && showTooltip) {
|
|
42
87
|
timeoutId = setTimeout(() => {
|
|
43
88
|
setShowTag(true);
|
|
44
|
-
},
|
|
89
|
+
}, tooltipDelay);
|
|
45
90
|
} else {
|
|
46
91
|
setShowTag(false);
|
|
47
92
|
}
|
|
@@ -49,7 +94,7 @@ export default function SingleButton({
|
|
|
49
94
|
return () => {
|
|
50
95
|
clearTimeout(timeoutId);
|
|
51
96
|
};
|
|
52
|
-
}, [isHovered]);
|
|
97
|
+
}, [isHovered, showTooltip, tooltipDelay]);
|
|
53
98
|
|
|
54
99
|
useEffect(() => {
|
|
55
100
|
setShowTag(false);
|
|
@@ -85,11 +130,168 @@ export default function SingleButton({
|
|
|
85
130
|
|
|
86
131
|
const displayLabel = copiedEmail ? "Email copied!" : label;
|
|
87
132
|
|
|
133
|
+
// Compute button classes
|
|
134
|
+
const baseButtonClass = "relative flex items-center rounded-md p-2 text-neutral-500 transition-colors duration-200 focus:outline-none";
|
|
135
|
+
// Only apply default classes if no custom style is provided
|
|
136
|
+
const stateClass = isPushed
|
|
137
|
+
? (activeClassName || (!activeStyle && "bg-neutral-200"))
|
|
138
|
+
: isHovered
|
|
139
|
+
? (hoverClassName || (!hoverStyle && "bg-neutral-100"))
|
|
140
|
+
: "";
|
|
141
|
+
|
|
142
|
+
// Compute button styles
|
|
143
|
+
const computedButtonStyle: React.CSSProperties = {
|
|
144
|
+
...buttonStyle,
|
|
145
|
+
...(isPushed && activeStyle),
|
|
146
|
+
...(isHovered && !isPushed && hoverStyle),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Compute icon classes and styles
|
|
150
|
+
const iconSizeStyle = { width: iconSize, height: iconSize };
|
|
151
|
+
const baseIconClass = "flex-shrink-0";
|
|
152
|
+
// Only apply default icon colors if no custom button color style is provided
|
|
153
|
+
const hasCustomColor = buttonStyle?.color;
|
|
154
|
+
const iconColorClass = hasCustomColor
|
|
155
|
+
? ""
|
|
156
|
+
: isPushed
|
|
157
|
+
? "text-neutral-700"
|
|
158
|
+
: "text-neutral-500";
|
|
159
|
+
|
|
160
|
+
// Compute label classes
|
|
161
|
+
const baseLabelClass = "whitespace-nowrap font-canvas-figtree text-sm font-medium text-neutral-700";
|
|
162
|
+
|
|
163
|
+
// Tooltip position based on vertical layout
|
|
164
|
+
const tooltipPositionClass = isVertical
|
|
165
|
+
? "left-full top-1/2 -translate-y-1/2 ml-2"
|
|
166
|
+
: "-top-10 left-1/2";
|
|
167
|
+
const tooltipTransform = isVertical
|
|
168
|
+
? { x: 0, y: "-50%" }
|
|
169
|
+
: { x: "-50%" };
|
|
170
|
+
|
|
171
|
+
// Render icon element
|
|
172
|
+
const renderIcon = () => {
|
|
173
|
+
if (!showIcon || !IconComponent) return null;
|
|
174
|
+
return (
|
|
175
|
+
<IconComponent
|
|
176
|
+
className={cn(baseIconClass, iconColorClass, iconClassName)}
|
|
177
|
+
style={iconSizeStyle}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Render label element
|
|
183
|
+
const renderLabel = (animated = false) => {
|
|
184
|
+
if (animated) {
|
|
185
|
+
return (
|
|
186
|
+
<motion.span
|
|
187
|
+
initial={{ opacity: 0, width: 0 }}
|
|
188
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
189
|
+
exit={{ opacity: 0, width: 0 }}
|
|
190
|
+
transition={{
|
|
191
|
+
duration: 0.1,
|
|
192
|
+
ease: "easeInOut",
|
|
193
|
+
}}
|
|
194
|
+
className={cn("overflow-hidden", baseLabelClass, labelClassName)}
|
|
195
|
+
style={labelStyle}
|
|
196
|
+
>
|
|
197
|
+
{displayLabel}
|
|
198
|
+
</motion.span>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return (
|
|
202
|
+
<span
|
|
203
|
+
className={cn(baseLabelClass, labelClassName)}
|
|
204
|
+
style={labelStyle}
|
|
205
|
+
>
|
|
206
|
+
{displayLabel}
|
|
207
|
+
</span>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Render tooltip
|
|
212
|
+
const renderTooltip = () => {
|
|
213
|
+
if (!showTooltip || !showTag || isPushed) return null;
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<AnimatePresence>
|
|
217
|
+
<motion.div
|
|
218
|
+
initial={{ opacity: 0, y: isVertical ? 0 : 5, scale: 0.9, ...tooltipTransform }}
|
|
219
|
+
animate={{ opacity: 1, y: 0, scale: 1, ...tooltipTransform }}
|
|
220
|
+
exit={{ opacity: 0, y: isVertical ? 0 : 5, scale: 0.9, ...tooltipTransform }}
|
|
221
|
+
transition={{
|
|
222
|
+
duration: 0.05,
|
|
223
|
+
ease: "easeOut",
|
|
224
|
+
}}
|
|
225
|
+
className={cn("pointer-events-none absolute z-50", tooltipPositionClass)}
|
|
226
|
+
>
|
|
227
|
+
<div className="rounded-sm bg-gradient-to-t from-black/10 to-transparent px-[1px] pb-[2.5px] pt-[1px]">
|
|
228
|
+
<div
|
|
229
|
+
className={cn(
|
|
230
|
+
"whitespace-nowrap rounded-sm px-2 py-1 font-canvas-figtree text-sm",
|
|
231
|
+
!tooltipStyle?.backgroundColor && "bg-neutral-50",
|
|
232
|
+
!tooltipStyle?.color && "text-neutral-600",
|
|
233
|
+
tooltipClassName,
|
|
234
|
+
)}
|
|
235
|
+
style={tooltipStyle}
|
|
236
|
+
>
|
|
237
|
+
{displayLabel}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</motion.div>
|
|
241
|
+
</AnimatePresence>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Render based on display mode
|
|
246
|
+
const renderContent = () => {
|
|
247
|
+
// Labels only mode
|
|
248
|
+
if (displayMode === "labels") {
|
|
249
|
+
return renderLabel();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Icons + labels always mode
|
|
253
|
+
if (displayMode === "icons-labels") {
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex items-center gap-2">
|
|
256
|
+
{renderIcon()}
|
|
257
|
+
{renderLabel()}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Compact mode - icons only, no expansion
|
|
263
|
+
if (displayMode === "compact") {
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
{renderIcon()}
|
|
267
|
+
{renderTooltip()}
|
|
268
|
+
</>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Icons mode (default) - expands on active
|
|
273
|
+
if (isPushed && allowExpand) {
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex items-center gap-2">
|
|
276
|
+
<div>{renderIcon()}</div>
|
|
277
|
+
{renderLabel(true)}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<>
|
|
284
|
+
{renderIcon()}
|
|
285
|
+
{renderTooltip()}
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
88
290
|
return (
|
|
89
291
|
<motion.button
|
|
90
292
|
aria-label={label}
|
|
91
|
-
className={
|
|
92
|
-
|
|
293
|
+
className={cn(baseButtonClass, stateClass, buttonClassName)}
|
|
294
|
+
style={computedButtonStyle}
|
|
93
295
|
onClick={handleClick}
|
|
94
296
|
onMouseEnter={() => setIsHovered(true)}
|
|
95
297
|
onMouseLeave={() => setIsHovered(false)}
|
|
@@ -100,55 +302,7 @@ export default function SingleButton({
|
|
|
100
302
|
damping: 25,
|
|
101
303
|
}}
|
|
102
304
|
>
|
|
103
|
-
{
|
|
104
|
-
<div className="flex items-center gap-2">
|
|
105
|
-
<div>
|
|
106
|
-
<IconComponent
|
|
107
|
-
className={`h-5 w-5 flex-shrink-0 ${isPushed ? (isLucideIconName ? "text-canvas-emphasis" : "text-white") : "text-canvas-medium"
|
|
108
|
-
}`}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
<motion.span
|
|
112
|
-
initial={{ opacity: 0, width: 0 }}
|
|
113
|
-
animate={{ opacity: 1, width: "auto" }}
|
|
114
|
-
exit={{ opacity: 0, width: 0 }}
|
|
115
|
-
transition={{
|
|
116
|
-
duration: 0.1,
|
|
117
|
-
ease: "easeInOut",
|
|
118
|
-
}}
|
|
119
|
-
className="overflow-hidden whitespace-nowrap font-canvas-figtree text-sm font-medium text-canvas-emphasis"
|
|
120
|
-
>
|
|
121
|
-
{displayLabel}
|
|
122
|
-
</motion.span>
|
|
123
|
-
</div>
|
|
124
|
-
) : (
|
|
125
|
-
<div>
|
|
126
|
-
<IconComponent
|
|
127
|
-
className={`h-5 w-5 flex-shrink-0 ${isPushed ? (isLucideIconName ? "text-canvas-emphasis" : "text-white") : "text-canvas-medium"
|
|
128
|
-
}`}
|
|
129
|
-
/>
|
|
130
|
-
<AnimatePresence>
|
|
131
|
-
{showTag && !isPushed && (
|
|
132
|
-
<motion.div
|
|
133
|
-
initial={{ opacity: 0, y: 5, scale: 0.9, x: "-50%" }}
|
|
134
|
-
animate={{ opacity: 1, y: 0, scale: 1, x: "-50%" }}
|
|
135
|
-
exit={{ opacity: 0, y: 5, scale: 0.9, x: "-50%" }}
|
|
136
|
-
transition={{
|
|
137
|
-
duration: 0.05,
|
|
138
|
-
ease: "easeOut",
|
|
139
|
-
}}
|
|
140
|
-
className="pointer-events-none absolute -top-10 left-1/2 z-50"
|
|
141
|
-
>
|
|
142
|
-
<div className="rounded-sm bg-gradient-to-t from-black/10 to-transparent px-[1px] pb-[2.5px] pt-[1px]">
|
|
143
|
-
<div className="whitespace-nowrap rounded-sm bg-canvas-offwhite px-2 py-1 font-canvas-figtree text-sm text-canvas-medium">
|
|
144
|
-
{displayLabel}
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</motion.div>
|
|
148
|
-
)}
|
|
149
|
-
</AnimatePresence>
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
305
|
+
{renderContent()}
|
|
152
306
|
</motion.button>
|
|
153
307
|
);
|
|
154
308
|
}
|
package/src/index.ts
CHANGED
package/src/lib/canvas.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { animate, type MotionValue, type Point } from "framer-motion";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
DEFAULT_CANVAS_WIDTH,
|
|
5
|
+
DEFAULT_CANVAS_HEIGHT,
|
|
6
6
|
MAX_DIM_RATIO,
|
|
7
7
|
INTRO_ASPECT_RATIO,
|
|
8
8
|
PAN_SPRING,
|
|
9
9
|
ScreenSizeEnum,
|
|
10
10
|
} from "./constants";
|
|
11
11
|
|
|
12
|
-
export const canvasWidth =
|
|
13
|
-
export const canvasHeight =
|
|
12
|
+
export const canvasWidth = DEFAULT_CANVAS_WIDTH;
|
|
13
|
+
export const canvasHeight = DEFAULT_CANVAS_HEIGHT;
|
|
14
14
|
|
|
15
15
|
// Re-export ScreenSizeEnum for backward compatibility
|
|
16
16
|
export { ScreenSizeEnum } from "./constants";
|
package/src/lib/constants.ts
CHANGED
|
@@ -24,10 +24,10 @@ export enum ScreenSizeEnum {
|
|
|
24
24
|
// ============================================================================
|
|
25
25
|
|
|
26
26
|
/** Default canvas width in pixels */
|
|
27
|
-
export const
|
|
27
|
+
export const DEFAULT_CANVAS_WIDTH = 6000;
|
|
28
28
|
|
|
29
29
|
/** Default canvas height in pixels */
|
|
30
|
-
export const
|
|
30
|
+
export const DEFAULT_CANVAS_HEIGHT = 4000;
|
|
31
31
|
|
|
32
32
|
// ============================================================================
|
|
33
33
|
// INTRO ANIMATION
|