@tscircuit/schematic-viewer 2.0.44 → 2.0.46

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.
@@ -34,6 +34,8 @@ export interface MouseTrackerContextValue {
34
34
  export const MouseTrackerContext =
35
35
  createContext<MouseTrackerContextValue | null>(null)
36
36
 
37
+ const DRAG_THRESHOLD_PX = 5
38
+
37
39
  const boundsAreEqual = (
38
40
  a: BoundingBoxBounds | null | undefined,
39
41
  b: BoundingBoxBounds | null | undefined,
@@ -60,6 +62,7 @@ export const MouseTracker = ({ children }: { children: ReactNode }) => {
60
62
  boundingBoxes: new Map<string, BoundingBoxRegistration>(),
61
63
  hoveringIds: new Set<string>(),
62
64
  subscribers: new Set<() => void>(),
65
+ mouseDownPosition: null as { x: number; y: number } | null,
63
66
  })
64
67
 
65
68
  const notifySubscribers = useCallback(() => {
@@ -158,11 +161,36 @@ export const MouseTracker = ({ children }: { children: ReactNode }) => {
158
161
  const handlePointerLeave = () => {
159
162
  if (storeRef.current.pointer === null) return
160
163
  storeRef.current.pointer = null
164
+ storeRef.current.mouseDownPosition = null
161
165
  updateHovering()
162
166
  }
163
167
 
168
+ const handleMouseDown = (event: MouseEvent) => {
169
+ storeRef.current.mouseDownPosition = {
170
+ x: event.clientX,
171
+ y: event.clientY,
172
+ }
173
+ }
174
+
164
175
  const handleClick = (event: MouseEvent) => {
165
176
  const { clientX, clientY } = event
177
+ const mouseDownPos = storeRef.current.mouseDownPosition
178
+
179
+ // Check if this was a drag (movement > threshold)
180
+ if (mouseDownPos) {
181
+ const distance = Math.sqrt(
182
+ Math.pow(clientX - mouseDownPos.x, 2) +
183
+ Math.pow(clientY - mouseDownPos.y, 2),
184
+ )
185
+ if (distance > DRAG_THRESHOLD_PX) {
186
+ // This was a drag, not a click - don't trigger onClick
187
+ storeRef.current.mouseDownPosition = null
188
+ return
189
+ }
190
+ }
191
+
192
+ storeRef.current.mouseDownPosition = null
193
+
166
194
  for (const registration of storeRef.current.boundingBoxes.values()) {
167
195
  const bounds = registration.bounds
168
196
  if (!bounds) continue
@@ -189,6 +217,7 @@ export const MouseTracker = ({ children }: { children: ReactNode }) => {
189
217
  window.addEventListener("pointerleave", handlePointerLeave)
190
218
  window.addEventListener("pointercancel", handlePointerLeave)
191
219
  window.addEventListener("blur", handlePointerLeave)
220
+ window.addEventListener("mousedown", handleMouseDown, { passive: true })
192
221
  window.addEventListener("click", handleClick, { passive: true })
193
222
 
194
223
  return () => {
@@ -198,6 +227,7 @@ export const MouseTracker = ({ children }: { children: ReactNode }) => {
198
227
  window.removeEventListener("pointerleave", handlePointerLeave)
199
228
  window.removeEventListener("pointercancel", handlePointerLeave)
200
229
  window.removeEventListener("blur", handlePointerLeave)
230
+ window.removeEventListener("mousedown", handleMouseDown)
201
231
  window.removeEventListener("click", handleClick)
202
232
  }
203
233
  }, [updateHovering])
@@ -35,6 +35,7 @@ interface Props {
35
35
  svgDivRef: React.RefObject<HTMLDivElement | null>
36
36
  containerRef: React.RefObject<HTMLDivElement | null>
37
37
  onComponentClick?: (componentId: string, event: MouseEvent) => void
38
+ onHoverChange?: (componentId: string, isHovering: boolean) => void
38
39
  showOutline: boolean
39
40
  circuitJsonKey: string
40
41
  }
@@ -44,6 +45,7 @@ export const SchematicComponentMouseTarget = ({
44
45
  svgDivRef,
45
46
  containerRef,
46
47
  onComponentClick,
48
+ onHoverChange,
47
49
  showOutline,
48
50
  circuitJsonKey,
49
51
  }: Props) => {
@@ -157,6 +159,13 @@ export const SchematicComponentMouseTarget = ({
157
159
  onClick: onComponentClick ? handleClick : undefined,
158
160
  })
159
161
 
162
+ // Notify parent of hover state changes
163
+ useEffect(() => {
164
+ if (onHoverChange) {
165
+ onHoverChange(componentId, hovering)
166
+ }
167
+ }, [hovering, componentId, onHoverChange])
168
+
160
169
  if (!measurement || !hovering || !showOutline) {
161
170
  return null
162
171
  }
@@ -172,10 +181,8 @@ export const SchematicComponentMouseTarget = ({
172
181
  width: rect.width,
173
182
  height: rect.height,
174
183
  border: "1.5px solid rgba(51, 153, 255, 0.9)",
175
- borderRadius: "6px",
176
184
  pointerEvents: "none",
177
185
  zIndex: zIndexMap.schematicComponentHoverOutline,
178
- boxShadow: "0 0 6px rgba(51, 153, 255, 0.35)",
179
186
  }}
180
187
  />
181
188
  )
@@ -7,7 +7,7 @@ import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSc
7
7
  import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
8
8
  import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
9
9
  import { enableDebug } from "lib/utils/debug"
10
- import { useEffect, useMemo, useRef, useState } from "react"
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
11
11
  import {
12
12
  fromString,
13
13
  identity,
@@ -123,6 +123,21 @@ export const SchematicViewer = ({
123
123
  if (disableGroups) return false
124
124
  return getStoredBoolean("schematic_viewer_show_groups", false)
125
125
  })
126
+ const [isHoveringClickableComponent, setIsHoveringClickableComponent] =
127
+ useState(false)
128
+ const hoveringComponentsRef = useRef<Set<string>>(new Set())
129
+
130
+ const handleComponentHoverChange = useCallback(
131
+ (componentId: string, isHovering: boolean) => {
132
+ if (isHovering) {
133
+ hoveringComponentsRef.current.add(componentId)
134
+ } else {
135
+ hoveringComponentsRef.current.delete(componentId)
136
+ }
137
+ setIsHoveringClickableComponent(hoveringComponentsRef.current.size > 0)
138
+ },
139
+ [],
140
+ )
126
141
  const svgDivRef = useRef<HTMLDivElement>(null)
127
142
  const touchStartRef = useRef<{ x: number; y: number } | null>(null)
128
143
 
@@ -297,6 +312,11 @@ export const SchematicViewer = ({
297
312
  : "auto",
298
313
  transformOrigin: "0 0",
299
314
  }}
315
+ className={
316
+ onSchematicComponentClicked
317
+ ? "schematic-component-clickable"
318
+ : undefined
319
+ }
300
320
  onTouchStart={(e) => {
301
321
  if (editModeEnabled && isInteractionEnabled && !showSpiceOverlay) {
302
322
  handleComponentTouchStartRef.current(e)
@@ -317,6 +337,11 @@ export const SchematicViewer = ({
317
337
 
318
338
  return (
319
339
  <MouseTracker>
340
+ {onSchematicComponentClicked && (
341
+ <style>
342
+ {`.schematic-component-clickable [data-schematic-component-id]:hover { cursor: pointer !important; }`}
343
+ </style>
344
+ )}
320
345
  <div
321
346
  ref={containerRef}
322
347
  style={{
@@ -329,7 +354,9 @@ export const SchematicViewer = ({
329
354
  ? "grabbing"
330
355
  : clickToInteractEnabled && !isInteractionEnabled
331
356
  ? "pointer"
332
- : "grab",
357
+ : isHoveringClickableComponent && onSchematicComponentClicked
358
+ ? "pointer"
359
+ : "grab",
333
360
  minHeight: "300px",
334
361
  ...containerStyle,
335
362
  }}
@@ -456,6 +483,7 @@ export const SchematicViewer = ({
456
483
  containerRef={containerRef}
457
484
  showOutline={true}
458
485
  circuitJsonKey={circuitJsonKey}
486
+ onHoverChange={handleComponentHoverChange}
459
487
  onComponentClick={(id, event) => {
460
488
  onSchematicComponentClicked?.({
461
489
  schematicComponentId: id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {