summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/features/layout/logic/marker.test.ts16
-rw-r--r--src/features/layout/logic/marker.ts25
-rw-r--r--src/features/layout/ui/TileLeaf.svelte29
-rw-r--r--src/features/layout/ui/TilePanels.svelte3
-rw-r--r--src/features/layout/ui/TileRoot.test.ts76
-rw-r--r--src/features/layout/ui/TileSplit.svelte5
-rw-r--r--src/features/layout/ui/TileStack.svelte6
7 files changed, 152 insertions, 8 deletions
diff --git a/src/features/layout/logic/marker.test.ts b/src/features/layout/logic/marker.test.ts
new file mode 100644
index 0000000..d9e3826
--- /dev/null
+++ b/src/features/layout/logic/marker.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest";
+import { markerOutline } from "./marker";
+
+describe("markerOutline — the diagnostic tile-border mapping", () => {
+ it("main → a 4px solid red outline (the main tile's marker)", () => {
+ expect(markerOutline("main")).toBe("4px solid red");
+ });
+
+ it("side → a 4px solid green outline (the side tile's marker)", () => {
+ expect(markerOutline("side")).toBe("4px solid green");
+ });
+
+ it("no marker → no outline (existing trees render exactly as before)", () => {
+ expect(markerOutline(undefined)).toBeUndefined();
+ });
+});
diff --git a/src/features/layout/logic/marker.ts b/src/features/layout/logic/marker.ts
new file mode 100644
index 0000000..eb1ff84
--- /dev/null
+++ b/src/features/layout/logic/marker.ts
@@ -0,0 +1,25 @@
+/**
+ * Diagnostic tile-border markers (the contract's additive `TileMarker`
+ * presentation field) → the inline outline the renderer draws. Pure
+ * decision logic — the .svelte tile components just apply the returned
+ * style value.
+ *
+ * INLINE STYLES, deliberately: a marker value arrives in TREE DATA at
+ * runtime, and Tailwind only generates classes it sees in source files — a
+ * dynamic class from tree data would silently render nothing. An outline
+ * (not a border) is used so the diagnostic draws WITHOUT changing any
+ * tile's layout/size (4px solid, clearly visible).
+ *
+ * The marker is purely presentational: unknown/absent → no outline
+ * (existing trees render exactly as before), and no other behavior is
+ * attached to it anywhere in the renderer.
+ */
+
+import type { TileMarker } from "@dispatch/layout-contract";
+
+/** The diagnostic outline style for a marker — `undefined` = draw nothing. */
+export function markerOutline(marker: TileMarker | undefined): string | undefined {
+ if (marker === "main") return "4px solid red";
+ if (marker === "side") return "4px solid green";
+ return undefined;
+}
diff --git a/src/features/layout/ui/TileLeaf.svelte b/src/features/layout/ui/TileLeaf.svelte
index cd61b6b..2d4a91b 100644
--- a/src/features/layout/ui/TileLeaf.svelte
+++ b/src/features/layout/ui/TileLeaf.svelte
@@ -4,20 +4,41 @@
* REACTIVE registry, so a late registration (plan 03) swaps the tile in
* without a tree change. An unknown view id renders a fallback tile —
* NEVER a crash (backend-owned trees can name views this client lacks).
+ *
+ * A diagnostic `marker` on the leaf draws an outline around it — the only
+ * node kind without a natural tile wrapper, so the wrapper exists only
+ * when marked (unmarked leaves render exactly as before).
*/
+ import type { TileMarker } from "@dispatch/layout-contract";
import { viewRegistry, type RegisteredView } from "../logic/registry";
+ import { markerOutline } from "../logic/marker";
import FallbackTile from "./FallbackTile.svelte";
- let { node, ports }: { node: { readonly view: string }; ports?: unknown } = $props();
+ let {
+ node,
+ ports,
+ }: {
+ node: { readonly view: string; readonly marker?: TileMarker };
+ ports?: unknown;
+ } = $props();
const entry: RegisteredView | undefined = $derived(
$viewRegistry.find((v) => v.id === node.view),
);
const View = $derived(entry?.component);
+ const outline = $derived(markerOutline(node.marker));
</script>
-{#if View}
- <View {ports} />
+{#snippet content()}
+ {#if View}
+ <View {ports} />
+ {:else}
+ <FallbackTile viewId={node.view} />
+ {/if}
+{/snippet}
+
+{#if outline !== undefined}
+ <div style:outline={outline}>{@render content()}</div>
{:else}
- <FallbackTile viewId={node.view} />
+ {@render content()}
{/if}
diff --git a/src/features/layout/ui/TilePanels.svelte b/src/features/layout/ui/TilePanels.svelte
index ca597fe..877ce2e 100644
--- a/src/features/layout/ui/TilePanels.svelte
+++ b/src/features/layout/ui/TilePanels.svelte
@@ -9,6 +9,7 @@
import type { LayoutOp, PanelsNode, Path } from "@dispatch/layout-contract";
import { viewRegistry } from "../logic/registry";
import { addPanelOp, firstPanelView, removePanelOp, swapPanelOp } from "../logic/panels-edit";
+ import { markerOutline } from "../logic/marker";
import TileLeaf from "./TileLeaf.svelte";
let {
@@ -29,7 +30,7 @@
const addView = $derived(firstPanelView(panelViews));
</script>
-<div class="flex min-h-0 flex-col gap-2">
+<div class="flex min-h-0 flex-col gap-2" style:outline={markerOutline(node.marker)}>
{#each node.panels as panel, idx (idx)}
<div class="flex flex-col rounded-lg bg-base-200 p-3">
<div class="flex items-center gap-1">
diff --git a/src/features/layout/ui/TileRoot.test.ts b/src/features/layout/ui/TileRoot.test.ts
index aba22a9..ace280a 100644
--- a/src/features/layout/ui/TileRoot.test.ts
+++ b/src/features/layout/ui/TileRoot.test.ts
@@ -51,6 +51,35 @@ function panelsOnlyTree(...views: string[]): LayoutTree {
return { version: 1, root: { kind: "panels", id: "side", panels: views.map(leaf) } };
}
+/** The default test tree WITH the diagnostic markers on its two root tiles. */
+function markedTestTree(): LayoutTree {
+ return {
+ version: 1,
+ root: {
+ kind: "split",
+ id: "root",
+ dir: "row",
+ children: [
+ {
+ size: 1,
+ node: {
+ kind: "stack",
+ id: "main",
+ marker: "main",
+ children: [{ grow: 1, node: leaf("fix-a") }],
+ },
+ },
+ {
+ size: 0,
+ basis: 320,
+ collapsible: true,
+ node: { kind: "panels", id: "side", marker: "side", panels: [leaf("fix-b")] },
+ },
+ ],
+ },
+ };
+}
+
function sidePanelsOf(tree: LayoutTree | null): string[] {
if (tree === null) return [];
const side = (tree.root as SplitNode).children?.[1]?.node;
@@ -206,6 +235,53 @@ describe("TileRoot", () => {
expect(stackColumn).toHaveClass("h-full");
});
+ it("a marked stack/panels node draws its diagnostic outline (main red, side green)", () => {
+ // The inline outline IS the honest seam: marker values arrive in runtime
+ // tree data (Tailwind can't see them — dynamic classes would silently
+ // render nothing), so the renderer applies an inline style, and the test
+ // asserts exactly what lands on the tile wrapper.
+ const { container } = render(TileRoot, {
+ props: { tree: markedTestTree(), collapse: createCollapseStore({ viewportWidth: 1024 }) },
+ });
+ const stack = container.querySelector("[data-testid='fixture-a']")?.closest(".flex-col");
+ expect(stack?.getAttribute("style")).toContain("outline: 4px solid red");
+ const panels = container.querySelector("[data-testid='fixture-b']")?.closest(".gap-2");
+ expect(panels?.getAttribute("style")).toContain("outline: 4px solid green");
+ });
+
+ it("an unmarked tree draws NO outlines (trees without the field render exactly as before)", () => {
+ const { container } = render(TileRoot, {
+ props: { tree: defaultTestTree(), collapse: createCollapseStore() },
+ });
+ expect(container.querySelectorAll("[style*='outline']")).toHaveLength(0);
+ });
+
+ it("an unknown marker value draws nothing (the renderer ignores what it doesn't know)", () => {
+ // Both root tiles marked with a value the renderer doesn't know.
+ const tree = markedTestTree();
+ const root = tree.root;
+ if (root.kind !== "split") return;
+ const junked = root.children.map((child) => ({
+ ...child,
+ node: { ...child.node, marker: "purple" } as unknown as typeof child.node,
+ }));
+ const marked: LayoutTree = { version: 1, root: { ...root, children: junked } };
+ const { container } = render(TileRoot, {
+ props: { tree: marked, collapse: createCollapseStore({ viewportWidth: 1024 }) },
+ });
+ expect(container.querySelectorAll("[style*='outline']")).toHaveLength(0);
+ });
+
+ it("a marked LEAF draws its outline around a wrapper (the only wrapperless node kind)", () => {
+ const tree: LayoutTree = {
+ version: 1,
+ root: { kind: "leaf", view: "fix-a", marker: "side" },
+ };
+ const { container } = render(TileRoot, { props: { tree, collapse: createCollapseStore() } });
+ const fixture = container.querySelector("[data-testid='fixture-a']");
+ expect(fixture?.parentElement?.getAttribute("style")).toContain("outline: 4px solid green");
+ });
+
it("the dropdown lists panel-eligible views only (decision 5)", () => {
render(TileRoot, { props: { tree: panelsOnlyTree("fix-b"), collapse: createCollapseStore() } });
const select = screen.getByRole("combobox", { name: "Select a view" });
diff --git a/src/features/layout/ui/TileSplit.svelte b/src/features/layout/ui/TileSplit.svelte
index 1f0ec64..b41f9fc 100644
--- a/src/features/layout/ui/TileSplit.svelte
+++ b/src/features/layout/ui/TileSplit.svelte
@@ -8,6 +8,7 @@
*/
import type { LayoutOp, Path, SplitNode } from "@dispatch/layout-contract";
import type { CollapseStore } from "../logic/collapse";
+ import { markerOutline } from "../logic/marker";
import TileNode from "./TileNode.svelte";
let {
@@ -38,7 +39,7 @@
</script>
{#if node.dir === "col"}
- <div class="flex min-h-0 w-full flex-col">
+ <div class="flex min-h-0 w-full flex-col" style:outline={markerOutline(node.marker)}>
{#each node.children as child, i (i)}
{@const key = keyOf(i, child.node.id)}
<div
@@ -50,7 +51,7 @@
{/each}
</div>
{:else}
- <div class="flex min-h-0 h-full flex-row">
+ <div class="flex min-h-0 h-full flex-row" style:outline={markerOutline(node.marker)}>
{#each node.children as child, i (i)}
{@const key = keyOf(i, child.node.id)}
{#if child.collapsible}
diff --git a/src/features/layout/ui/TileStack.svelte b/src/features/layout/ui/TileStack.svelte
index ac7091a..d2ee267 100644
--- a/src/features/layout/ui/TileStack.svelte
+++ b/src/features/layout/ui/TileStack.svelte
@@ -12,6 +12,7 @@
*/
import type { LayoutOp, Path, StackNode } from "@dispatch/layout-contract";
import type { CollapseStore } from "../logic/collapse";
+ import { markerOutline } from "../logic/marker";
import TileNode from "./TileNode.svelte";
let {
@@ -29,7 +30,10 @@
} = $props();
</script>
-<div class="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
+<div
+ class="flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
+ style:outline={markerOutline(node.marker)}
+>
{#each node.children as child, i (i)}
<div class="min-h-0 min-w-0" style={"flex: " + (child.grow ?? 0) + (child.grow === undefined ? " 0 auto" : " 1 0%")}>
<TileNode node={child.node} path={[...path, i]} {ports} {collapse} {onEdit} />