summaryrefslogtreecommitdiffhomepage
path: root/src/features/layout/logic/seed.ts
blob: 7cd0ba36e629c79286a1e0748750921ebf658fe0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
 * Boot-time tree seeding: the default layout with today's sidebar selection
 * overlaid from localStorage. Byte-compatible with today's boot behavior
 * (`dispatch.sidebar.views` read; unknown/removed kinds dropped — the tree
 * schema has no "unknown view" panel state, so a stale id is skipped exactly
 * like a view that no longer exists).
 */

import {
  DEFAULT_LAYOUT,
  findById,
  type LayoutNode,
  type LayoutTree,
  type LeafNode,
  type PanelsNode,
  type Path,
} from "@dispatch/layout-contract";

/**
 * Today's default sidebar selection (the moved-from-top tab bar + the model
 * controls) — the fallback when nothing (or nothing valid) is stored.
 */
export const DEFAULT_PANEL_VIEWS: readonly string[] = ["tabs", "model"];

/**
 * Seed a tree from DEFAULT_LAYOUT with `storedViews` (the parsed
 * `dispatch.sidebar.views` value, or null when absent) overlaid onto the
 * default tree's panels node.
 *
 * - null / non-array / array without any known view → the defaults.
 * - Known views are kept in the stored order; unknown ids are dropped.
 */
export function seedLayout(storedViews: unknown, panelViewIds: readonly string[]): LayoutTree {
  const known = new Set(panelViewIds);
  const stored = Array.isArray(storedViews)
    ? storedViews.filter((v): v is string => typeof v === "string")
    : [];
  const panels = stored.filter((v) => known.has(v));
  const selected = panels.length > 0 ? panels : DEFAULT_PANEL_VIEWS.filter((v) => known.has(v));
  return overlaySidePanels(DEFAULT_LAYOUT, selected);
}

/**
 * A NEW tree: `base` with its `"side"` panels node replaced by leaves mounting
 * `views` (in order). Targets the panels node by id (falls back to no-op when
 * the base has none); the rest of the tree is shared, not copied.
 */
export function overlaySidePanels(base: LayoutTree, views: readonly string[]): LayoutTree {
  const found = findById(base, "side");
  if (found === null || found.node.kind !== "panels") return base;
  const panels: readonly LeafNode[] = views.map((view) => ({ kind: "leaf", view }));
  const next: PanelsNode = { ...found.node, panels };
  return { version: 1, root: replaceChild(base.root, found.path, next) };
}

/** Rebuild the node at `path` (immutably); the replacement must be a panels node. */
function replaceChild(node: LayoutNode, path: Path, replacement: PanelsNode): LayoutNode {
  if (path.length === 0) return replacement;
  const [head, ...rest] = path;
  if (typeof head !== "number") return node;
  if (node.kind === "split") {
    return {
      ...node,
      children: node.children.map((child, i) =>
        i === head ? { ...child, node: replaceChild(child.node, rest, replacement) } : child,
      ),
    };
  }
  if (node.kind === "stack") {
    return {
      ...node,
      children: node.children.map((child, i) =>
        i === head ? { ...child, node: replaceChild(child.node, rest, replacement) } : child,
      ),
    };
  }
  return node;
}