@jbrowse/app-core 4.3.0 → 5.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -0,0 +1,88 @@
1
+ import type { LayoutTree, NodeKind } from './tree.ts';
2
+ /**
3
+ * A view named by a layout leaf: an index into the view list the layout is
4
+ * applied against (a session spec's own `views`, or `session.views` for the
5
+ * live `applyLayoutSpec`), or a view id.
6
+ */
7
+ export type LayoutViewRef = number | string;
8
+ /**
9
+ * The `layout` a session spec / URL param states, in the vocabulary users
10
+ * write it in, and the same shape the live `applyLayoutSpec` action takes — one
11
+ * shape, so an agent that has written one has written the other. **This shape
12
+ * is public** — it is documented as a URL parameter — so it keeps
13
+ * `horizontal`/`vertical`/`tabs` and percentage `size` rather than being
14
+ * renamed to match the internal tree.
15
+ *
16
+ * `resolveLayoutSpec` turns the indexes into ids; the tree builder below takes
17
+ * the resolved form only.
18
+ */
19
+ export interface LayoutSpecNode<View = LayoutViewRef> {
20
+ /** a leaf: the views to stack vertically in one tab */
21
+ views?: View[];
22
+ /** how children divide the space; `tabs` puts them in one cell as tabs */
23
+ direction?: 'horizontal' | 'vertical' | 'tabs';
24
+ children?: LayoutSpecNode<View>[];
25
+ /** share of the parent, as a percentage */
26
+ size?: number;
27
+ }
28
+ export type ResolvedLayoutSpecNode = LayoutSpecNode<string>;
29
+ /**
30
+ * Resolve every view a spec names to an id against `allViewIds`, the list its
31
+ * indexes count into. Anything that resolves to nothing throws, naming what was
32
+ * received: an untyped caller (the MCP `run_javascript` tool) that writes
33
+ * `viewIds` where `views` goes, or an index past the end, used to be accepted
34
+ * as an empty leaf and collapse the workspace into one blank tab with nothing
35
+ * said.
36
+ *
37
+ * A node stating no `views` and no `children` is NOT that slip — it is the
38
+ * empty panel `treeFromSpec` has always built, and refusing it here made the
39
+ * two surfaces disagree about the one shape they share. The slip is an
40
+ * unrecognized KEY, which is what `viewIds` actually is, and naming it beats
41
+ * the old message that could only say the node needed something else.
42
+ *
43
+ * `allViewIds` is undefined only on a model that composes no view list, where
44
+ * an id cannot be checked and an index cannot mean anything.
45
+ */
46
+ export declare function resolveLayoutSpec(spec: LayoutSpecNode, allViewIds: string[] | undefined): ResolvedLayoutSpecNode;
47
+ /** A request to move one view relative to the others. Public plugin API. */
48
+ export interface PendingMove {
49
+ type: 'newTab' | 'splitRight';
50
+ viewId: string;
51
+ }
52
+ /**
53
+ * Build a layout tree from a spec.
54
+ *
55
+ * The old converter had to answer "which dockview group does a nested split
56
+ * address?" and could not, because dockview forces orientation to alternate by
57
+ * depth — which is why `size` was honoured only on the top-level split, and
58
+ * only if every panel there carried one. Here the spec's nesting is the tree's
59
+ * nesting, so `size` applies wherever it is written and there is no all-or-
60
+ * nothing pass.
61
+ */
62
+ export declare function treeFromSpec(spec: ResolvedLayoutSpecNode, nextId: (kind: NodeKind) => string): LayoutTree;
63
+ /** Every viewId a spec names, depth-first — the order it states. */
64
+ export declare function viewIdsInSpec(spec: ResolvedLayoutSpecNode): string[];
65
+ /** How "arrange everything" lays the whole session out. */
66
+ export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
67
+ /**
68
+ * Re-arrange every view at once, one view per cell.
69
+ *
70
+ * The four whole-workspace commands, as a spec. dockview had these as imperative
71
+ * re-tiling — walk every panel and re-add it relative to the first, in an order
72
+ * chosen so the grid came out right — and the grid case had to compute its own
73
+ * row/column arithmetic against `api.panels` because there was no way to state
74
+ * the shape. Here the shape IS the statement, so a tiling is a spec and goes
75
+ * through the same `applyLayoutSpec` a session spec does.
76
+ *
77
+ * `grid` fills row-major at ceil(sqrt(n)) columns, which is what the dockview
78
+ * version computed; a trailing row shorter than the rest keeps its cells at full
79
+ * width, since `resolveSizes` reads bare siblings as an even share of their own
80
+ * branch rather than of the grid.
81
+ */
82
+ export declare function tileLayoutSpec(viewIds: string[], mode: TileMode): ResolvedLayoutSpecNode;
83
+ /**
84
+ * `setPendingMove` as a spec: everything else keeps its side, the named view
85
+ * takes the other. With nothing else on screen there is nothing to split from,
86
+ * so the view just takes the space.
87
+ */
88
+ export declare function specForPendingMove(move: PendingMove, allViewIds: string[]): ResolvedLayoutSpecNode;
@@ -0,0 +1,226 @@
1
+ import { normalize } from './tree.js';
2
+ // `views`/`children` are pulled off by name; the rest pass through to the tree,
3
+ // so an unrecognized one is a key the writer expected to mean something
4
+ const layoutNodeKeys = new Set(['views', 'direction', 'children', 'size']);
5
+ /**
6
+ * Resolve every view a spec names to an id against `allViewIds`, the list its
7
+ * indexes count into. Anything that resolves to nothing throws, naming what was
8
+ * received: an untyped caller (the MCP `run_javascript` tool) that writes
9
+ * `viewIds` where `views` goes, or an index past the end, used to be accepted
10
+ * as an empty leaf and collapse the workspace into one blank tab with nothing
11
+ * said.
12
+ *
13
+ * A node stating no `views` and no `children` is NOT that slip — it is the
14
+ * empty panel `treeFromSpec` has always built, and refusing it here made the
15
+ * two surfaces disagree about the one shape they share. The slip is an
16
+ * unrecognized KEY, which is what `viewIds` actually is, and naming it beats
17
+ * the old message that could only say the node needed something else.
18
+ *
19
+ * `allViewIds` is undefined only on a model that composes no view list, where
20
+ * an id cannot be checked and an index cannot mean anything.
21
+ */
22
+ export function resolveLayoutSpec(spec, allViewIds) {
23
+ const describe = (value) => typeof value === 'string' ? `"${value}"` : String(value);
24
+ const resolveRef = (ref) => {
25
+ if (typeof ref === 'number') {
26
+ if (!allViewIds) {
27
+ throw new Error(`Layout names view index ${ref}, but this session has no view list for an index to count into; name the view by id`);
28
+ }
29
+ const id = allViewIds[ref];
30
+ if (id === undefined) {
31
+ throw new Error(`Layout names view index ${ref}, but the session has ${allViewIds.length} view(s)${allViewIds.length ? ` (indexes 0-${allViewIds.length - 1})` : ''}`);
32
+ }
33
+ return id;
34
+ }
35
+ if (typeof ref !== 'string') {
36
+ throw new Error(`Layout "views" entries are view indexes or view ids; received ${describe(ref)}`);
37
+ }
38
+ if (allViewIds && !allViewIds.includes(ref)) {
39
+ throw new Error(`Layout names view id ${describe(ref)}, which is not a view in this session (ids: ${allViewIds.map(describe).join(', ') || 'none'})`);
40
+ }
41
+ return ref;
42
+ };
43
+ const resolveNode = (node) => {
44
+ const { views, children, ...rest } = node;
45
+ const unknown = Object.keys(rest).filter(key => !layoutNodeKeys.has(key));
46
+ if (unknown.length > 0) {
47
+ throw new Error(`Layout node has unrecognized key(s) ${unknown.map(describe).join(', ')}; a leaf names its views with "views" (view indexes or ids) and a container nests "children"`);
48
+ }
49
+ if (views !== undefined && !Array.isArray(views)) {
50
+ throw new Error(`Layout "views" is an array of view indexes or ids; received ${describe(views)}`);
51
+ }
52
+ if (children !== undefined && !Array.isArray(children)) {
53
+ throw new Error(`Layout "children" is an array of layout nodes; received ${describe(children)}`);
54
+ }
55
+ return {
56
+ ...rest,
57
+ ...(views === undefined ? {} : { views: views.map(resolveRef) }),
58
+ ...(children === undefined
59
+ ? {}
60
+ : { children: children.map(resolveNode) }),
61
+ };
62
+ };
63
+ const resolved = resolveNode(spec);
64
+ const seen = new Set();
65
+ const repeated = new Set(viewIdsInSpec(resolved).filter(id => {
66
+ const already = seen.has(id);
67
+ seen.add(id);
68
+ return already;
69
+ }));
70
+ if (repeated.size > 0) {
71
+ throw new Error(`Layout seats view ${[...repeated].map(describe).join(', ')} in more than one cell; a view lives in exactly one tab, so name it once`);
72
+ }
73
+ return resolved;
74
+ }
75
+ /**
76
+ * The effective size of each child of one branch.
77
+ *
78
+ * `size` is documented as a **percentage**, so a spec that sizes some siblings
79
+ * and leaves the rest bare means the rest to share what is left over: `70` and
80
+ * blank is 70/30. Defaulting a bare sibling to 1 instead reads a weight against
81
+ * a percentage, and the panel comes out at 1/71 of the width — present, about a
82
+ * pixel wide, and reported by nothing.
83
+ *
84
+ * The mixed case used to be unreachable: dockview forced orientation to
85
+ * alternate by depth, so the converter discarded any partially-sized branch
86
+ * wholesale and raised a notification saying so. Nesting works now, which means
87
+ * this has to mean something instead.
88
+ *
89
+ * All-sized and none-sized both stay plain weights — `normalize` renormalises
90
+ * them, so `70`/`30` and `7`/`3` are the same layout and neither has to sum to
91
+ * anything in particular.
92
+ */
93
+ function resolveSizes(children) {
94
+ const stated = children.map(child => child.size);
95
+ const named = stated.filter(size => size !== undefined);
96
+ if (named.length === 0 || named.length === children.length) {
97
+ return stated.map(size => size ?? 1);
98
+ }
99
+ const claimed = named.reduce((a, b) => a + b, 0);
100
+ const remainder = 100 - claimed;
101
+ const share = remainder > 0
102
+ ? remainder / (children.length - named.length)
103
+ : // an over-subscribed branch has nothing left to hand out, so a bare
104
+ // sibling takes a typical share rather than collapsing to nothing
105
+ claimed / named.length;
106
+ return stated.map(size => size ?? share);
107
+ }
108
+ /**
109
+ * Build a layout tree from a spec.
110
+ *
111
+ * The old converter had to answer "which dockview group does a nested split
112
+ * address?" and could not, because dockview forces orientation to alternate by
113
+ * depth — which is why `size` was honoured only on the top-level split, and
114
+ * only if every panel there carried one. Here the spec's nesting is the tree's
115
+ * nesting, so `size` applies wherever it is written and there is no all-or-
116
+ * nothing pass.
117
+ */
118
+ export function treeFromSpec(spec, nextId) {
119
+ function build(node, size) {
120
+ if (node.views) {
121
+ return {
122
+ id: nextId('panel'),
123
+ size,
124
+ tabs: [{ id: nextId('tab'), viewIds: [...node.views] }],
125
+ activeTabId: undefined,
126
+ };
127
+ }
128
+ const children = node.children ?? [];
129
+ if (children.length === 0) {
130
+ return undefined;
131
+ }
132
+ // `tabs` is not a split: every child's views become a tab in one cell.
133
+ //
134
+ // A tab holds a FLAT stack of views, so a container child has no split to
135
+ // become — and the docs say containers nest arbitrarily deep, so one can be
136
+ // written. Its views are flattened into a single tab; they used to be
137
+ // dropped from the layout outright, which is invisible rather than wrong,
138
+ // since homing then swept them into whichever tab happened to be showing.
139
+ // `loadSessionSpec` reports the flattening, as it does the sizes below.
140
+ if (node.direction === 'tabs') {
141
+ const tabs = children.flatMap(child => {
142
+ const viewIds = viewIdsInSpec(child);
143
+ return child.views === undefined && viewIds.length === 0
144
+ ? []
145
+ : [{ id: nextId('tab'), viewIds }];
146
+ });
147
+ return {
148
+ id: nextId('panel'),
149
+ size,
150
+ tabs,
151
+ activeTabId: tabs[0]?.id,
152
+ };
153
+ }
154
+ const sizes = resolveSizes(children);
155
+ const built = children.flatMap((child, i) => {
156
+ const subtree = build(child, sizes[i]);
157
+ return subtree ? [subtree] : [];
158
+ });
159
+ return built.length === 0
160
+ ? undefined
161
+ : {
162
+ id: nextId('branch'),
163
+ size,
164
+ direction: node.direction === 'vertical' ? 'column' : 'row',
165
+ children: built,
166
+ };
167
+ }
168
+ const root = build(spec, spec.size ?? 1);
169
+ return root ? normalize(root) : { id: nextId('panel'), size: 1, tabs: [] };
170
+ }
171
+ /** Every viewId a spec names, depth-first — the order it states. */
172
+ export function viewIdsInSpec(spec) {
173
+ return [
174
+ ...(spec.views ?? []),
175
+ ...(spec.children ?? []).flatMap(viewIdsInSpec),
176
+ ];
177
+ }
178
+ /**
179
+ * Re-arrange every view at once, one view per cell.
180
+ *
181
+ * The four whole-workspace commands, as a spec. dockview had these as imperative
182
+ * re-tiling — walk every panel and re-add it relative to the first, in an order
183
+ * chosen so the grid came out right — and the grid case had to compute its own
184
+ * row/column arithmetic against `api.panels` because there was no way to state
185
+ * the shape. Here the shape IS the statement, so a tiling is a spec and goes
186
+ * through the same `applyLayoutSpec` a session spec does.
187
+ *
188
+ * `grid` fills row-major at ceil(sqrt(n)) columns, which is what the dockview
189
+ * version computed; a trailing row shorter than the rest keeps its cells at full
190
+ * width, since `resolveSizes` reads bare siblings as an even share of their own
191
+ * branch rather than of the grid.
192
+ */
193
+ export function tileLayoutSpec(viewIds, mode) {
194
+ // Nothing to arrange: one view is the whole workspace whatever the mode, and
195
+ // no views leaves the empty panel the tree already guarantees.
196
+ if (viewIds.length <= 1) {
197
+ return { views: [...viewIds] };
198
+ }
199
+ const cell = (id) => ({ views: [id] });
200
+ if (mode !== 'grid') {
201
+ return { direction: mode, children: viewIds.map(cell) };
202
+ }
203
+ const cols = Math.ceil(Math.sqrt(viewIds.length));
204
+ const rows = [];
205
+ for (let i = 0; i < viewIds.length; i += cols) {
206
+ rows.push({
207
+ direction: 'horizontal',
208
+ children: viewIds.slice(i, i + cols).map(cell),
209
+ });
210
+ }
211
+ return { direction: 'vertical', children: rows };
212
+ }
213
+ /**
214
+ * `setPendingMove` as a spec: everything else keeps its side, the named view
215
+ * takes the other. With nothing else on screen there is nothing to split from,
216
+ * so the view just takes the space.
217
+ */
218
+ export function specForPendingMove(move, allViewIds) {
219
+ const others = allViewIds.filter(id => id !== move.viewId);
220
+ return others.length > 0
221
+ ? {
222
+ direction: move.type === 'splitRight' ? 'horizontal' : 'tabs',
223
+ children: [{ views: others }, { views: [move.viewId] }],
224
+ }
225
+ : { views: [move.viewId] };
226
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Moving the boundary between two panes: pure, so it is checkable without a DOM
3
+ * or a synthetic pointer. The handle in `LayoutRenderer.tsx` only measures.
4
+ */
5
+ /**
6
+ * A pane never shrinks below this many pixels.
7
+ *
8
+ * dockview's `MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH`/`_HEIGHT`. A `flex-grow`
9
+ * share of zero is legal, so without it a pane can be dragged — or `Home`'d —
10
+ * to nothing, taking its tab strip and views with it.
11
+ */
12
+ export declare const MIN_PANE_PX = 100;
13
+ /** The space the two panes either side of the boundary before `index` share. */
14
+ export declare function pairSpan(sizes: number[], index: number): number;
15
+ /**
16
+ * `sizes` with that boundary moved to `position`, measured in the same units.
17
+ *
18
+ * The move stays inside the pair, so every other pane holds still. The pointer
19
+ * and the arrow keys are the same gesture at two resolutions and both land here.
20
+ *
21
+ * `pairPx` is the pair's current pixel span, needed only to convert
22
+ * `MIN_PANE_PX` into a share; 0 means nothing measurable (jsdom, or a pane not
23
+ * laid out yet) and gives the unconstrained clamp.
24
+ */
25
+ export declare function withBoundaryAt(sizes: number[], index: number, position: number, pairPx?: number): number[];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Moving the boundary between two panes: pure, so it is checkable without a DOM
3
+ * or a synthetic pointer. The handle in `LayoutRenderer.tsx` only measures.
4
+ */
5
+ /**
6
+ * A pane never shrinks below this many pixels.
7
+ *
8
+ * dockview's `MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH`/`_HEIGHT`. A `flex-grow`
9
+ * share of zero is legal, so without it a pane can be dragged — or `Home`'d —
10
+ * to nothing, taking its tab strip and views with it.
11
+ */
12
+ export const MIN_PANE_PX = 100;
13
+ /** The space the two panes either side of the boundary before `index` share. */
14
+ export function pairSpan(sizes, index) {
15
+ return sizes[index - 1] + sizes[index];
16
+ }
17
+ /**
18
+ * `sizes` with that boundary moved to `position`, measured in the same units.
19
+ *
20
+ * The move stays inside the pair, so every other pane holds still. The pointer
21
+ * and the arrow keys are the same gesture at two resolutions and both land here.
22
+ *
23
+ * `pairPx` is the pair's current pixel span, needed only to convert
24
+ * `MIN_PANE_PX` into a share; 0 means nothing measurable (jsdom, or a pane not
25
+ * laid out yet) and gives the unconstrained clamp.
26
+ */
27
+ export function withBoundaryAt(sizes, index, position, pairPx = 0) {
28
+ const pair = pairSpan(sizes, index);
29
+ // no room for two minimums: the boundary stops in the middle rather than
30
+ // pinning both ends past each other
31
+ const floor = pairPx > 0 ? Math.min((MIN_PANE_PX / pairPx) * pair, pair / 2) : 0;
32
+ const before = Math.min(Math.max(position, floor), pair - floor);
33
+ const next = [...sizes];
34
+ next[index - 1] = before;
35
+ next[index] = pair - before;
36
+ return next;
37
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The layout tree, as pure functions over plain snapshots.
3
+ *
4
+ * This is the half of an MST-native workspace that carries the risk, so it is
5
+ * deliberately not MST: no nodes, no parents, no lifecycle, nothing to be alive
6
+ * or dead. Every operation is `tree in -> tree out`, which means the invariants
7
+ * below can be tested exhaustively instead of observed to hold.
8
+ *
9
+ * The trade being made is worth naming, because it is not "complexity for no
10
+ * complexity". Reconciling with dockview is replaced by **normalisation**: a
11
+ * tree that has just had a panel removed, or a branch split, is usually not in
12
+ * canonical form, and every operation has to put it back. That is real work and
13
+ * it is where this design's bugs would live. What it is not is timing: there is
14
+ * no event, no re-entrancy, no window during which the tree is half-updated,
15
+ * and no second owner to disagree with.
16
+ */
17
+ /**
18
+ * One tab. Its content is a **vertical stack of views**, which is JBrowse's own
19
+ * concept and the reason a generic window manager never fits cleanly: dockview
20
+ * has a group holding tabs, and we need a third level under that.
21
+ */
22
+ export interface TabNode {
23
+ id: string;
24
+ /** membership only — `session.views` is the order (see WorkspaceLayout/CLAUDE.md) */
25
+ viewIds: string[];
26
+ /** set only when the user renames it; otherwise derived from the views */
27
+ title?: string;
28
+ }
29
+ /** A cell of the grid: the space a tab strip and one visible tab live in. */
30
+ export interface PanelNode {
31
+ id: string;
32
+ /** share of the parent branch's space, relative to its siblings */
33
+ size: number;
34
+ tabs: TabNode[];
35
+ activeTabId?: string;
36
+ }
37
+ export interface BranchNode {
38
+ id: string;
39
+ size: number;
40
+ direction: 'row' | 'column';
41
+ children: LayoutTree[];
42
+ }
43
+ export type LayoutTree = PanelNode | BranchNode;
44
+ /** What an id is being minted for. Ids are prefixed with it, so they read. */
45
+ export type NodeKind = 'panel' | 'tab' | 'branch';
46
+ export declare function isBranch(node: LayoutTree): node is BranchNode;
47
+ export declare function panels(node: LayoutTree): PanelNode[];
48
+ export declare function tabs(node: LayoutTree): TabNode[];
49
+ /** A tab and the panel holding it — there are no parent pointers to walk. */
50
+ export interface TabHome {
51
+ panel: PanelNode;
52
+ tab: TabNode;
53
+ }
54
+ export declare function findTab(node: LayoutTree, tabId: string): TabHome | undefined;
55
+ /** The tab a panel is showing: the one it names, or its first. */
56
+ export declare function activeTabIn(panel: PanelNode): TabNode | undefined;
57
+ export declare function tabContainingView(node: LayoutTree, viewId: string): TabHome | undefined;
58
+ export declare function panelContainingView(node: LayoutTree, viewId: string): PanelNode | undefined;
59
+ /**
60
+ * Put a tree back in canonical form. Four rules, applied bottom-up:
61
+ *
62
+ * 1. a branch with no children is dropped by its parent
63
+ * 2. a branch with one child is replaced by that child, which inherits the
64
+ * branch's size — so the space the branch occupied does not move
65
+ * 3. a branch directly inside a branch of the same direction is flattened into
66
+ * it, its children's sizes scaled to preserve their share of the whole
67
+ * 4. sizes are renormalised so a node's siblings always sum to 1
68
+ *
69
+ * Rule 3 is the one dockview cannot express: it forces orientation to alternate
70
+ * by depth, so `row` inside `row` is not representable and a nested split gets
71
+ * silently reparented. Here it is representable AND canonicalised, which is
72
+ * what makes `size` work at any depth rather than only on the top-level split.
73
+ *
74
+ * Empty panels are NOT dropped, and neither are empty tabs. A tab with no views
75
+ * is exactly what "new empty tab" creates — it shows the view launcher — so
76
+ * pruning empties wholesale would delete the thing the user just asked for.
77
+ */
78
+ export declare function normalize(node: LayoutTree): LayoutTree;
79
+ /**
80
+ * Split `panelId`, putting `newPanel` beside it in `direction`.
81
+ *
82
+ * Always builds a branch around the panel and lets `normalize` flatten it into
83
+ * the parent when the directions agree. Trying to decide up front whether to
84
+ * nest or to insert as a sibling is the same decision rule twice, and the two
85
+ * copies drift.
86
+ */
87
+ export declare function splitPanel(root: LayoutTree, panelId: string, direction: 'row' | 'column', newPanel: PanelNode, before?: boolean): LayoutTree;
88
+ /** Drop a panel. Its space goes back to its siblings via renormalisation. */
89
+ export declare function removePanel(root: LayoutTree, panelId: string): LayoutTree;
90
+ /** Set the sizes of one branch's children, e.g. from a splitter drag. */
91
+ export declare function setSizes(root: LayoutTree, branchId: string, sizes: number[]): LayoutTree;
92
+ export declare function addTab(root: LayoutTree, panelId: string, tab: TabNode): LayoutTree;
93
+ /**
94
+ * Close a tab. The panel stays — an empty panel shows the view launcher, which
95
+ * is a state the user can reach deliberately.
96
+ */
97
+ export declare function removeTab(root: LayoutTree, tabId: string): LayoutTree;
98
+ /**
99
+ * Move a tab into another panel, at `index` if given.
100
+ *
101
+ * One function returning one tree, so there is no instant at which the tab is
102
+ * in both panels or neither — the state the imperative bridge had to wrap an
103
+ * explicit `runInAction` around to hide from its own reconcile autorun.
104
+ *
105
+ * **`index` counts the tabs as they are ordered NOW**, before the move. That is
106
+ * the only reading a caller can supply, because it is the strip the user is
107
+ * looking at — and it differs from the post-removal ordering exactly when the
108
+ * tab is moving within its own panel and is currently to the LEFT of the gap it
109
+ * was dropped in. Dragging A to the gap between B and C in `[A, B, C]` is index
110
+ * 2 on screen; taking A out first makes that gap index 1, and inserting at 2
111
+ * lands it after C instead. Adjusting here rather than in the caller keeps the
112
+ * whole "remove then insert" mechanic inside the one function that does it.
113
+ *
114
+ * dockview reaches the same rule from the other end and it is worth knowing
115
+ * they agree, since this is the fiddliest arithmetic in the file: its tabs
116
+ * container computes `insertionIndex - (sourceIndex !== -1 && sourceIndex <
117
+ * insertionIndex ? 1 : 0)`, which is the condition below with the same bounds.
118
+ */
119
+ export declare function moveTabToPanel(root: LayoutTree, tabId: string, targetPanelId: string, index?: number): LayoutTree;
120
+ export declare function setActiveTab(root: LayoutTree, panelId: string, tabId: string): LayoutTree;
121
+ export declare function renameTab(root: LayoutTree, tabId: string, title: string | undefined): LayoutTree;
122
+ export declare function addViewToTab(root: LayoutTree, tabId: string, viewId: string): LayoutTree;
123
+ /** Take a view out of whatever tab holds it, leaving the tab in place. */
124
+ export declare function removeView(root: LayoutTree, viewId: string): LayoutTree;
125
+ /**
126
+ * Make the tree's membership agree with the session's list of views.
127
+ *
128
+ * The only reconciliation left, and one-directional: `session.views` owns which
129
+ * views exist, and nothing reads back.
130
+ *
131
+ * **`viewIds` is the session's WHOLE set.** A shorter list does not leave the
132
+ * rest alone — it unhomes them, and the next homing pass sweeps them into one
133
+ * tab.
134
+ */
135
+ export declare function homeViews(root: LayoutTree, viewIds: string[], activePanelId: string | undefined, nextTabId: () => string): LayoutTree;
136
+ /**
137
+ * Drop a panel that has been left with no tabs, unless it is the only one.
138
+ *
139
+ * Deliberately NOT a normalisation rule. A panel with no tabs is reachable on
140
+ * purpose, so pruning empties wholesale would delete a space the user asked
141
+ * for. It is a step the *drag* gesture takes about its own source panel,
142
+ * because dragging the last tab out of a split and leaving a blank half is the
143
+ * one place an empty panel is clearly not what was meant.
144
+ */
145
+ export declare function pruneEmptyPanel(root: LayoutTree, panelId: string): LayoutTree;
146
+ /**
147
+ * Drop a tab that a move has left with no views, unless it is the panel's last.
148
+ *
149
+ * The mirror of `pruneEmptyPanel`, and deliberately as narrow: an empty tab is
150
+ * legitimate (it shows the view launcher), so this only ever runs on the tab a
151
+ * move just emptied, never as a rule over the tree.
152
+ *
153
+ * Which is why closing a tab's last VIEW leaves the tab standing where a move
154
+ * out of it does not: the move acted somewhere else and left this behind, and
155
+ * the close acted here, where an empty tab is a usable place to open the next
156
+ * thing. A cell's last tab prunes because a blank cell is not usable.
157
+ */
158
+ export declare function pruneEmptyTabIn(root: LayoutTree, panelId: string, tabId: string): LayoutTree;