@tscircuit/schematic-viewer 2.0.33 → 2.0.35

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.
@@ -4,9 +4,15 @@ export const EditIcon = ({
4
4
  onClick,
5
5
  active,
6
6
  }: { onClick: () => void; active: boolean }) => {
7
+ const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
8
+ e.preventDefault()
9
+ onClick()
10
+ }
11
+
7
12
  return (
8
13
  <div
9
- onClick={onClick}
14
+ onClick={handleInteraction}
15
+ onTouchEnd={handleInteraction}
10
16
  style={{
11
17
  position: "absolute",
12
18
  top: "16px",
@@ -4,9 +4,15 @@ export const GridIcon = ({
4
4
  onClick,
5
5
  active,
6
6
  }: { onClick: () => void; active: boolean }) => {
7
+ const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
8
+ e.preventDefault()
9
+ onClick()
10
+ }
11
+
7
12
  return (
8
13
  <div
9
- onClick={onClick}
14
+ onClick={handleInteraction}
15
+ onTouchEnd={handleInteraction}
10
16
  style={{
11
17
  position: "absolute",
12
18
  top: "56px",
@@ -26,6 +26,7 @@ import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay"
26
26
  import { zIndexMap } from "../utils/z-index-map"
27
27
  import { useSpiceSimulation } from "../hooks/useSpiceSimulation"
28
28
  import { getSpiceFromCircuitJson } from "../utils/spice-utils"
29
+ import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
29
30
 
30
31
  interface Props {
31
32
  circuitJson: CircuitJson
@@ -39,6 +40,7 @@ interface Props {
39
40
  clickToInteractEnabled?: boolean
40
41
  colorOverrides?: ColorOverrides
41
42
  spiceSimulationEnabled?: boolean
43
+ disableGroups?: boolean
42
44
  }
43
45
 
44
46
  export const SchematicViewer = ({
@@ -53,6 +55,7 @@ export const SchematicViewer = ({
53
55
  clickToInteractEnabled = false,
54
56
  colorOverrides,
55
57
  spiceSimulationEnabled = false,
58
+ disableGroups = false,
56
59
  }: Props) => {
57
60
  if (debug) {
58
61
  enableDebug()
@@ -102,7 +105,10 @@ export const SchematicViewer = ({
102
105
  !clickToInteractEnabled,
103
106
  )
104
107
  const [showViewMenu, setShowViewMenu] = useState(false)
105
- const [showSchematicGroups, setShowSchematicGroups] = useState(false)
108
+ const [showSchematicGroups, setShowSchematicGroups] = useState(() => {
109
+ if (disableGroups) return false
110
+ return getStoredBoolean("schematic_viewer_show_groups", false)
111
+ })
106
112
  const svgDivRef = useRef<HTMLDivElement>(null)
107
113
  const touchStartRef = useRef<{ x: number; y: number } | null>(null)
108
114
 
@@ -207,18 +213,21 @@ export const SchematicViewer = ({
207
213
  return [...unappliedEditEvents, ...internalEditEvents]
208
214
  }, [unappliedEditEvents, internalEditEvents])
209
215
 
210
- const { handleMouseDown, isDragging, activeEditEvent } = useComponentDragging(
211
- {
212
- onEditEvent: handleEditEvent,
213
- cancelDrag,
214
- realToSvgProjection,
215
- svgToScreenProjection,
216
- circuitJson,
217
- editEvents: editEventsWithUnappliedEditEvents,
218
- enabled: editModeEnabled && isInteractionEnabled && !showSpiceOverlay,
219
- snapToGrid,
220
- },
221
- )
216
+ const {
217
+ handleMouseDown,
218
+ handleTouchStart: handleComponentTouchStart,
219
+ isDragging,
220
+ activeEditEvent,
221
+ } = useComponentDragging({
222
+ onEditEvent: handleEditEvent,
223
+ cancelDrag,
224
+ realToSvgProjection,
225
+ svgToScreenProjection,
226
+ circuitJson,
227
+ editEvents: editEventsWithUnappliedEditEvents,
228
+ enabled: editModeEnabled && isInteractionEnabled && !showSpiceOverlay,
229
+ snapToGrid,
230
+ })
222
231
 
223
232
  useChangeSchematicComponentLocationsInSvg({
224
233
  svgDivRef,
@@ -240,7 +249,7 @@ export const SchematicViewer = ({
240
249
  svgDivRef,
241
250
  circuitJson,
242
251
  circuitJsonKey,
243
- showGroups: showSchematicGroups,
252
+ showGroups: showSchematicGroups && !disableGroups,
244
253
  })
245
254
 
246
255
  const svgDiv = useMemo(
@@ -255,11 +264,23 @@ export const SchematicViewer = ({
255
264
  : "auto",
256
265
  transformOrigin: "0 0",
257
266
  }}
267
+ onTouchStart={(e) => {
268
+ if (editModeEnabled && isInteractionEnabled && !showSpiceOverlay) {
269
+ handleComponentTouchStart(e)
270
+ }
271
+ }}
258
272
  // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
259
273
  dangerouslySetInnerHTML={{ __html: svgString }}
260
274
  />
261
275
  ),
262
- [svgString, isInteractionEnabled, clickToInteractEnabled],
276
+ [
277
+ svgString,
278
+ isInteractionEnabled,
279
+ clickToInteractEnabled,
280
+ editModeEnabled,
281
+ showSpiceOverlay,
282
+ handleComponentTouchStart,
283
+ ],
263
284
  )
264
285
 
265
286
  return (
@@ -367,7 +388,12 @@ export const SchematicViewer = ({
367
388
  isVisible={showViewMenu}
368
389
  onClose={() => setShowViewMenu(false)}
369
390
  showGroups={showSchematicGroups}
370
- onToggleGroups={setShowSchematicGroups}
391
+ onToggleGroups={(value) => {
392
+ if (!disableGroups) {
393
+ setShowSchematicGroups(value)
394
+ setStoredBoolean("schematic_viewer_show_groups", value)
395
+ }
396
+ }}
371
397
  />
372
398
  {spiceSimulationEnabled && (
373
399
  <SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
@@ -32,6 +32,7 @@ export const useComponentDragging = ({
32
32
  snapToGrid?: boolean
33
33
  }): {
34
34
  handleMouseDown: (e: React.MouseEvent) => void
35
+ handleTouchStart: (e: React.TouchEvent) => void
35
36
  isDragging: boolean
36
37
  activeEditEvent: EditSchematicComponentLocationEventWithElement | null
37
38
  } => {
@@ -74,45 +75,37 @@ export const useComponentDragging = ({
74
75
  })
75
76
  }, [editEvents])
76
77
 
77
- const handleMouseDown = useCallback(
78
- (e: React.MouseEvent) => {
79
- if (!enabled) return
78
+ const startDrag = useCallback(
79
+ (clientX: number, clientY: number, target: Element) => {
80
+ if (!enabled) return false
80
81
 
81
- const target = e.target as SVGElement
82
82
  const componentGroup = target.closest(
83
83
  '[data-circuit-json-type="schematic_component"]',
84
84
  )
85
- if (!componentGroup) return
85
+ if (!componentGroup) return false
86
86
 
87
87
  const schematic_component_id = componentGroup.getAttribute(
88
88
  "data-schematic-component-id",
89
89
  )
90
- if (!schematic_component_id) return
90
+ if (!schematic_component_id) return false
91
91
 
92
92
  if (cancelDrag) cancelDrag()
93
93
 
94
94
  const schematic_component = su(circuitJson).schematic_component.get(
95
95
  schematic_component_id,
96
96
  )
97
- if (!schematic_component) return
97
+ if (!schematic_component) return false
98
98
 
99
- dragStartPosRef.current = {
100
- x: e.clientX,
101
- y: e.clientY,
102
- }
99
+ dragStartPosRef.current = { x: clientX, y: clientY }
103
100
 
104
- // Get the current position of the component
105
- // Check if we're already tracking this component
106
101
  let current_position: { x: number; y: number }
107
102
  const trackedPosition = componentPositionsRef.current.get(
108
103
  schematic_component_id,
109
104
  )
110
105
 
111
106
  if (trackedPosition) {
112
- // Use the tracked position from previous edits
113
107
  current_position = { ...trackedPosition }
114
108
  } else {
115
- // Calculate position based on component data and edit events
116
109
  const editEventOffset = getComponentOffsetDueToEvents({
117
110
  editEvents,
118
111
  schematic_component_id: schematic_component_id,
@@ -123,7 +116,6 @@ export const useComponentDragging = ({
123
116
  y: schematic_component.center.y + editEventOffset.y,
124
117
  }
125
118
 
126
- // Store this initial position
127
119
  componentPositionsRef.current.set(schematic_component_id, {
128
120
  ...current_position,
129
121
  })
@@ -142,17 +134,36 @@ export const useComponentDragging = ({
142
134
 
143
135
  activeEditEventRef.current = newEditEvent
144
136
  setActiveEditEvent(newEditEvent)
137
+ return true
145
138
  },
146
139
  [cancelDrag, enabled, circuitJson, editEvents],
147
140
  )
148
141
 
149
- const handleMouseMove = useCallback(
150
- (e: MouseEvent) => {
142
+ const handleMouseDown = useCallback(
143
+ (e: React.MouseEvent) => {
144
+ startDrag(e.clientX, e.clientY, e.target as Element)
145
+ },
146
+ [startDrag],
147
+ )
148
+
149
+ const handleTouchStart = useCallback(
150
+ (e: React.TouchEvent) => {
151
+ if (e.touches.length !== 1) return
152
+ const touch = e.touches[0]
153
+ if (startDrag(touch.clientX, touch.clientY, e.target as Element)) {
154
+ e.preventDefault()
155
+ }
156
+ },
157
+ [startDrag],
158
+ )
159
+
160
+ const updateDragPosition = useCallback(
161
+ (clientX: number, clientY: number) => {
151
162
  if (!activeEditEventRef.current || !dragStartPosRef.current) return
152
163
 
153
164
  const screenDelta = {
154
- x: e.clientX - dragStartPosRef.current.x,
155
- y: e.clientY - dragStartPosRef.current.y,
165
+ x: clientX - dragStartPosRef.current.x,
166
+ y: clientY - dragStartPosRef.current.y,
156
167
  }
157
168
 
158
169
  const mmDelta = {
@@ -180,19 +191,33 @@ export const useComponentDragging = ({
180
191
  [realToScreenProjection, snapToGrid],
181
192
  )
182
193
 
183
- const handleMouseUp = useCallback(() => {
194
+ const handleMouseMove = useCallback(
195
+ (e: MouseEvent) => updateDragPosition(e.clientX, e.clientY),
196
+ [updateDragPosition],
197
+ )
198
+
199
+ const handleTouchMove = useCallback(
200
+ (e: TouchEvent) => {
201
+ if (e.touches.length !== 1 || !activeEditEventRef.current) return
202
+ e.preventDefault()
203
+ const touch = e.touches[0]
204
+ updateDragPosition(touch.clientX, touch.clientY)
205
+ },
206
+ [updateDragPosition],
207
+ )
208
+
209
+ const endDrag = useCallback(() => {
184
210
  if (!activeEditEventRef.current) return
185
211
  const finalEvent = {
186
212
  ...activeEditEventRef.current,
187
213
  in_progress: false,
188
214
  }
189
215
 
190
- // Update our stored position for this component
191
216
  componentPositionsRef.current.set(finalEvent.schematic_component_id, {
192
217
  ...finalEvent.new_center,
193
218
  })
194
219
 
195
- debug("handleMouseUp calling onEditEvent with new edit event", {
220
+ debug("endDrag calling onEditEvent with new edit event", {
196
221
  newEditEvent: finalEvent,
197
222
  })
198
223
  if (onEditEvent) onEditEvent(finalEvent)
@@ -201,17 +226,25 @@ export const useComponentDragging = ({
201
226
  setActiveEditEvent(null)
202
227
  }, [onEditEvent])
203
228
 
229
+ const handleMouseUp = useCallback(() => endDrag(), [endDrag])
230
+ const handleTouchEnd = useCallback(() => endDrag(), [endDrag])
231
+
204
232
  useEffect(() => {
205
233
  window.addEventListener("mousemove", handleMouseMove)
206
234
  window.addEventListener("mouseup", handleMouseUp)
235
+ window.addEventListener("touchmove", handleTouchMove, { passive: false })
236
+ window.addEventListener("touchend", handleTouchEnd)
207
237
  return () => {
208
238
  window.removeEventListener("mousemove", handleMouseMove)
209
239
  window.removeEventListener("mouseup", handleMouseUp)
240
+ window.removeEventListener("touchmove", handleTouchMove)
241
+ window.removeEventListener("touchend", handleTouchEnd)
210
242
  }
211
- }, [handleMouseMove, handleMouseUp])
243
+ }, [handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd])
212
244
 
213
245
  return {
214
246
  handleMouseDown,
247
+ handleTouchStart,
215
248
  isDragging: !!activeEditEventRef.current,
216
249
  activeEditEvent: activeEditEvent,
217
250
  }
@@ -0,0 +1,63 @@
1
+ import { useCallback } from "react"
2
+
3
+ export const STORAGE_KEYS = {
4
+ IS_SHOWING_SCHEMATIC_GROUPS: "schematic_viewer_show_groups",
5
+ } as const
6
+
7
+ export const getStoredBoolean = (
8
+ key: string,
9
+ defaultValue: boolean,
10
+ ): boolean => {
11
+ if (typeof window === "undefined") return defaultValue
12
+ try {
13
+ const stored = localStorage.getItem(key)
14
+ return stored !== null ? JSON.parse(stored) : defaultValue
15
+ } catch {
16
+ return defaultValue
17
+ }
18
+ }
19
+
20
+ export const setStoredBoolean = (key: string, value: boolean): void => {
21
+ if (typeof window === "undefined") return
22
+ try {
23
+ localStorage.setItem(key, JSON.stringify(value))
24
+ } catch {}
25
+ }
26
+
27
+ export const useLocalStorage = () => {
28
+ const getBoolean = useCallback(
29
+ (key: string, defaultValue: boolean): boolean => {
30
+ return getStoredBoolean(key, defaultValue)
31
+ },
32
+ [],
33
+ )
34
+
35
+ const setBoolean = useCallback((key: string, value: boolean): void => {
36
+ setStoredBoolean(key, value)
37
+ }, [])
38
+
39
+ return {
40
+ getBoolean,
41
+ setBoolean,
42
+ }
43
+ }
44
+
45
+ export const useLocalStorageValue = (key: string, defaultValue: boolean) => {
46
+ const { getBoolean, setBoolean } = useLocalStorage()
47
+
48
+ const getValue = useCallback(() => {
49
+ return getBoolean(key, defaultValue)
50
+ }, [getBoolean, key, defaultValue])
51
+
52
+ const setValue = useCallback(
53
+ (value: boolean) => {
54
+ setBoolean(key, value)
55
+ },
56
+ [setBoolean, key],
57
+ )
58
+
59
+ return {
60
+ getValue,
61
+ setValue,
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {