diff --git a/rsconcept/frontend/src/components/flow/use-continous-panning.tsx b/rsconcept/frontend/src/components/flow/use-continous-panning.tsx new file mode 100644 index 00000000..9443580c --- /dev/null +++ b/rsconcept/frontend/src/components/flow/use-continous-panning.tsx @@ -0,0 +1,79 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { useReactFlow } from 'reactflow'; + +interface PanOptions { + panSpeed: number; +} + +export function useContinuousPan( + ref: React.RefObject, + options: PanOptions = { + panSpeed: 15 + } +) { + const { getViewport, setViewport } = useReactFlow(); + + const keysPressed = useRef>(new Set()); + const rafRef = useRef(null); + + const panLoop = useCallback(() => { + const viewport = getViewport(); + let { x, y } = viewport; + + if (keysPressed.current.has('KeyW')) y += options.panSpeed; + if (keysPressed.current.has('KeyS')) y -= options.panSpeed; + if (keysPressed.current.has('KeyA')) x += options.panSpeed; + if (keysPressed.current.has('KeyD')) x -= options.panSpeed; + + setViewport({ x, y, zoom: viewport.zoom }, { duration: 0 }); + // eslint-disable-next-line react-hooks/immutability + rafRef.current = requestAnimationFrame(() => panLoop()); + }, [options.panSpeed, getViewport, setViewport]); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return; + if (!['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) return; + + event.preventDefault(); + event.stopPropagation(); + keysPressed.current.add(event.code); + if (rafRef.current === null) { + rafRef.current = requestAnimationFrame(panLoop); + } + }; + + const handleKeyUp = (event: KeyboardEvent) => { + keysPressed.current.delete(event.code); + if (keysPressed.current.size === 0 && rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + }; + + const handleBlur = () => { + keysPressed.current.clear(); + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + }; + + element.addEventListener('keydown', handleKeyDown, { passive: false }); + element.addEventListener('keyup', handleKeyUp); + element.addEventListener('blur', handleBlur); + + return () => { + element.removeEventListener('keydown', handleKeyDown); + element.removeEventListener('keyup', handleKeyUp); + element.removeEventListener('blur', handleBlur); + + if (rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [ref, panLoop]); +} diff --git a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx index 14f0d1d6..27b3a93e 100644 --- a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx +++ b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx @@ -1,3 +1,7 @@ +import { IconEdgeType } from '@/features/rsform/components/icon-edge-type'; +import { IconGraphMode } from '@/features/rsform/components/icon-graph-mode'; +import { InteractionMode, TGEdgeType } from '@/features/rsform/stores/term-graph'; + import { Divider } from '@/components/container'; import { IconChild, @@ -80,6 +84,9 @@ export function HelpRSGraphTerm() {
  • Space – перемещение экрана
  • +
  • + WASD - направленное перемещение +
  • переход к связанной
  • @@ -96,6 +103,18 @@ export function HelpRSGraphTerm() { Открыть{' '} +
  • + Просмотр графа +
  • +
  • + Редактирование связей +
  • +
  • + Атрибутирование +
  • +
  • + Определение +
  • diff --git a/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts b/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts index ff15380e..a14f5c75 100644 --- a/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts +++ b/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts @@ -22,7 +22,6 @@ export class RSFormLoader { private schema: IRSForm; private graph: Graph = new Graph(); private association_graph: Graph = new Graph(); - private full_graph: Graph = new Graph(); private cstByAlias = new Map(); private cstByID = new Map(); @@ -42,7 +41,6 @@ export class RSFormLoader { result.cstByAlias = this.cstByAlias; result.cstByID = this.cstByID; result.attribution_graph = this.association_graph; - result.full_graph = this.full_graph; return result; } @@ -52,7 +50,6 @@ export class RSFormLoader { this.cstByID.set(cst.id, cst); this.graph.addNode(cst.id); this.association_graph.addNode(cst.id); - this.full_graph.addNode(cst.id); }); } @@ -63,7 +60,6 @@ export class RSFormLoader { const source = this.cstByAlias.get(alias); if (source) { this.graph.addEdge(source.id, cst.id); - this.full_graph.addEdge(source.id, cst.id); } }); }); @@ -113,9 +109,6 @@ export class RSFormLoader { this.schema.attribution.forEach(attrib => { const container = this.cstByID.get(attrib.container)!; container.attributes.push(attrib.attribute); - if (!this.full_graph.hasEdge(attrib.attribute, attrib.container)) { - this.full_graph.addEdge(attrib.container, attrib.attribute); - } this.association_graph.addEdge(attrib.container, attrib.attribute); }); } diff --git a/rsconcept/frontend/src/features/rsform/colors.ts b/rsconcept/frontend/src/features/rsform/colors.ts index 9e6c96b5..798bf5ef 100644 --- a/rsconcept/frontend/src/features/rsform/colors.ts +++ b/rsconcept/frontend/src/features/rsform/colors.ts @@ -200,7 +200,7 @@ export function colorBgCstClass(cstClass: CstClass): string { case CstClass.BASIC: return APP_COLORS.bgGreen; case CstClass.DERIVED: return APP_COLORS.bgBlue; case CstClass.STATEMENT: return APP_COLORS.bgRed; - case CstClass.TEMPLATE: return APP_COLORS.bgTeal; + case CstClass.TEMPLATE: return APP_COLORS.bgPurple; } } diff --git a/rsconcept/frontend/src/features/rsform/components/term-graph/graph/tg-edge.tsx b/rsconcept/frontend/src/features/rsform/components/term-graph/graph/tg-edge.tsx index 714419e3..23d90e34 100644 --- a/rsconcept/frontend/src/features/rsform/components/term-graph/graph/tg-edge.tsx +++ b/rsconcept/frontend/src/features/rsform/components/term-graph/graph/tg-edge.tsx @@ -29,6 +29,7 @@ export function TermGraphEdge({ id, markerEnd, style, ...props }: EdgeProps) { return ( <> + ); } diff --git a/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx b/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx index 2233fcbb..981980b6 100644 --- a/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx +++ b/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx @@ -18,6 +18,7 @@ import { import { type Styling } from '@/components/props'; import { cn } from '@/components/utils'; import { type Graph } from '@/models/graph'; +import { prepareTooltip } from '@/utils/utils'; interface ToolbarGraphSelectionProps extends Styling { value: number[]; @@ -26,12 +27,14 @@ interface ToolbarGraphSelectionProps extends Styling { isCore: (item: number) => boolean; isCrucial: (item: number) => boolean; isInherited: (item: number) => boolean; + tipHotkeys?: boolean; } export function ToolbarGraphSelection({ className, graph, value, + tipHotkeys, isCore, isInherited, isCrucial, @@ -110,7 +113,8 @@ export function ToolbarGraphSelection({ return (
    } onClick={handleSelectReset} disabled={emptySelection} @@ -127,14 +131,16 @@ export function ToolbarGraphSelection({ } onClick={handleExpandInputs} disabled={emptySelection} /> } onClick={handleExpandOutputs} disabled={emptySelection} @@ -142,14 +148,16 @@ export function ToolbarGraphSelection({ } onClick={handleSelectAllInputs} disabled={emptySelection} /> } onClick={handleSelectAllOutputs} disabled={emptySelection} @@ -157,7 +165,14 @@ export function ToolbarGraphSelection({ Максимизация
    дополнение выделения конституентами,
    зависимыми только от выделенных' + : prepareTooltip( + 'Максимизация - дополнение выделения конституентами, зависимыми только от выделенных', + '5' + ) + } aria-label='Максимизация - дополнение выделения конституентами, зависимыми только от выделенных' icon={} onClick={handleSelectMaximize} @@ -165,6 +180,7 @@ export function ToolbarGraphSelection({ /> } onClick={handleSelectInvert} /> @@ -181,25 +197,29 @@ export function ToolbarGraphSelection({ } onClick={handleSelectCore} /> } onClick={handleSelectCrucial} /> } onClick={handleSelectOwned} /> } onClick={handleSelectInherited} /> diff --git a/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx b/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx index 9b026dd2..2f1cf2cd 100644 --- a/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx +++ b/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx @@ -5,6 +5,7 @@ import { Controller, useForm } from 'react-hook-form'; import { MiniButton } from '@/components/control'; import { Checkbox } from '@/components/input'; import { ModalForm } from '@/components/modal'; +import { prepareTooltip } from '@/utils/utils'; import { CstType } from '../backend/types'; import { IconCstType } from '../components/icon-cst-type'; @@ -35,19 +36,31 @@ export function DlgGraphParams() { } + render={({ field }) => ( + + )} /> ( - + )} /> } + render={({ field }) => ( + + )} />
    -
    +

    Типы конституент

    -
    +
    {Object.values(CstType).map(cstType => { const fieldName = cstTypeToFilterKey[cstType]; return ( diff --git a/rsconcept/frontend/src/features/rsform/models/rsform-api.ts b/rsconcept/frontend/src/features/rsform/models/rsform-api.ts index ddbd3faa..0609396f 100644 --- a/rsconcept/frontend/src/features/rsform/models/rsform-api.ts +++ b/rsconcept/frontend/src/features/rsform/models/rsform-api.ts @@ -328,7 +328,8 @@ export function sortItemsForInlineSynthesis(receiver: IRSForm, items: readonly I /** Remove alias from expression. */ export function removeAliasReference(expression: string, alias: string): string { - return expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL'); + const result = expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL'); + return result === 'DEL' ? '' : result; } /** Add alias to expression. */ diff --git a/rsconcept/frontend/src/features/rsform/models/rsform.ts b/rsconcept/frontend/src/features/rsform/models/rsform.ts index 9dbfe9b2..a12907fc 100644 --- a/rsconcept/frontend/src/features/rsform/models/rsform.ts +++ b/rsconcept/frontend/src/features/rsform/models/rsform.ts @@ -148,7 +148,6 @@ export interface IRSForm extends ILibraryItemData { stats: IRSFormStats; graph: Graph; attribution_graph: Graph; - full_graph: Graph; cstByAlias: Map; cstByID: Map; } diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx index 7c0f5bf3..e040b743 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx @@ -15,13 +15,14 @@ import { import clsx from 'clsx'; import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; +import { useContinuousPan } from '@/components/flow/use-continous-panning'; import { useWindowSize } from '@/hooks/use-window-size'; import { useFitHeight, useMainHeight } from '@/stores/app-layout'; import { PARAMETER } from '@/utils/constants'; import { errorMsg } from '@/utils/labels'; import { withPreventDefault } from '@/utils/utils'; -import { ParsingStatus } from '../../../backend/types'; +import { CstType, ParsingStatus } from '../../../backend/types'; import { useCreateAttribution } from '../../../backend/use-create-attribution'; import { useMutatingRSForm } from '../../../backend/use-mutating-rsform'; import { useUpdateConstituenta } from '../../../backend/use-update-constituenta'; @@ -33,7 +34,7 @@ import { SelectColoring } from '../../../components/term-graph/select-coloring'; import { SelectEdgeType } from '../../../components/term-graph/select-edge-type'; import { ViewHidden } from '../../../components/term-graph/view-hidden'; import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api'; -import { addAliasReference } from '../../../models/rsform-api'; +import { addAliasReference, isBasicConcept } from '../../../models/rsform-api'; import { InteractionMode, TGEdgeType, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph'; import { useRSEdit } from '../rsedit-context'; @@ -55,6 +56,9 @@ export function TGFlow() { const { isSmall } = useWindowSize(); const mainHeight = useMainHeight(); const { fitView, viewportInitialized } = useReactFlow(); + const flowRef = useRef(null); + + useContinuousPan(flowRef); const mode = useTermGraphStore(state => state.mode); const toggleMode = useTermGraphStore(state => state.toggleMode); @@ -62,6 +66,8 @@ export function TGFlow() { const setConnectionStart = useTGConnectionStore(state => state.setStart); const connectionType = useTGConnectionStore(state => state.connectionType); const toggleText = useTermGraphStore(state => state.toggleText); + const toggleClustering = useTermGraphStore(state => state.toggleClustering); + const toggleHermits = useTermGraphStore(state => state.toggleHermits); const { createAttribution } = useCreateAttribution(); const { updateConstituenta } = useUpdateConstituenta(); @@ -79,7 +85,8 @@ export function TGFlow() { navigateCst, selectedEdges, setSelectedEdges, - deselectAll + deselectAll, + createCst } = useRSEdit(); const [nodes, setNodes, onNodesChange] = useNodesState([]); @@ -90,14 +97,9 @@ export function TGFlow() { const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem'); function onSelectionChange({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) { - if (mode === InteractionMode.explore) { - const ids = nodes.map(node => Number(node.id)); - setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]); - setSelectedEdges([]); - } else { - setSelectedCst([]); - setSelectedEdges(edges.map(edge => edge.id)); - } + const ids = nodes.map(node => Number(node.id)); + setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]); + setSelectedEdges(edges.map(edge => edge.id)); } useOnSelectionChange({ onChange: onSelectionChange @@ -294,14 +296,118 @@ export function TGFlow() { deselectAll(); } + function handleCreateCst() { + const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' '); + createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition); + } + + function handleSelectCore() { + const isCore = (cstID: number) => { + const cst = schema.cstByID.get(cstID); + return !!cst && isBasicConcept(cst.cst_type); + }; + const core = [...filteredGraph.nodes.keys()].filter(isCore); + setSelectedCst([...core, ...filteredGraph.expandInputs(core)]); + } + + function handleSelectOwned() { + setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => !schema.cstByID.get(cstID)?.is_inherited)); + } + + function handleSelectInherited() { + setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.is_inherited ?? false)); + } + + function handleSelectCrucial() { + setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.crucial ?? false)); + } + + function handleExpandOutputs() { + setSelectedCst(prev => [...prev, ...filteredGraph.expandOutputs(prev)]); + } + + function handleExpandInputs() { + setSelectedCst(prev => [...prev, ...filteredGraph.expandInputs(prev)]); + } + + function handleSelectMaximize() { + setSelectedCst(prev => filteredGraph.maximizePart(prev)); + } + + function handleSelectInvert() { + setSelectedCst(prev => [...filteredGraph.nodes.keys()].filter(item => !prev.includes(item))); + } + + function handleSelectAllInputs() { + setSelectedCst(prev => [...prev, ...filteredGraph.expandAllInputs(prev)]); + } + + function handleSelectAllOutputs() { + setSelectedCst(prev => [...prev, ...filteredGraph.expandAllOutputs(prev)]); + } + + function handleSelectionHotkey(eventCode: string): boolean { + if (eventCode === 'Escape') { + setFocus(null); + return true; + } + if (eventCode === 'Digit1') { + handleExpandInputs(); + return true; + } + if (eventCode === 'Digit2') { + handleExpandOutputs(); + return true; + } + if (eventCode === 'Digit3') { + handleSelectAllInputs(); + return true; + } + if (eventCode === 'Digit4') { + handleSelectAllOutputs(); + return true; + } + if (eventCode === 'Digit5') { + handleSelectMaximize(); + return true; + } + if (eventCode === 'Digit6') { + handleSelectInvert(); + return true; + } + if (eventCode === 'KeyZ') { + handleSelectCore(); + return true; + } + if (eventCode === 'KeyX') { + handleSelectCrucial(); + return true; + } + if (eventCode === 'KeyC') { + handleSelectOwned(); + return true; + } + if (eventCode === 'KeyY') { + handleSelectInherited(); + return true; + } + + return false; + } + function handleKeyDown(event: React.KeyboardEvent) { if (isProcessing) { return; } - if (event.code === 'Escape') { - withPreventDefault(() => setFocus(null))(event); + if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) { return; } + if (handleSelectionHotkey(event.code)) { + event.preventDefault(); + event.stopPropagation(); + return; + } + if (event.code === 'KeyG') { withPreventDefault(() => fitView(flowOptions.fitViewOptions))(event); return; @@ -310,8 +416,16 @@ export function TGFlow() { withPreventDefault(toggleText)(event); return; } + if (event.code === 'KeyV') { + withPreventDefault(toggleClustering)(event); + return; + } + if (event.code === 'KeyB') { + withPreventDefault(toggleHermits)(event); + return; + } - if (isContentEditable && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + if (isContentEditable) { if (event.code === 'KeyQ') { withPreventDefault(handleToggleMode)(event); return; @@ -320,7 +434,11 @@ export function TGFlow() { withPreventDefault(toggleEdgeType)(event); return; } - if (event.code === 'Delete') { + if (event.code === 'KeyR') { + withPreventDefault(handleCreateCst)(event); + return; + } + if (event.code === 'Delete' || event.code === 'Backquote') { withPreventDefault(handleDeleteSelected)(event); return; } @@ -329,6 +447,7 @@ export function TGFlow() { return (
    @@ -174,6 +174,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra {focusCst ? setFocus(null)} /> : null} {!focusCst && mode === InteractionMode.explore ? ( { const cst = schema.cstByID.get(cstID); @@ -203,7 +204,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra ) : null} {isContentEditable ? ( } onClick={handleCreateCst} disabled={isProcessing} @@ -211,7 +212,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra ) : null} {isContentEditable ? ( } onClick={onDeleteSelected} disabled={!canDeleteSelected || isProcessing} diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx index 3ecdd8d5..bfb1ea95 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx @@ -26,7 +26,6 @@ import { useRSFormSuspense } from '../../backend/use-rsform'; import { useUpdateConstituenta } from '../../backend/use-update-constituenta'; import { type IConstituenta } from '../../models/rsform'; import { generateAlias, removeAliasReference } from '../../models/rsform-api'; -import { InteractionMode, useTermGraphStore } from '../../stores/term-graph'; import { RSEditContext, RSTabID } from './rsedit-context'; @@ -58,13 +57,12 @@ export const RSEditState = ({ const isContentEditable = isMutable && !isArchive; const isAttachedToOSS = schema.oss.length > 0; const isEditor = !!user.id && schema.editors.includes(user.id); - const mode = useTermGraphStore(state => state.mode); const [selectedCst, setSelectedCst] = useState([]); const [selectedEdges, setSelectedEdges] = useState([]); const canDeleteSelected = (selectedCst.length > 0 && selectedCst.every(id => !schema.cstByID.get(id)?.is_inherited)) || - (selectedEdges.length === 1 && mode === InteractionMode.edit); + (selectedCst.length === 0 && selectedEdges.length === 1); const [focusCst, setFocusCst] = useState(null); const activeCst = selectedCst.length === 0 ? null : schema.cstByID.get(selectedCst[selectedCst.length - 1])!; @@ -268,9 +266,9 @@ export const RSEditState = ({ if (!canDeleteSelected) { return; } - if (mode === InteractionMode.explore || selectedEdges.length === 0) { + if (selectedCst.length > 0) { deleteSelectedCst(); - } else { + } else if (selectedEdges.length === 1) { deleteSelectedEdge(); } } diff --git a/rsconcept/frontend/src/features/rsform/stores/term-graph.ts b/rsconcept/frontend/src/features/rsform/stores/term-graph.ts index 303c70d8..04a8b1ac 100644 --- a/rsconcept/frontend/src/features/rsform/stores/term-graph.ts +++ b/rsconcept/frontend/src/features/rsform/stores/term-graph.ts @@ -72,6 +72,7 @@ interface TermGraphStore { toggleText: () => void; toggleClustering: () => void; toggleGraphType: () => void; + toggleHermits: () => void; foldHidden: boolean; toggleFoldHidden: () => void; @@ -125,6 +126,7 @@ export const useTermGraphStore = create()( toggleFocusOutputs: () => set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })), toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })), + toggleHermits: () => set(state => ({ filter: { ...state.filter, noHermits: !state.filter.noHermits } })), toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })), toggleGraphType: () => set(state => ({ diff --git a/rsconcept/frontend/src/styling/reactflow.css b/rsconcept/frontend/src/styling/reactflow.css index bc8e7c9f..d90c6f9d 100644 --- a/rsconcept/frontend/src/styling/reactflow.css +++ b/rsconcept/frontend/src/styling/reactflow.css @@ -84,11 +84,26 @@ cursor: inherit; &.selected { - filter: drop-shadow(0 0 4px var(--color-primary)); + filter: drop-shadow(0 0 4px var(--color-primary)) drop-shadow(0 0 6px var(--color-primary)); + + .dark & { + filter: drop-shadow(0 0 4px var(--color-accent-orange-foreground)) + drop-shadow(0 0 6px var(--color-accent-orange-foreground)); + } } - .mode-edit & { - cursor: pointer; + .mode-space & { + pointer-events: none; + } +} + +.rf-edge-events { + pointer-events: stroke; + stroke-width: 6; + stroke: transparent; + + .mode-space & { + pointer-events: none; } }