From be62b3138279bd0a0654248e6691ab5b7be96fc5 Mon Sep 17 00:00:00 2001 From: Guillaume Onfroy Date: Fri, 18 Sep 2026 00:15:39 +0200 Subject: [PATCH] feat(app): lift a graph node on hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsed graph cards were inert under the cursor, leaving the canvas without any read on what the pointer was over. Hovering a node now lifts it 2px. The top card and the folded layers under it carry the same transform, so a stack travels as one object and its peek stays as it looks at rest, and only the floor of the stack — the card actually casting onto the canvas — deepens its shadow. The hover is driven from the node root rather than the card so the corner badges, which sit over the card but outside it, don't break it. Expanded containers sit it out: their members are separate canvas nodes that wouldn't travel with the frame. By Digitl --- .../app/app/assets/css/main.css | 42 +++++++++++++++++++ .../app/app/components/graph/AssetNode.vue | 4 +- .../app/app/components/graph/CardStack.vue | 7 +++- .../app/app/components/graph/SourceNode.vue | 13 ++++-- .../app/components/graph/SourceTypeNode.vue | 5 ++- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/interloper-app/app/app/assets/css/main.css b/packages/interloper-app/app/app/assets/css/main.css index 44a41f31..de4bb269 100644 --- a/packages/interloper-app/app/app/assets/css/main.css +++ b/packages/interloper-app/app/app/assets/css/main.css @@ -107,8 +107,11 @@ --id-chip-text: #1b5fbf; --graph-model: #6c5ce7; --graph-card-shadow: 0 2px 8px rgb(11 42 66 / 0.07); + --graph-card-shadow-raised: 0 6px 16px rgb(11 42 66 / 0.11); --graph-asset-shadow: 0 1px 4px rgb(11 42 66 / 0.06); + --graph-asset-shadow-raised: 0 4px 12px rgb(11 42 66 / 0.1); --graph-layer-shadow: 0 2px 6px rgb(11 42 66 / 0.06); + --graph-layer-shadow-raised: 0 4px 10px rgb(11 42 66 / 0.09); } /* Design mono section label ("eyebrow"): pair with a text color utility. */ @@ -194,8 +197,11 @@ --id-chip-text: color-mix(in srgb, var(--ui-primary) 70%, white); --graph-model: #9d8ff0; --graph-card-shadow: 0 2px 8px rgb(0 0 0 / 0.45); + --graph-card-shadow-raised: 0 6px 16px rgb(0 0 0 / 0.55); --graph-asset-shadow: 0 1px 4px rgb(0 0 0 / 0.4); + --graph-asset-shadow-raised: 0 4px 12px rgb(0 0 0 / 0.5); --graph-layer-shadow: 0 2px 6px rgb(0 0 0 / 0.4); + --graph-layer-shadow-raised: 0 4px 10px rgb(0 0 0 / 0.5); --vf-node-bg: var(--ui-bg-elevated); --vf-node-text: var(--color-gray-400); @@ -216,6 +222,42 @@ box-shadow: var(--graph-layer-shadow); } +/* Hovering anywhere on a node lifts it: the top card and the folded layers under + it travel together, as one stack. Driven from the node root so the corner + badges, which sit over the card but outside it, don't break the hover. Worn by + collapsed cards only: an expanded container's members are separate canvas + nodes that wouldn't travel with it. */ +.graph-raise, +.graph-stack-layer { + transition: transform 150ms ease-out, box-shadow 150ms ease-out; +} + +.graph-node:hover .graph-raise, +.graph-node:hover .graph-stack-layer { + transform: translateY(-2px); +} + +/* Only the floor of the stack deepens its shadow — the one card actually casting + onto the canvas. The shadows between the folded layers stay put. */ +.graph-node:hover .graph-layer.graph-stack-floor { + box-shadow: var(--graph-layer-shadow-raised); +} + +.graph-node:hover .graph-card.graph-stack-floor { + box-shadow: var(--graph-card-shadow-raised); +} + +.graph-node:hover .graph-asset.graph-stack-floor { + box-shadow: var(--graph-asset-shadow-raised); +} + +@media (prefers-reduced-motion: reduce) { + .graph-node:hover .graph-raise, + .graph-node:hover .graph-stack-layer { + transform: none; + } +} + /* Identifier chip: a discriminator or other machine id shown next to a name. */ .id-chip { padding: 2px 8px; diff --git a/packages/interloper-app/app/app/components/graph/AssetNode.vue b/packages/interloper-app/app/app/components/graph/AssetNode.vue index 0d8ae3d4..52265e72 100644 --- a/packages/interloper-app/app/app/components/graph/AssetNode.vue +++ b/packages/interloper-app/app/app/components/graph/AssetNode.vue @@ -151,7 +151,7 @@ const contextMenuItems = computed(() => { diff --git a/packages/interloper-app/app/app/components/graph/SourceNode.vue b/packages/interloper-app/app/app/components/graph/SourceNode.vue index 3dac31b7..99183579 100644 --- a/packages/interloper-app/app/app/components/graph/SourceNode.vue +++ b/packages/interloper-app/app/app/components/graph/SourceNode.vue @@ -125,14 +125,19 @@ const isMaterializing = computed(() => const headHeight = computed(() => props.nested ? 64 : 84) const showChip = computed(() => !!props.source.discriminator && props.source.discriminator !== props.source.name) -const frameClass = computed(() => props.nested - ? 'rounded-[14px] border border-[var(--graph-nested-line)] bg-[var(--graph-nested-bg)]' - : ['graph-card rounded-2xl border border-[var(--graph-card-line)] bg-default', props.selected && 'outline-2 outline-primary outline-offset-4']) +const frameClass = computed(() => [ + collapsed.value && 'graph-raise', + // With layers folded under it the card is no longer what the stack casts from. + collapsed.value && props.layers === 0 && 'graph-stack-floor', + props.nested + ? 'rounded-[14px] border border-[var(--graph-nested-line)] bg-[var(--graph-nested-bg)]' + : ['graph-card rounded-2xl border border-[var(--graph-card-line)] bg-default', props.selected && 'outline-2 outline-primary outline-offset-4'], +])