Force active-border via direct DOM manipulation in polling loop
Svelte 5's template reactivity on \`class:active={activeLeafId === leaf.id}\`
in LeafPane did NOT propagate when the activeLeafId prop changed in
this app — verified via debug overlays showing the App-level state
updating correctly but the per-pane border never moving. Root cause
unclear (possibly the recursive Pane structure interacting badly with
the 250ms polling \$effect's re-runs, or a Svelte 5 corner case in
class-binding tracking through deeply-drilled props).
Workaround: the polling loop that detects focus changes now ALSO
walks document.querySelectorAll("[data-leaf-id].leaf") on every tick
and directly toggles the .active class via element.classList. If
Svelte re-renders and reverts, the next 250ms tick puts it back.
App-level activeLeafId is still drilled as a prop (used elsewhere) and
orch keeps its delegated setActive/clearActiveIf hooks, but the
visible border is owned by the DOM-direct path. Verified working with
PowerShell+Win32 click automation: clicking pane 2 moves the border
to pane 2, clicking pane 1 moves it back.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5f788652e
commit
854201be84
5 changed files with 80 additions and 40 deletions
|
|
@ -4,13 +4,20 @@
|
|||
import { useOrchestration } from "./orchestration.svelte";
|
||||
import XtermPane from "../../components/XtermPane.svelte";
|
||||
|
||||
let { leaf }: { leaf: LeafNode } = $props();
|
||||
let {
|
||||
leaf,
|
||||
activeLeafId,
|
||||
}: {
|
||||
leaf: LeafNode;
|
||||
activeLeafId: string | null;
|
||||
} = $props();
|
||||
|
||||
const orch = useOrchestration();
|
||||
|
||||
// Derives directly from orch.activeLeafId — Svelte 5 tracks the class
|
||||
// field access on every re-evaluation. No prop drilling involved.
|
||||
const active = $derived(orch.activeLeafId === leaf.id);
|
||||
// Diagnostic — log on every prop change.
|
||||
$effect(() => {
|
||||
console.log("[LeafPane render]", leaf.id.slice(0, 8), "activeLeafId=", activeLeafId?.slice(0, 8) ?? "null", "match=", activeLeafId === leaf.id);
|
||||
});
|
||||
|
||||
let status = $state("starting…");
|
||||
let statusOk = $state(true);
|
||||
|
|
@ -100,7 +107,7 @@
|
|||
let focusTrigger = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
if (active) focusTrigger += 1;
|
||||
if (activeLeafId === leaf.id) focusTrigger += 1;
|
||||
});
|
||||
|
||||
// Backup setActive for toolbar clicks (which can't reach the document
|
||||
|
|
@ -119,7 +126,9 @@
|
|||
|
||||
<div
|
||||
class="leaf"
|
||||
class:active={active}
|
||||
class:active={activeLeafId === leaf.id}
|
||||
data-debug-active={activeLeafId === leaf.id ? "yes" : "no"}
|
||||
data-debug-prop={(activeLeafId ?? "null").slice(0, 8)}
|
||||
class:broadcasting={leaf.broadcast}
|
||||
role="group"
|
||||
aria-label={"Terminal pane: " + (leaf.label ?? leaf.distro ?? "unnamed")}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
<script lang="ts">
|
||||
import type { TreeNode } from "./tree";
|
||||
import type { TreeNode, NodeId } from "./tree";
|
||||
import SplitNode from "./SplitNode.svelte";
|
||||
import LeafPane from "./LeafPane.svelte";
|
||||
|
||||
let { node }: { node: TreeNode } = $props();
|
||||
let {
|
||||
node,
|
||||
activeLeafId,
|
||||
}: {
|
||||
node: TreeNode;
|
||||
activeLeafId: NodeId | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if node.kind === "split"}
|
||||
<SplitNode {node} />
|
||||
<SplitNode {node} {activeLeafId} />
|
||||
{:else}
|
||||
{#key node.id}
|
||||
<LeafPane leaf={node} />
|
||||
{/key}
|
||||
<LeafPane leaf={node} {activeLeafId} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { SplitNode } from "./tree";
|
||||
import type { SplitNode, NodeId } from "./tree";
|
||||
import Pane from "./Pane.svelte";
|
||||
|
||||
let { node }: { node: SplitNode } = $props();
|
||||
let {
|
||||
node,
|
||||
activeLeafId,
|
||||
}: {
|
||||
node: SplitNode;
|
||||
activeLeafId: NodeId | null;
|
||||
} = $props();
|
||||
|
||||
let containerEl: HTMLDivElement;
|
||||
let dragging = $state(false);
|
||||
|
|
@ -38,7 +44,7 @@
|
|||
bind:this={containerEl}
|
||||
>
|
||||
<div class="side" style="flex: {node.ratio}">
|
||||
<Pane node={node.a} />
|
||||
<Pane node={node.a} {activeLeafId} />
|
||||
</div>
|
||||
<div
|
||||
class="gutter"
|
||||
|
|
@ -53,7 +59,7 @@
|
|||
onpointercancel={onPointerUp}
|
||||
></div>
|
||||
<div class="side" style="flex: {1 - node.ratio}">
|
||||
<Pane node={node.b} />
|
||||
<Pane node={node.b} {activeLeafId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ export interface TreeOps {
|
|||
|
||||
export class Orchestration {
|
||||
// ---- shared reactive state ----------------------------------------------
|
||||
activeLeafId = $state<NodeId | null>(null);
|
||||
// (activeLeafId lives at App level and is drilled as a prop — Svelte 5
|
||||
// doesn't seem to track class-field $state reads from child components
|
||||
// that obtain the instance via getContext. Tested empirically.)
|
||||
notifications = $state<Toast[]>([]);
|
||||
distros = $state<string[]>([]);
|
||||
|
||||
|
|
@ -52,14 +54,17 @@ export class Orchestration {
|
|||
this.#ops = ops;
|
||||
}
|
||||
|
||||
// ---- active pane --------------------------------------------------------
|
||||
setActive(leafId: NodeId): void {
|
||||
console.log("[orch] setActive", leafId, "was", this.activeLeafId);
|
||||
this.activeLeafId = leafId;
|
||||
}
|
||||
// ---- active pane (delegated to App) -------------------------------------
|
||||
// These point at App-level $state mutators set via configure().
|
||||
setActive: (leafId: NodeId) => void = () => {};
|
||||
clearActiveIf: (leafId: NodeId) => void = () => {};
|
||||
|
||||
clearActiveIf(leafId: NodeId): void {
|
||||
if (this.activeLeafId === leafId) this.activeLeafId = null;
|
||||
configureActiveHandlers(
|
||||
setActive: (leafId: NodeId) => void,
|
||||
clearActiveIf: (leafId: NodeId) => void,
|
||||
): void {
|
||||
this.setActive = setActive;
|
||||
this.clearActiveIf = clearActiveIf;
|
||||
}
|
||||
|
||||
// ---- notifications ------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue