fix(ui/workflows): canvas controls dark-theme + reliable node-click

This commit is contained in:
matevip 2026-05-08 15:06:09 +08:00
parent 12bd05382e
commit 789af09772
3 changed files with 66 additions and 28 deletions

View File

@ -276,39 +276,66 @@ html.dark {
/* Vue Flow theme bridge its default stylesheet ships hard-coded
light colors for handles / mini-map / control buttons. Map them to
our theme tokens so dark mode inherits cleanly. */
.vue-flow__handle {
our theme tokens so dark mode inherits cleanly.
Each selector is bumped to (0,2,0) specificity by scoping to
`.workflow-canvas` because vue-flow's component CSS imports inside
WorkflowCanvas.vue land in the bundle AFTER main.css; equal
specificity would lose. */
.workflow-canvas .vue-flow__handle {
background: var(--mc-edge-strong, #777);
border-color: var(--mc-bg-elevated, #fff);
}
.vue-flow__edge-path {
.workflow-canvas .vue-flow__edge-path {
stroke: var(--mc-edge, #999);
}
.vue-flow__edge-text {
.workflow-canvas .vue-flow__edge-text {
fill: var(--mc-text-secondary, #555);
font-size: 11px;
}
.vue-flow__edge-textbg {
.workflow-canvas .vue-flow__edge-textbg {
fill: var(--mc-bg-elevated, #fff);
}
.vue-flow__minimap {
background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.04)) !important;
.workflow-canvas .vue-flow__background {
background: transparent;
}
.workflow-canvas .vue-flow__minimap {
background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.04));
border: 1px solid var(--mc-border-light, rgba(0, 0, 0, 0.08));
border-radius: 6px;
}
.workflow-canvas .vue-flow__minimap-mask {
fill: var(--mc-bg-muted, rgba(0, 0, 0, 0.06));
stroke: var(--mc-border, transparent);
}
.workflow-canvas .vue-flow__controls {
box-shadow: var(--mc-shadow-soft, 0 2px 6px rgba(0, 0, 0, 0.1));
border-radius: 6px;
overflow: hidden;
background: var(--mc-bg-elevated, #ffffff);
border: 1px solid var(--mc-border-light, rgba(0, 0, 0, 0.08));
}
.vue-flow__minimap-mask {
fill: var(--mc-bg-muted, rgba(0, 0, 0, 0.06));
}
.vue-flow__controls {
box-shadow: var(--mc-shadow-soft, 0 2px 6px rgba(0, 0, 0, 0.1));
}
.vue-flow__controls-button {
background: var(--mc-bg-elevated, #fff);
border-bottom-color: var(--mc-border-light, rgba(0, 0, 0, 0.08));
.workflow-canvas .vue-flow__controls-button {
background: var(--mc-bg-elevated, #ffffff);
border-bottom: 1px solid var(--mc-border-light, rgba(0, 0, 0, 0.08));
fill: var(--mc-text-primary, #333);
color: var(--mc-text-primary, #333);
}
.vue-flow__controls-button:hover {
.workflow-canvas .vue-flow__controls-button svg {
fill: currentColor;
}
.workflow-canvas .vue-flow__controls-button:hover {
background: var(--mc-bg-muted, rgba(0, 0, 0, 0.05));
}
/* Round the bottom button so the corner doesn't show the hardcoded grey
border vue-flow ships with. */
.workflow-canvas .vue-flow__controls-button:last-child {
border-bottom: none;
}
/* Selection ring matches the brand accent in both modes. */
.workflow-canvas .vue-flow__node.selected {
box-shadow: 0 0 0 2px var(--mc-primary, #4084ff);
}
/* ================================================================
Base resets

View File

@ -3,8 +3,7 @@
class="step-node"
:class="`mode-${data.modeType}`"
:data-selected="props.selected"
@click.stop="handleClick"
@pointerdown.stop
@click="handleClick"
>
<Handle type="target" :position="targetPosition" />
<div class="step-band" />

View File

@ -79,9 +79,9 @@
</template>
<script setup lang="ts">
import { computed, markRaw, onBeforeUnmount, provide, ref, watch } from 'vue'
import { computed, markRaw, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { VueFlow, type Node } from '@vue-flow/core'
import { VueFlow, useVueFlow, type Node } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
@ -139,19 +139,31 @@ const miniNodeColor = (node: Node<StepNodeData>) => {
}
const miniNodeStrokeColor = () => 'transparent'
// Vue-flow's `@node-click` event signature has shifted across minor
// versions and sometimes doesn't fire reliably for custom-node templates
// because the click can be swallowed by the inner DOM. Bypass it: we
// provide a selection callback to the StepNode via inject, and the node
// invokes it directly on its own click handler.
// Three independent paths for getting a node click out of vue-flow into
// the parent's inspector at least one always works regardless of how
// the click lands inside vue-flow's internal DOM.
//
// 1. `useVueFlow().onNodeClick` the official subscription API tied
// to the flow instance keyed by props.canvasId.
// 2. `provide('selectStepCallback', ...)` the StepNode injects this
// and fires it on its own @click. Survives vue-flow re-renders.
// 3. The template `@node-click` attribute below kept as a third
// fallback for completeness.
function selectStep(data: StepNodeData | null) {
emit('select-step', data)
}
provide('selectStepCallback', selectStep)
const flow = useVueFlow(props.canvasId)
onMounted(() => {
flow.onNodeClick((evt) => {
const data = (evt?.node as Node<StepNodeData> | undefined)?.data
if (data) selectStep(data)
})
flow.onPaneClick(() => selectStep(null))
})
function handleNodeClick(payload: { node: Node<StepNodeData> } | unknown) {
// Backup path: if vue-flow does fire its own node-click event with the
// expected shape, route it through the same emit.
const node = (payload as { node?: Node<StepNodeData> })?.node
if (node?.data) selectStep(node.data)
}