mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:11:45 +03:00
Change grab cursors; fix some bugs (#10215)
Change grab cursors: - Node has grab/grabbed icons for only halo and icon. Fix bugs: - Empty part of top bar no longer blocks mouse events. - JSON viz: Clickable parts of inline elements now use pointer cursor when hovered. - Doc panel breadcrumbs: Icon can be clicked (behavior now consistent with cursor shown). https://github.com/enso-org/enso/assets/1047859/3e48a6c1-3f43-497f-82ad-eb787e9c9643 Closes #10166. # Important Notes - New global `clickable` class replaces `cursor: pointer`; the class can be applied closer to the event handler. - Refactor: Extracted `GraphNode` output port logic to a new component.
This commit is contained in:
parent
5339484285
commit
3e37faa34d
@ -52,7 +52,7 @@ test('Hover behaviour of edges', async ({ page }) => {
|
||||
await expect(edgeElements).toHaveCount(EDGE_PARTS)
|
||||
|
||||
const targetEdge = edgeElements.first()
|
||||
await expect(targetEdge).toHaveClass('edge io')
|
||||
await expect(targetEdge).toHaveClass(/\bio\b/)
|
||||
|
||||
// Hover over edge to the left of node with binding `ten`.
|
||||
await targetEdge.hover({
|
||||
|
@ -84,7 +84,6 @@ registerAutoBlurHandler()
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
font-size: 11.5px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@ -113,3 +113,11 @@
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -7,16 +7,15 @@ const emit = defineEmits<{ click: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Breadcrumb">
|
||||
<div class="Breadcrumb clickable" @click.stop="emit('click')">
|
||||
<SvgIcon v-if="props.icon" :name="props.icon || ''" />
|
||||
<span @click.stop="emit('click')" v-text="props.text"></span>
|
||||
<span v-text="props.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Breadcrumb {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
@ -48,7 +48,11 @@ const annotations = computed<Array<string | undefined>>(() => {
|
||||
<template>
|
||||
<ul v-if="props.items.items.length > 0">
|
||||
<li v-for="(item, index) in props.items.items" :key="index" :class="props.items.kind">
|
||||
<a :class="['link', props.items.kind]" @click.stop="emit('linkClicked', item.id)">
|
||||
<a
|
||||
:class="props.items.kind"
|
||||
class="link clickable"
|
||||
@click.stop="emit('linkClicked', item.id)"
|
||||
>
|
||||
<span class="entryName">{{ qnSplit(item.name)[1] }}</span>
|
||||
<span class="arguments">{{ ' ' + argumentsList(item.arguments) }}</span>
|
||||
</a>
|
||||
@ -61,7 +65,6 @@ const annotations = computed<Array<string | undefined>>(() => {
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
|
@ -571,7 +571,7 @@ const connected = computed(() => isConnected(props.edge))
|
||||
<path
|
||||
v-if="connected"
|
||||
:d="basePath"
|
||||
class="edge io"
|
||||
class="edge io clickable"
|
||||
:data-source-node-id="sourceNode"
|
||||
:data-target-node-id="targetNode"
|
||||
:data-testid="edgeIsBroken ? 'broken-edge' : null"
|
||||
@ -628,7 +628,6 @@ const connected = computed(() => isConnected(props.edge))
|
||||
stroke-width: 14;
|
||||
stroke: transparent;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
}
|
||||
.edge.visible {
|
||||
stroke-width: 4;
|
||||
|
@ -7,6 +7,7 @@ import GraphNodeMessage, {
|
||||
iconForMessageType,
|
||||
type MessageType,
|
||||
} from '@/components/GraphEditor/GraphNodeMessage.vue'
|
||||
import GraphNodeOutputPorts from '@/components/GraphEditor/GraphNodeOutputPorts.vue'
|
||||
import GraphNodeSelection from '@/components/GraphEditor/GraphNodeSelection.vue'
|
||||
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
|
||||
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
|
||||
@ -16,7 +17,6 @@ import NodeWidgetTree, {
|
||||
ICON_WIDTH,
|
||||
} from '@/components/GraphEditor/NodeWidgetTree.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { useApproach } from '@/composables/animation'
|
||||
import { useDoubleClick } from '@/composables/doubleClick'
|
||||
import { usePointer, useResizeObserver } from '@/composables/events'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
@ -32,19 +32,8 @@ import type { Opt } from '@/util/data/opt'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import { displayedIconOf } from '@/util/getIconName'
|
||||
import { setIfUndefined } from 'lib0/map'
|
||||
import type { ExternalId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import type { EffectScope } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
onUnmounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch, watchEffect } from 'vue'
|
||||
|
||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||
const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
||||
@ -91,12 +80,6 @@ const projectStore = useProjectStore()
|
||||
const graph = useGraphStore()
|
||||
const navigator = injectGraphNavigator(true)
|
||||
|
||||
const outputPortsSet = computed(() => {
|
||||
const bindings = graph.db.nodeOutputPorts.lookup(nodeId.value)
|
||||
if (bindings.size === 0) return new Set([nodeId.value])
|
||||
return bindings
|
||||
})
|
||||
|
||||
const nodeId = computed(() => asNodeId(props.node.rootExpr.id))
|
||||
const potentialSelfArgumentId = computed(() => props.node.primarySubject)
|
||||
const connectedSelfArgumentId = computed(() =>
|
||||
@ -244,9 +227,11 @@ const isVisualizationFullscreen = computed(() => props.node.vis?.fullscreen ?? f
|
||||
|
||||
const bgStyleVariables = computed(() => {
|
||||
const { x: width, y: height } = nodeSize.value
|
||||
const visBelowNode = graphSelectionSize.value.y - nodeSize.value.y
|
||||
return {
|
||||
'--node-width': `${width}px`,
|
||||
'--node-height': `${height}px`,
|
||||
'--output-port-transform': `translateY(${visBelowNode}px)`,
|
||||
}
|
||||
})
|
||||
|
||||
@ -375,11 +360,6 @@ function getRelatedSpanOffset(domNode: globalThis.Node, domOffset: number): numb
|
||||
return domOffset
|
||||
}
|
||||
|
||||
const handlePortClick = useDoubleClick(
|
||||
(event: PointerEvent, portId: AstId) => emit('outputPortClick', event, portId),
|
||||
(event: PointerEvent, portId: AstId) => emit('outputPortDoubleClick', event, portId),
|
||||
).handleClick
|
||||
|
||||
const handleNodeClick = useDoubleClick(
|
||||
(e: MouseEvent) => {
|
||||
if (!significantMove.value) {
|
||||
@ -392,78 +372,6 @@ const handleNodeClick = useDoubleClick(
|
||||
},
|
||||
).handleClick
|
||||
|
||||
interface PortData {
|
||||
clipRange: [number, number]
|
||||
label: string | undefined
|
||||
portId: AstId
|
||||
}
|
||||
|
||||
const outputPorts = computed((): PortData[] => {
|
||||
const ports = outputPortsSet.value
|
||||
const numPorts = ports.size
|
||||
return Array.from(ports, (portId, index): PortData => {
|
||||
return {
|
||||
clipRange: [index / numPorts, (index + 1) / numPorts],
|
||||
label: numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) : undefined,
|
||||
portId,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const outputHovered = ref<AstId>()
|
||||
const anyPortDisconnected = computed(() => {
|
||||
for (const port of outputPortsSet.value) {
|
||||
if (graph.disconnectedEdgePorts.has(port)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
const portsVisible = computed(
|
||||
() =>
|
||||
selectionVisible.value ||
|
||||
(outputHovered.value && outputPortsSet.value.has(outputHovered.value)) ||
|
||||
anyPortDisconnected.value,
|
||||
)
|
||||
|
||||
const portsHoverAnimation = useApproach(() => (portsVisible.value ? 1 : 0), 50, 0.01)
|
||||
|
||||
watchEffect(() => emit('update:hoverAnim', portsHoverAnimation.value))
|
||||
|
||||
const hoverAnimations = new Map<AstId, [ReturnType<typeof useApproach>, EffectScope]>()
|
||||
watchEffect(() => {
|
||||
const ports = outputPortsSet.value
|
||||
for (const key of hoverAnimations.keys())
|
||||
if (!ports.has(key)) {
|
||||
hoverAnimations.get(key)?.[1].stop()
|
||||
hoverAnimations.delete(key)
|
||||
}
|
||||
for (const port of outputPortsSet.value) {
|
||||
setIfUndefined(hoverAnimations, port, () => {
|
||||
// Because `useApproach` uses `onScopeDispose` and we are calling it dynamically (i.e. not at
|
||||
// the setup top-level), we need to create a detached scope for each invocation.
|
||||
const scope = effectScope(true)
|
||||
const approach = scope.run(() =>
|
||||
useApproach(() => (outputHovered.value === port ? 1 : 0), 50, 0.01),
|
||||
)!
|
||||
return [approach, scope]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up dynamically created detached scopes.
|
||||
onScopeDispose(() => hoverAnimations.forEach(([_, scope]) => scope.stop()))
|
||||
|
||||
function portGroupStyle(port: PortData) {
|
||||
const [start, end] = port.clipRange
|
||||
const visBelowNode = graphSelectionSize.value.y - nodeSize.value.y
|
||||
return {
|
||||
'--hover-animation': portsHoverAnimation.value,
|
||||
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
|
||||
'--port-clip-start': start,
|
||||
'--port-clip-end': end,
|
||||
transform: `translateY(${visBelowNode}px`,
|
||||
}
|
||||
}
|
||||
|
||||
const visRect = shallowRef<Rect>()
|
||||
function updateVisualizationRect(rect: Rect | undefined) {
|
||||
visRect.value = rect
|
||||
@ -515,9 +423,9 @@ watchEffect(() => {
|
||||
<Teleport :to="graphNodeSelections">
|
||||
<GraphNodeSelection
|
||||
v-if="navigator && !edited"
|
||||
:class="{ dragged: isDragged }"
|
||||
:nodePosition="props.node.position"
|
||||
:nodeSize="graphSelectionSize"
|
||||
:class="{ draggable: true, dragged: isDragged }"
|
||||
:selected
|
||||
:nodeId
|
||||
:color
|
||||
@ -533,7 +441,7 @@ watchEffect(() => {
|
||||
<div class="binding" v-text="node.pattern?.code()" />
|
||||
<button
|
||||
v-if="!menuVisible && isRecordingOverridden"
|
||||
class="overrideRecordButton"
|
||||
class="overrideRecordButton clickable"
|
||||
data-testid="recordingOverriddenButton"
|
||||
@click="isRecordingOverridden = false"
|
||||
>
|
||||
@ -617,20 +525,13 @@ watchEffect(() => {
|
||||
/>
|
||||
<svg class="bgPaths" :style="bgStyleVariables">
|
||||
<rect class="bgFill" />
|
||||
<template v-for="port of outputPorts" :key="port.portId">
|
||||
<g :style="portGroupStyle(port)">
|
||||
<g class="portClip">
|
||||
<rect
|
||||
class="outputPortHoverArea"
|
||||
@pointerenter="outputHovered = port.portId"
|
||||
@pointerleave="outputHovered = undefined"
|
||||
@pointerdown.stop.prevent="handlePortClick($event, port.portId)"
|
||||
/>
|
||||
<rect class="outputPort" />
|
||||
</g>
|
||||
<text class="outputPortLabel">{{ port.label }}</text>
|
||||
</g>
|
||||
</template>
|
||||
<GraphNodeOutputPorts
|
||||
:nodeId="nodeId"
|
||||
:forceVisible="selectionVisible"
|
||||
@portClick="(...args) => emit('outputPortClick', ...args)"
|
||||
@portDoubleClick="(...args) => emit('outputPortDoubleClick', ...args)"
|
||||
@update:hoverAnim="emit('update:hoverAnim', $event)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@ -651,68 +552,6 @@ watchEffect(() => {
|
||||
--output-port-hover-width: 20px;
|
||||
}
|
||||
|
||||
.outputPort,
|
||||
.outputPortHoverArea {
|
||||
x: calc(0px - var(--output-port-width) / 2);
|
||||
y: calc(0px - var(--output-port-width) / 2);
|
||||
height: calc(var(--node-height) + var(--output-port-width));
|
||||
width: calc(var(--node-width) + var(--output-port-width));
|
||||
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
|
||||
|
||||
fill: none;
|
||||
stroke: var(--node-color-port);
|
||||
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap-anim));
|
||||
transition: stroke 0.2s ease;
|
||||
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
|
||||
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
|
||||
--radius-arclength: calc(
|
||||
(var(--node-border-radius) + var(--output-port-width) * 0.5) * 2 * 3.141592653589793
|
||||
);
|
||||
|
||||
stroke-dasharray: calc(var(--horizontal-line) + var(--radius-arclength) * 0.5) 10000%;
|
||||
stroke-dashoffset: calc(
|
||||
0px - var(--horizontal-line) - var(--vertical-line) - var(--radius-arclength) * 0.25
|
||||
);
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.outputPort {
|
||||
--output-port-overlap-anim: calc(var(--hover-animation) * var(--output-port-overlap));
|
||||
--output-port-width: calc(
|
||||
var(--output-port-max-width) * var(--hover-animation) + var(--output-port-hovered-extra-width) *
|
||||
var(--direct-hover-animation) - var(--output-port-overlap-anim)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outputPortHoverArea {
|
||||
--output-port-width: var(--output-port-hover-width);
|
||||
stroke-width: var(--output-port-hover-width);
|
||||
stroke: transparent;
|
||||
/* Make stroke visible to debug the active area: */
|
||||
/* stroke: red; */
|
||||
stroke-linecap: butt;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.portClip {
|
||||
clip-path: inset(
|
||||
0 calc((1 - var(--port-clip-end)) * (100% + 1px) - 0.5px) 0
|
||||
calc(var(--port-clip-start) * (100% + 1px) + 0.5px)
|
||||
);
|
||||
}
|
||||
|
||||
.outputPortLabel {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
text-anchor: middle;
|
||||
opacity: calc(var(--hover-animation) * var(--hover-animation));
|
||||
fill: var(--node-color-primary);
|
||||
transform: translate(50%, calc(var(--node-height) + var(--output-port-max-width) + 16px));
|
||||
}
|
||||
|
||||
.bgFill {
|
||||
width: var(--node-width);
|
||||
height: var(--node-height);
|
||||
@ -828,7 +667,6 @@ watchEffect(() => {
|
||||
|
||||
.overrideRecordButton {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
@ -841,11 +679,7 @@ watchEffect(() => {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dragged {
|
||||
cursor: grabbing;
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
</style>
|
||||
|
185
app/gui2/src/components/GraphEditor/GraphNodeOutputPorts.vue
Normal file
185
app/gui2/src/components/GraphEditor/GraphNodeOutputPorts.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { useApproach } from '@/composables/animation'
|
||||
import { useDoubleClick } from '@/composables/doubleClick'
|
||||
import { useGraphStore, type NodeId } from '@/stores/graph'
|
||||
import { setIfUndefined } from 'lib0/map'
|
||||
import type { AstId } from 'shared/ast'
|
||||
import { computed, effectScope, onScopeDispose, ref, watchEffect, type EffectScope } from 'vue'
|
||||
|
||||
const props = defineProps<{ nodeId: NodeId; forceVisible: boolean }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
portClick: [event: PointerEvent, portId: AstId]
|
||||
portDoubleClick: [event: PointerEvent, portId: AstId]
|
||||
'update:hoverAnim': [progress: number]
|
||||
}>()
|
||||
|
||||
const graph = useGraphStore()
|
||||
|
||||
// === Ports ===
|
||||
|
||||
interface PortData {
|
||||
clipRange: [number, number]
|
||||
label: string | undefined
|
||||
portId: AstId
|
||||
}
|
||||
|
||||
const outputPortsSet = computed(() => {
|
||||
const bindings = graph.db.nodeOutputPorts.lookup(props.nodeId)
|
||||
if (bindings.size === 0) return new Set([props.nodeId])
|
||||
return bindings
|
||||
})
|
||||
|
||||
const outputPorts = computed((): PortData[] => {
|
||||
const ports = outputPortsSet.value
|
||||
const numPorts = ports.size
|
||||
return Array.from(ports, (portId, index): PortData => {
|
||||
return {
|
||||
clipRange: [index / numPorts, (index + 1) / numPorts],
|
||||
label: numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) : undefined,
|
||||
portId,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// === Interactivity ===
|
||||
|
||||
const outputHovered = ref<AstId>()
|
||||
const anyPortDisconnected = computed(() => {
|
||||
for (const port of outputPortsSet.value) {
|
||||
if (graph.disconnectedEdgePorts.has(port)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const handlePortClick = useDoubleClick(
|
||||
(event: PointerEvent, portId: AstId) => emit('portClick', event, portId),
|
||||
(event: PointerEvent, portId: AstId) => emit('portDoubleClick', event, portId),
|
||||
).handleClick
|
||||
|
||||
// === Rendering ===
|
||||
|
||||
const portsVisible = computed(
|
||||
() =>
|
||||
props.forceVisible ||
|
||||
(outputHovered.value && outputPortsSet.value.has(outputHovered.value)) ||
|
||||
anyPortDisconnected.value,
|
||||
)
|
||||
|
||||
const portsHoverAnimation = useApproach(() => (portsVisible.value ? 1 : 0), 50, 0.01)
|
||||
|
||||
watchEffect(() => emit('update:hoverAnim', portsHoverAnimation.value))
|
||||
|
||||
const hoverAnimations = new Map<AstId, [ReturnType<typeof useApproach>, EffectScope]>()
|
||||
watchEffect(() => {
|
||||
const ports = outputPortsSet.value
|
||||
for (const key of hoverAnimations.keys())
|
||||
if (!ports.has(key)) {
|
||||
hoverAnimations.get(key)?.[1].stop()
|
||||
hoverAnimations.delete(key)
|
||||
}
|
||||
for (const port of outputPortsSet.value) {
|
||||
setIfUndefined(hoverAnimations, port, () => {
|
||||
// Because `useApproach` uses `onScopeDispose` and we are calling it dynamically (i.e. not at
|
||||
// the setup top-level), we need to create a detached scope for each invocation.
|
||||
const scope = effectScope(true)
|
||||
const approach = scope.run(() =>
|
||||
useApproach(() => (outputHovered.value === port ? 1 : 0), 50, 0.01),
|
||||
)!
|
||||
return [approach, scope]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up dynamically created detached scopes.
|
||||
onScopeDispose(() => hoverAnimations.forEach(([_, scope]) => scope.stop()))
|
||||
|
||||
function portGroupStyle(port: PortData) {
|
||||
const [start, end] = port.clipRange
|
||||
return {
|
||||
'--hover-animation': portsHoverAnimation.value,
|
||||
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
|
||||
'--port-clip-start': start,
|
||||
'--port-clip-end': end,
|
||||
transform: 'var(--output-port-transform)',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="port of outputPorts" :key="port.portId">
|
||||
<g :style="portGroupStyle(port)">
|
||||
<g class="portClip">
|
||||
<rect
|
||||
class="outputPortHoverArea clickable"
|
||||
@pointerenter="outputHovered = port.portId"
|
||||
@pointerleave="outputHovered = undefined"
|
||||
@pointerdown.stop.prevent="handlePortClick($event, port.portId)"
|
||||
/>
|
||||
<rect class="outputPort" />
|
||||
</g>
|
||||
<text class="outputPortLabel">{{ port.label }}</text>
|
||||
</g>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outputPort,
|
||||
.outputPortHoverArea {
|
||||
x: calc(0px - var(--output-port-width) / 2);
|
||||
y: calc(0px - var(--output-port-width) / 2);
|
||||
height: calc(var(--node-height) + var(--output-port-width));
|
||||
width: calc(var(--node-width) + var(--output-port-width));
|
||||
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
|
||||
|
||||
fill: none;
|
||||
stroke: var(--node-color-port);
|
||||
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap-anim));
|
||||
transition: stroke 0.2s ease;
|
||||
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
|
||||
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
|
||||
--radius-arclength: calc((var(--node-border-radius) + var(--output-port-width) * 0.5) * 2 * pi);
|
||||
|
||||
stroke-dasharray: calc(var(--horizontal-line) + var(--radius-arclength) * 0.5) 10000%;
|
||||
stroke-dashoffset: calc(
|
||||
0px - var(--horizontal-line) - var(--vertical-line) - var(--radius-arclength) * 0.25
|
||||
);
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.outputPort {
|
||||
--output-port-overlap-anim: calc(var(--hover-animation) * var(--output-port-overlap));
|
||||
--output-port-width: calc(
|
||||
var(--output-port-max-width) * var(--hover-animation) + var(--output-port-hovered-extra-width) *
|
||||
var(--direct-hover-animation) - var(--output-port-overlap-anim)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outputPortHoverArea {
|
||||
--output-port-width: var(--output-port-hover-width);
|
||||
stroke-width: var(--output-port-hover-width);
|
||||
stroke: transparent;
|
||||
/* Make stroke visible to debug the active area: */
|
||||
/* stroke: red; */
|
||||
stroke-linecap: butt;
|
||||
pointer-events: stroke;
|
||||
}
|
||||
|
||||
.portClip {
|
||||
clip-path: inset(
|
||||
0 calc((1 - var(--port-clip-end)) * (100% + 1px) - 0.5px) 0
|
||||
calc(var(--port-clip-start) * (100% + 1px) + 0.5px)
|
||||
);
|
||||
}
|
||||
|
||||
.outputPortLabel {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
text-anchor: middle;
|
||||
opacity: calc(var(--hover-animation) * var(--hover-animation));
|
||||
fill: var(--node-color-primary);
|
||||
transform: translate(50%, calc(var(--node-height) + var(--output-port-max-width) + 16px));
|
||||
}
|
||||
</style>
|
@ -108,7 +108,7 @@ export const ICON_WIDTH = 16
|
||||
<!-- Display an icon for the node if no widget in the tree provides one. -->
|
||||
<SvgIcon
|
||||
v-if="!props.connectedSelfArgumentId"
|
||||
class="icon grab-handle nodeCategoryIcon"
|
||||
class="icon grab-handle nodeCategoryIcon draggable"
|
||||
:style="{ margin: `0 ${GRAB_HANDLE_X_MARGIN_R}px 0 ${GRAB_HANDLE_X_MARGIN_L}px` }"
|
||||
:name="props.icon"
|
||||
@click.right.stop.prevent="emit('openFullMenu')"
|
||||
|
@ -388,7 +388,7 @@ declare module '@/providers/widgetRegistry' {
|
||||
<template>
|
||||
<div
|
||||
ref="widgetRoot"
|
||||
class="WidgetSelection"
|
||||
class="WidgetSelection clickable"
|
||||
:class="{ multiSelect: isMulti }"
|
||||
@click.stop="toggleDropdownWidget"
|
||||
@pointerover="isHovered = true"
|
||||
@ -424,7 +424,6 @@ declare module '@/providers/widgetRegistry' {
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-height: var(--node-port-height);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
@ -26,7 +26,7 @@ export const widgetDefinition = defineWidget(
|
||||
|
||||
<template>
|
||||
<SvgIcon
|
||||
class="WidgetSelfIcon icon nodeCategoryIcon r-24"
|
||||
class="WidgetSelfIcon icon nodeCategoryIcon draggable r-24"
|
||||
:name="icon"
|
||||
@click.right.stop.prevent="tree.emitOpenFullMenu()"
|
||||
/>
|
||||
|
@ -23,7 +23,7 @@ function onClick() {
|
||||
<TooltipTrigger>
|
||||
<template #default="triggerProps">
|
||||
<button
|
||||
class="MenuButton"
|
||||
class="MenuButton clickable"
|
||||
:aria-label="props.title ?? ''"
|
||||
:class="{ toggledOn, toggledOff: toggledOn === false, disabled }"
|
||||
:disabled="disabled ?? false"
|
||||
@ -48,7 +48,6 @@ function onClick() {
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--color-menu-entry-hover-bg);
|
||||
|
@ -17,7 +17,6 @@ span {
|
||||
|
||||
.NavBreadcrumb {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-full);
|
||||
|
||||
> .blur-container {
|
||||
|
@ -24,6 +24,7 @@ const emit = defineEmits<{ selected: [index: number] }>()
|
||||
:text="breadcrumb.label"
|
||||
:active="breadcrumb.active"
|
||||
:title="index === 0 ? 'Project Name' : ''"
|
||||
class="clickable"
|
||||
@click.stop="emit('selected', index)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -42,7 +42,6 @@ const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolea
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
padding: 4px 4px;
|
||||
width: 42px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-end {
|
||||
|
@ -87,6 +87,10 @@ const barStyle = computed(() => {
|
||||
/* FIXME[sb]: Get correct offset from dashboard. */
|
||||
left: 9px;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-menu-enter-active,
|
||||
|
@ -47,6 +47,7 @@ onMounted(() => setTimeout(() => rootNode.value?.querySelector('button')?.focus(
|
||||
v-for="type_ in props.types"
|
||||
:key="visIdKey(type_)"
|
||||
:class="{ selected: visIdentifierEquals(props.modelValue, type_) }"
|
||||
class="clickable"
|
||||
@click.stop="emit('update:modelValue', type_)"
|
||||
>
|
||||
<button>
|
||||
@ -97,7 +98,6 @@ button {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
|
@ -25,7 +25,7 @@ function entryTitle(index: number) {
|
||||
v-for="(child, index) in props.data"
|
||||
:key="index"
|
||||
:title="entryTitle(index)"
|
||||
class="element"
|
||||
class="element clickable"
|
||||
@click.stop="emit('createProjection', [$event.shiftKey ? [...props.data.keys()] : [index]])"
|
||||
>
|
||||
<JsonValueWidget
|
||||
@ -50,7 +50,6 @@ function entryTitle(index: number) {
|
||||
.block > .element {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.element:not(:last-child)::after {
|
||||
display: inline;
|
||||
|
@ -29,7 +29,7 @@ function entryTitle(key: string) {
|
||||
v-for="[key, value] in Object.entries(props.data)"
|
||||
:key="key"
|
||||
:title="entryTitle(key)"
|
||||
class="field"
|
||||
class="field clickable"
|
||||
@click.stop="emit('createProjection', [$event.shiftKey ? Object.keys(props.data) : [key]])"
|
||||
>
|
||||
<span class="key" v-text="JSON.stringify(key)" />:
|
||||
@ -63,7 +63,6 @@ function entryTitle(key: string) {
|
||||
.block > .field {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.field:not(:last-child)::after {
|
||||
display: inline;
|
||||
|
@ -4,14 +4,13 @@ const emit = defineEmits<{ 'update:modelValue': [modelValue: boolean] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Checkbox r-24" @click.stop="emit('update:modelValue', !props.modelValue)">
|
||||
<div class="Checkbox r-24 clickable" @click.stop="emit('update:modelValue', !props.modelValue)">
|
||||
<div :class="{ hidden: !props.modelValue }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Checkbox {
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 6px;
|
||||
|
@ -74,21 +74,21 @@ export interface DropdownEntry {
|
||||
<template>
|
||||
<div class="DropdownWidget" :style="styleVars">
|
||||
<ul class="list scrollable" @wheel.stop>
|
||||
<template v-for="entry in sortedValues" :key="entry.value">
|
||||
<li v-if="entry.selected">
|
||||
<div class="item selected" @click.stop="emit('clickEntry', entry, $event.altKey)">
|
||||
<span v-text="entry.value"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-else class="item" @click.stop="emit('clickEntry', entry, $event.altKey)">
|
||||
<span v-text="entry.value"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li
|
||||
v-for="entry in sortedValues"
|
||||
:key="entry.value"
|
||||
:class="{ selected: entry.selected }"
|
||||
class="item clickable"
|
||||
@click.stop="emit('clickEntry', entry, $event.altKey)"
|
||||
>
|
||||
<div class="itemContent" v-text="entry.value"></div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="enableSortButton" class="sort">
|
||||
<div class="sort-background"></div>
|
||||
<SvgIcon
|
||||
:name="ICON_LOOKUP[sortDirection]"
|
||||
class="clickable"
|
||||
@click="sortDirection = NEXT_SORT_DIRECTION[sortDirection]"
|
||||
/>
|
||||
</div>
|
||||
@ -145,7 +145,7 @@ li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.list span {
|
||||
.list .itemContent {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
@ -216,9 +216,4 @@ li.item:hover {
|
||||
background-color: var(--color-port-connected);
|
||||
}
|
||||
}
|
||||
|
||||
.item,
|
||||
.sort {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user