diff options
| author | Adam Malczewski <[email protected]> | 2026-09-05 20:44:09 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-05 20:44:09 +0900 |
| commit | d39cce17765c809cef4555c9640d12ed171cf4af (patch) | |
| tree | f5a4db2ea6d8f7acf0e00b04e2712d5c7b8b3fb0 | |
| parent | e3d5f3df3577f91b933d7ad51a339dbb05ba53dd (diff) | |
| download | dispatch-web-d39cce17765c809cef4555c9640d12ed171cf4af.tar.gz dispatch-web-d39cce17765c809cef4555c9640d12ed171cf4af.zip | |
feat(layout): renderer renders tile border markers (main red / side green)
| -rw-r--r-- | src/features/layout/logic/marker.test.ts | 16 | ||||
| -rw-r--r-- | src/features/layout/logic/marker.ts | 25 | ||||
| -rw-r--r-- | src/features/layout/ui/TileLeaf.svelte | 29 | ||||
| -rw-r--r-- | src/features/layout/ui/TilePanels.svelte | 3 | ||||
| -rw-r--r-- | src/features/layout/ui/TileRoot.test.ts | 76 | ||||
| -rw-r--r-- | src/features/layout/ui/TileSplit.svelte | 5 | ||||
| -rw-r--r-- | src/features/layout/ui/TileStack.svelte | 6 |
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} /> |
