@hunterchen/canvas 0.5.0 → 0.7.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.
@@ -1,19 +1,56 @@
1
1
  import { type Point, useTransform, motion } from "framer-motion";
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useState, useMemo } from "react";
3
3
  import { useCanvasContext } from "../../contexts/CanvasContext";
4
4
  import {
5
5
  TOOLBAR_OPACITY_POS_EPS,
6
6
  TOOLBAR_OPACITY_SCALE_EPS,
7
7
  } from "../../lib/constants";
8
+ import { cn } from "../../lib/utils";
9
+ import type { ToolbarConfig, ToolbarPosition } from "../../types";
8
10
 
9
11
  type ToolbarProps = {
10
12
  homeCoordinates?: Point;
13
+ config?: ToolbarConfig;
11
14
  };
12
15
 
13
- const Toolbar = ({ homeCoordinates = { x: 0, y: 0 } }: ToolbarProps) => {
16
+ const positionStyles: Record<ToolbarPosition, string> = {
17
+ "top-left": "left-4 top-6 sm:top-4",
18
+ "top-right": "right-4 top-6 sm:top-4",
19
+ "bottom-left": "left-4 bottom-6 sm:bottom-4",
20
+ "bottom-right": "right-4 bottom-6 sm:bottom-4",
21
+ };
22
+
23
+ const Toolbar = ({
24
+ homeCoordinates = { x: 0, y: 0 },
25
+ config = {},
26
+ }: ToolbarProps) => {
14
27
  const { x, y, scale } = useCanvasContext();
15
28
  const [hasMounted, setHasMounted] = useState(false);
16
29
 
30
+ const {
31
+ display = "both",
32
+ position = "top-left",
33
+ disableAutoHide = false,
34
+ className,
35
+ coordinatesClassName,
36
+ scaleClassName,
37
+ separatorClassName,
38
+ style,
39
+ coordinatesStyle,
40
+ scaleStyle,
41
+ separator = "|",
42
+ separatorGap,
43
+ coordinatesFormat,
44
+ scaleFormat,
45
+ } = config;
46
+
47
+ const separatorStyle: React.CSSProperties | undefined = separatorGap
48
+ ? {
49
+ marginInline:
50
+ typeof separatorGap === "number" ? `${separatorGap}px` : separatorGap,
51
+ }
52
+ : undefined;
53
+
17
54
  useEffect(() => {
18
55
  setHasMounted(true);
19
56
  }, []);
@@ -21,44 +58,120 @@ const Toolbar = ({ homeCoordinates = { x: 0, y: 0 } }: ToolbarProps) => {
21
58
  // numeric MotionValues
22
59
  const rawDx = useTransform(
23
60
  [x, scale],
24
- ([lx, ls]) => -((lx as number) / (ls as number)) + homeCoordinates.x,
61
+ ([lx, ls]) => -((lx as number) / (ls as number)) + homeCoordinates.x
25
62
  );
26
63
  const rawDy = useTransform(
27
64
  [y, scale],
28
- ([ly, ls]) => -((ly as number) / (ls as number)) + homeCoordinates.y,
65
+ ([ly, ls]) => -((ly as number) / (ls as number)) + homeCoordinates.y
29
66
  );
30
67
 
31
- // formatted MotionValues
68
+ // formatted MotionValues for default display
32
69
  const displayX = useTransform(rawDx, (v) => Math.round(v).toString());
33
70
  const displayY = useTransform(rawDy, (v) => Math.round(v).toString());
34
71
  const displayScale = useTransform(scale, (v) => v.toFixed(2));
35
72
 
36
- const opacity = useTransform([rawDx, rawDy, scale], ([dx, dy, ls]) =>
37
- Math.abs(dx as number) < TOOLBAR_OPACITY_POS_EPS &&
73
+ // For custom formatters, we need to use state to track values
74
+ const [currentX, setCurrentX] = useState(0);
75
+ const [currentY, setCurrentY] = useState(0);
76
+ const [currentScale, setCurrentScale] = useState(1);
77
+
78
+ useEffect(() => {
79
+ const unsubX = rawDx.on("change", (v) => setCurrentX(Math.round(v)));
80
+ const unsubY = rawDy.on("change", (v) => setCurrentY(Math.round(v)));
81
+ const unsubScale = scale.on("change", (v) => setCurrentScale(v));
82
+ return () => {
83
+ unsubX();
84
+ unsubY();
85
+ unsubScale();
86
+ };
87
+ }, [rawDx, rawDy, scale]);
88
+
89
+ const opacity = useTransform([rawDx, rawDy, scale], ([dx, dy, ls]) => {
90
+ if (disableAutoHide) return 1;
91
+ return Math.abs(dx as number) < TOOLBAR_OPACITY_POS_EPS &&
38
92
  Math.abs(dy as number) < TOOLBAR_OPACITY_POS_EPS &&
39
93
  Math.abs((ls as number) - 1) < TOOLBAR_OPACITY_SCALE_EPS
40
94
  ? 0
41
- : 1,
42
- );
95
+ : 1;
96
+ });
43
97
 
44
98
  const handlePointerDown = (e: React.PointerEvent) => e.stopPropagation();
45
99
 
100
+ const showCoordinates = display === "coordinates" || display === "both";
101
+ const showScale = display === "scale" || display === "both";
102
+ const showSeparator = display === "both";
103
+
104
+ // Compute formatted values
105
+ const formattedCoordinates = useMemo(() => {
106
+ if (coordinatesFormat) {
107
+ return coordinatesFormat(currentX, currentY);
108
+ }
109
+ return null; // Will use motion spans for default
110
+ }, [coordinatesFormat, currentX, currentY]);
111
+
112
+ const formattedScale = useMemo(() => {
113
+ if (scaleFormat) {
114
+ return scaleFormat(currentScale);
115
+ }
116
+ return null; // Will use motion span for default
117
+ }, [scaleFormat, currentScale]);
118
+
119
+ // Placeholder content for SSR/initial render
120
+ const placeholderContent = useMemo(() => {
121
+ const parts: string[] = [];
122
+ if (showCoordinates) parts.push("(0, 0)");
123
+ if (showSeparator) parts.push(separator);
124
+ if (showScale) parts.push("1.00x");
125
+ return parts.join("");
126
+ }, [showCoordinates, showScale, showSeparator, separator]);
127
+
46
128
  return (
47
129
  <motion.div
48
- className="absolute left-4 top-6 z-[1000] cursor-default select-none rounded-[10px] border border-border bg-canvas-offwhite p-2 font-mono text-xs text-canvas-heavy shadow-[0_6px_12px_rgba(0,0,0,0.10)] sm:top-4 md:text-sm"
130
+ className={cn(
131
+ "absolute z-[1000] cursor-default select-none rounded-[10px] border border-border bg-canvas-offwhite p-2 font-mono text-xs text-canvas-heavy shadow-[0_6px_12px_rgba(0,0,0,0.10)] md:text-sm",
132
+ positionStyles[position],
133
+ className
134
+ )}
49
135
  onPointerDown={handlePointerDown}
50
136
  data-toolbar-button
51
- style={{ opacity }}
137
+ style={{ opacity, ...style }}
52
138
  >
53
139
  {hasMounted ? (
54
140
  <>
55
- (<motion.span>{displayX}</motion.span>,{" "}
56
- <motion.span>{displayY}</motion.span>)
57
- <span className="text-canvas-light"> |</span>{" "}
58
- <motion.span>{displayScale}</motion.span>x
141
+ {showCoordinates && (
142
+ <span className={coordinatesClassName} style={coordinatesStyle}>
143
+ {formattedCoordinates !== null ? (
144
+ formattedCoordinates
145
+ ) : (
146
+ <>
147
+ (<motion.span>{displayX}</motion.span>,{" "}
148
+ <motion.span>{displayY}</motion.span>)
149
+ </>
150
+ )}
151
+ </span>
152
+ )}
153
+ {showSeparator && (
154
+ <span
155
+ className={cn("text-canvas-light", separatorClassName)}
156
+ style={separatorStyle}
157
+ >
158
+ {separator}
159
+ </span>
160
+ )}
161
+ {showScale && (
162
+ <span className={scaleClassName} style={scaleStyle}>
163
+ {formattedScale !== null ? (
164
+ formattedScale
165
+ ) : (
166
+ <>
167
+ <motion.span>{displayScale}</motion.span>x
168
+ </>
169
+ )}
170
+ </span>
171
+ )}
59
172
  </>
60
173
  ) : (
61
- <span style={{ opacity: 0 }}>(0, 0) | 1.00x</span>
174
+ <span style={{ opacity: 0 }}>{placeholderContent}</span>
62
175
  )}
63
176
  </motion.div>
64
177
  );
package/src/index.ts CHANGED
@@ -1,41 +1,59 @@
1
1
  // Components
2
- export { default as Canvas } from './components/canvas/canvas';
3
- export { CanvasComponent } from './components/canvas/component';
4
- export { Draggable, DraggableImage } from './components/canvas/draggable';
5
- export { CanvasWrapper, growTransition } from './components/canvas/wrapper';
6
- export { default as CanvasToolbar } from './components/canvas/toolbar';
7
- export { default as CanvasNavbar } from './components/canvas/navbar';
2
+ export { default as Canvas } from "./components/canvas/canvas";
3
+ export { CanvasComponent } from "./components/canvas/component";
4
+ export { Draggable, DraggableImage } from "./components/canvas/draggable";
5
+ export { CanvasWrapper, growTransition } from "./components/canvas/wrapper";
6
+ export { default as CanvasToolbar } from "./components/canvas/toolbar";
7
+ export { default as CanvasNavbar } from "./components/canvas/navbar";
8
8
 
9
9
  // Background Components
10
10
  export {
11
- DefaultCanvasBackground,
12
- DefaultWrapperBackground,
13
- DefaultIntroContent,
14
- DEFAULT_CANVAS_GRADIENT,
15
- DEFAULT_INTRO_GRADIENT,
16
- DEFAULT_CANVAS_BOX_GRADIENT,
17
- } from './components/canvas/backgrounds';
11
+ DefaultCanvasBackground,
12
+ DefaultWrapperBackground,
13
+ DefaultIntroContent,
14
+ DEFAULT_CANVAS_GRADIENT,
15
+ DEFAULT_INTRO_GRADIENT,
16
+ DEFAULT_CANVAS_BOX_GRADIENT,
17
+ } from "./components/canvas/backgrounds";
18
18
  export type {
19
- DefaultCanvasBackgroundProps,
20
- DefaultWrapperBackgroundProps,
21
- DefaultIntroContentProps,
22
- } from './components/canvas/backgrounds';
19
+ DefaultCanvasBackgroundProps,
20
+ DefaultWrapperBackgroundProps,
21
+ DefaultIntroContentProps,
22
+ } from "./components/canvas/backgrounds";
23
23
 
24
24
  // Contexts
25
- export { CanvasContext, CanvasProvider, useCanvasContext } from './contexts/CanvasContext';
26
- export type { CanvasContextState } from './contexts/CanvasContext';
27
- export { PerformanceProvider, usePerformanceMode, usePerformance } from './contexts/PerformanceContext';
28
- export type { PerformanceMode, PerformanceConfig } from './contexts/PerformanceContext';
25
+ export {
26
+ CanvasContext,
27
+ CanvasProvider,
28
+ useCanvasContext,
29
+ } from "./contexts/CanvasContext";
30
+ export type { CanvasContextState } from "./contexts/CanvasContext";
31
+ export {
32
+ PerformanceProvider,
33
+ usePerformanceMode,
34
+ usePerformance,
35
+ } from "./contexts/PerformanceContext";
36
+ export type {
37
+ PerformanceMode,
38
+ PerformanceConfig,
39
+ } from "./contexts/PerformanceContext";
29
40
 
30
41
  // Hooks
31
- export { default as useWindowDimensions } from './hooks/useWindowDimensions';
32
- export { usePerformanceMode as usePerformanceModeLegacy } from './hooks/usePerformanceMode';
42
+ export { default as useWindowDimensions } from "./hooks/useWindowDimensions";
43
+ export { usePerformanceMode as usePerformanceModeLegacy } from "./hooks/usePerformanceMode";
33
44
 
34
45
  // Utilities
35
- export * from './lib/canvas';
36
- export * from './lib/constants';
37
- export * from './lib/utils';
38
- export * from './utils/performance';
46
+ export * from "./lib/canvas";
47
+ export * from "./lib/constants";
48
+ export * from "./lib/utils";
49
+ export * from "./utils/performance";
39
50
 
40
51
  // Types
41
- export type { SectionCoordinates, NavItem, CanvasSection } from './types';
52
+ export type {
53
+ SectionCoordinates,
54
+ NavItem,
55
+ CanvasSection,
56
+ ToolbarConfig,
57
+ ToolbarPosition,
58
+ ToolbarDisplayMode,
59
+ } from "./types";
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
- CANVAS_WIDTH,
5
- CANVAS_HEIGHT,
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 = CANVAS_WIDTH;
13
- export const canvasHeight = CANVAS_HEIGHT;
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";
@@ -24,10 +24,10 @@ export enum ScreenSizeEnum {
24
24
  // ============================================================================
25
25
 
26
26
  /** Default canvas width in pixels */
27
- export const CANVAS_WIDTH = 6000;
27
+ export const DEFAULT_CANVAS_WIDTH = 6000;
28
28
 
29
29
  /** Default canvas height in pixels */
30
- export const CANVAS_HEIGHT = 4000;
30
+ export const DEFAULT_CANVAS_HEIGHT = 4000;
31
31
 
32
32
  // ============================================================================
33
33
  // INTRO ANIMATION
@@ -38,3 +38,62 @@ export interface NavItem {
38
38
  /** If true, clicking this section triggers the reset/home behavior */
39
39
  isHome?: boolean;
40
40
  }
41
+
42
+ /**
43
+ * Preset positions for the toolbar
44
+ */
45
+ export type ToolbarPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
46
+
47
+ /**
48
+ * What to display in the toolbar
49
+ */
50
+ export type ToolbarDisplayMode = 'coordinates' | 'scale' | 'both';
51
+
52
+ /**
53
+ * Configuration options for the canvas toolbar
54
+ */
55
+ export interface ToolbarConfig {
56
+ // === Visibility ===
57
+ /** Hide the toolbar entirely. Default: false */
58
+ hidden?: boolean;
59
+
60
+ // === Display Mode ===
61
+ /** What to show: 'coordinates', 'scale', or 'both'. Default: 'both' */
62
+ display?: ToolbarDisplayMode;
63
+
64
+ // === Positioning ===
65
+ /** Preset position. Default: 'top-left' */
66
+ position?: ToolbarPosition;
67
+
68
+ // === Auto-hide Behavior ===
69
+ /** Disable auto-hide when at home position. Default: false */
70
+ disableAutoHide?: boolean;
71
+
72
+ // === Styling (Tailwind-friendly) ===
73
+ /** Additional className for the container */
74
+ className?: string;
75
+ /** Additional className for the coordinates text */
76
+ coordinatesClassName?: string;
77
+ /** Additional className for the scale text */
78
+ scaleClassName?: string;
79
+ /** Additional className for the separator */
80
+ separatorClassName?: string;
81
+
82
+ // === Styling (non-Tailwind / inline styles) ===
83
+ /** Inline styles for the container */
84
+ style?: React.CSSProperties;
85
+ /** Inline styles for the coordinates */
86
+ coordinatesStyle?: React.CSSProperties;
87
+ /** Inline styles for the scale */
88
+ scaleStyle?: React.CSSProperties;
89
+
90
+ // === Content Customization ===
91
+ /** Custom separator between coordinates and scale. Default: ' | ' */
92
+ separator?: string;
93
+ /** Gap around the separator in pixels or CSS value. Default: undefined (uses inline spacing) */
94
+ separatorGap?: number | string;
95
+ /** Format for coordinates. Default: '(x, y)' */
96
+ coordinatesFormat?: (x: number, y: number) => string;
97
+ /** Format for scale. Default: '1.00x' */
98
+ scaleFormat?: (scale: number) => string;
99
+ }