sh3-core 0.19.1 → 0.19.3
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.
- package/dist/Sh3.svelte +3 -1
- package/dist/actions/menuBarModel.js +8 -0
- package/dist/actions/menuBarModel.test.js +61 -0
- package/dist/app/admin/ApiKeysView.svelte +6 -5
- package/dist/app/store/PermissionConfirmModal.svelte +23 -0
- package/dist/app/store/PermissionConfirmModal.svelte.d.ts +1 -0
- package/dist/app/store/StoreView.svelte +6 -1
- package/dist/chrome/CompactChrome.svelte.test.js +7 -4
- package/dist/env/client.d.ts +5 -4
- package/dist/env/client.js +11 -17
- package/dist/env/serverUrl.d.ts +2 -0
- package/dist/env/serverUrl.js +8 -0
- package/dist/gestures/index.d.ts +17 -0
- package/dist/gestures/index.js +27 -0
- package/dist/keys/client.js +6 -7
- package/dist/keys/revocation-bus.svelte.js +11 -1
- package/dist/layout/compact/CarouselTabs.svelte +150 -14
- package/dist/layout/compact/CarouselTabs.svelte.test.js +222 -2
- package/dist/layout/compact/CompactRenderer.svelte +1 -1
- package/dist/layout/compact/CompactRenderer.svelte.test.js +5 -3
- package/dist/layout/compact/derive.js +7 -16
- package/dist/layout/compact/derive.test.js +30 -9
- package/dist/layout/drag.svelte.js +16 -3
- package/dist/layout/inspection.d.ts +20 -9
- package/dist/layout/inspection.js +66 -11
- package/dist/layout/inspection.svelte.test.d.ts +1 -0
- package/dist/layout/inspection.svelte.test.js +114 -0
- package/dist/layout/store.schemaVersion.test.js +2 -2
- package/dist/layout/types.d.ts +11 -8
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.js +2 -2
- package/dist/overlays/FloatFrame.svelte +93 -22
- package/dist/primitives/ResizableSplitter.svelte +42 -8
- package/dist/registry/checkFetch.d.ts +6 -0
- package/dist/registry/checkFetch.js +23 -0
- package/dist/sh3/views/KeysAndPeers.svelte +4 -3
- package/dist/shards/activate-runtime.test.js +99 -1
- package/dist/shards/activate.svelte.js +12 -3
- package/dist/shards/registry.d.ts +8 -1
- package/dist/shards/registry.js +13 -2
- package/dist/shards/registry.test.js +25 -4
- package/dist/shards/types.d.ts +14 -1
- package/dist/shell-shard/ScrollbackView.svelte +145 -67
- package/dist/shell-shard/ScrollbackView.svelte.test.d.ts +1 -0
- package/dist/shell-shard/ScrollbackView.svelte.test.js +182 -0
- package/dist/shell-shard/dispatch-gating.test.js +38 -2
- package/dist/shell-shard/dispatch.js +9 -1
- package/dist/shell-shard/registry-resolve.test.js +50 -0
- package/dist/shell-shard/registry.d.ts +2 -1
- package/dist/shell-shard/registry.js +12 -2
- package/dist/shell-shard/verbs/help.js +5 -4
- package/dist/shell-shard/verbs/help.svelte.test.js +5 -2
- package/dist/verbs/types.d.ts +10 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -128,26 +128,26 @@ describe('derive', () => {
|
|
|
128
128
|
describe('tabs nodes', () => {
|
|
129
129
|
it('tabs of body slots stay in body root', () => {
|
|
130
130
|
const tree = {
|
|
131
|
-
type: 'tabs', activeTab: 0,
|
|
131
|
+
type: 'tabs', activeTab: 0, role: 'body',
|
|
132
132
|
tabs: [
|
|
133
|
-
{ slotId: 't1', viewId: 'v:t1', label: 'Tab 1'
|
|
134
|
-
{ slotId: 't2', viewId: 'v:t2', label: 'Tab 2'
|
|
133
|
+
{ slotId: 't1', viewId: 'v:t1', label: 'Tab 1' },
|
|
134
|
+
{ slotId: 't2', viewId: 'v:t2', label: 'Tab 2' },
|
|
135
135
|
],
|
|
136
136
|
};
|
|
137
137
|
const result = derive(tree);
|
|
138
138
|
expect(result.bodyRoot.type).toBe('tabs');
|
|
139
139
|
});
|
|
140
|
-
it('tabs
|
|
140
|
+
it('body tabs node alongside sidebar slot stays in body root', () => {
|
|
141
141
|
var _a;
|
|
142
142
|
const tree = {
|
|
143
143
|
type: 'split', direction: 'horizontal', sizes: [0.2, 0.8],
|
|
144
144
|
children: [
|
|
145
145
|
{ type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
|
|
146
146
|
{
|
|
147
|
-
type: 'tabs', activeTab: 0,
|
|
147
|
+
type: 'tabs', activeTab: 0, role: 'body',
|
|
148
148
|
tabs: [
|
|
149
|
-
{ slotId: 't1', viewId: 'v:t1', label: 'Tab 1'
|
|
150
|
-
{ slotId: 't2', viewId: 'v:t2', label: 'Tab 2'
|
|
149
|
+
{ slotId: 't1', viewId: 'v:t1', label: 'Tab 1' },
|
|
150
|
+
{ slotId: 't2', viewId: 'v:t2', label: 'Tab 2' },
|
|
151
151
|
],
|
|
152
152
|
},
|
|
153
153
|
],
|
|
@@ -156,13 +156,33 @@ describe('derive', () => {
|
|
|
156
156
|
expect(result.bodyRoot.type).toBe('tabs');
|
|
157
157
|
expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb']);
|
|
158
158
|
});
|
|
159
|
+
it('sidebar-tagged tabs node lifts all tabs into a drawer', () => {
|
|
160
|
+
var _a;
|
|
161
|
+
const tree = {
|
|
162
|
+
type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
|
|
163
|
+
children: [
|
|
164
|
+
{
|
|
165
|
+
type: 'tabs', activeTab: 0, role: 'sidebar',
|
|
166
|
+
tabs: [
|
|
167
|
+
{ slotId: 'sb-1', viewId: 'v:sb-1', label: 'Files' },
|
|
168
|
+
{ slotId: 'sb-2', viewId: 'v:sb-2', label: 'Search' },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{ type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const result = derive(tree);
|
|
175
|
+
expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb-1', 'sb-2']);
|
|
176
|
+
expect(result.bodyRoot.slotId).toBe('body');
|
|
177
|
+
});
|
|
159
178
|
});
|
|
160
179
|
describe('carousels', () => {
|
|
161
180
|
it('includes a carousels Map in the output', () => {
|
|
162
181
|
var _a;
|
|
163
182
|
const tree = {
|
|
164
183
|
type: 'tabs',
|
|
165
|
-
|
|
184
|
+
role: 'body',
|
|
185
|
+
tabs: [{ slotId: 't0', viewId: null, label: 'Only' }],
|
|
166
186
|
activeTab: 0,
|
|
167
187
|
};
|
|
168
188
|
const out = derive(tree);
|
|
@@ -184,7 +204,8 @@ describe('derive', () => {
|
|
|
184
204
|
{ type: 'slot', slotId: 'sb', viewId: null, role: 'sidebar' },
|
|
185
205
|
{
|
|
186
206
|
type: 'tabs',
|
|
187
|
-
|
|
207
|
+
role: 'body',
|
|
208
|
+
tabs: [{ slotId: 't0', viewId: null, label: 'Body Tab' }],
|
|
188
209
|
activeTab: 0,
|
|
189
210
|
},
|
|
190
211
|
],
|
|
@@ -44,7 +44,7 @@ import { cleanupTree, insertTabIntoTabs, moveTabWithinTabs, removeTabBySlotId, s
|
|
|
44
44
|
import { layoutStore } from './store.svelte';
|
|
45
45
|
import { isEmptyContent } from './floats';
|
|
46
46
|
import { claim, revoke } from '../gestures/pointerClaim';
|
|
47
|
-
import { ancestorCount } from '../gestures';
|
|
47
|
+
import { ancestorCount, logGesture } from '../gestures';
|
|
48
48
|
export const dragState = $state({
|
|
49
49
|
phase: 'idle',
|
|
50
50
|
source: null,
|
|
@@ -116,6 +116,8 @@ function removeGlobalListeners() {
|
|
|
116
116
|
delete document.body.dataset.dragging;
|
|
117
117
|
}
|
|
118
118
|
function onPointerMove(e) {
|
|
119
|
+
if (e.pointerId !== activeDragPointerId)
|
|
120
|
+
return;
|
|
119
121
|
dragState.pointerX = e.clientX;
|
|
120
122
|
dragState.pointerY = e.clientY;
|
|
121
123
|
if (dragState.phase === 'pending') {
|
|
@@ -126,7 +128,9 @@ function onPointerMove(e) {
|
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
|
-
function onPointerUp(
|
|
131
|
+
function onPointerUp(e) {
|
|
132
|
+
if (e.pointerId !== activeDragPointerId)
|
|
133
|
+
return;
|
|
130
134
|
const wasDragging = dragState.phase === 'dragging';
|
|
131
135
|
if (wasDragging) {
|
|
132
136
|
commit();
|
|
@@ -137,7 +141,16 @@ function onPointerUp(_e) {
|
|
|
137
141
|
}
|
|
138
142
|
teardown();
|
|
139
143
|
}
|
|
140
|
-
function onPointerCancel(
|
|
144
|
+
function onPointerCancel(e) {
|
|
145
|
+
// Filter by pointer id. Without this, any pointercancel on the window
|
|
146
|
+
// (palm contact, ghost touch, stylus cancellation while a finger drag
|
|
147
|
+
// is active) would tear down a legitimate tab drag — same touch-only
|
|
148
|
+
// auto-release class as in carousel / floatframe / splitter.
|
|
149
|
+
if (e.pointerId !== activeDragPointerId) {
|
|
150
|
+
logGesture('tabdrag:cancel-other-id', e, { activeDragPointerId });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
logGesture('tabdrag:cancel-our-id', e, { activeDragPointerId });
|
|
141
154
|
teardown();
|
|
142
155
|
}
|
|
143
156
|
function rootNode(ref) {
|
|
@@ -69,19 +69,30 @@ export declare function popoutView(slotId: string): string | null;
|
|
|
69
69
|
export declare function dockFloat(floatId: string): boolean;
|
|
70
70
|
/**
|
|
71
71
|
* Dock a view into the currently-rendered layout without caring which
|
|
72
|
-
* root it is. Used by the Ctrl+` sh3 hotkey
|
|
73
|
-
* somewhere sensible" callers. Policy:
|
|
72
|
+
* root it is. Used by the Ctrl+` sh3 hotkey, the `open <viewId>` verb,
|
|
73
|
+
* and other "just put it somewhere sensible" callers. Policy:
|
|
74
74
|
*
|
|
75
75
|
* 1. If a tab with the same `viewId` already exists, focus it and
|
|
76
76
|
* return. Callers don't want a second instance of a singleton view
|
|
77
77
|
* every time they hit the shortcut.
|
|
78
|
-
* 2.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
78
|
+
* 2. Prefer a tabs group whose entries are explicitly flagged
|
|
79
|
+
* `role: 'body'` — that's the canonical "main content" target when
|
|
80
|
+
* authors marked it. This step matches user intent better than
|
|
81
|
+
* "first tabs group found" in layouts that pair a sidebar tabs
|
|
82
|
+
* group with a body tabs group.
|
|
83
|
+
* 3. Otherwise, prefer a standalone slot explicitly flagged
|
|
84
|
+
* `role: 'body'` — split it horizontally and place the new entry
|
|
85
|
+
* on the right (same shape as the generic slot fallback below).
|
|
86
|
+
* 4. Otherwise, append to the first tabs group found.
|
|
87
|
+
* 5. Otherwise, split the first slot leaf horizontally. This is the
|
|
88
|
+
* "floating window" fallback described in roadmap SH9 / DF3.
|
|
89
|
+
*
|
|
90
|
+
* Steps 2-3 use a *strict* role check (`=== 'body'`, not resolveRole).
|
|
91
|
+
* An unmarked slot defaults to body via resolveRole, but treating every
|
|
92
|
+
* unmarked slot as a body anchor would make step 3 swallow every
|
|
93
|
+
* standalone slot in the tree before step 4 ever got a chance — which
|
|
94
|
+
* inverts the documented fallback order. Only explicit body markers
|
|
95
|
+
* promote a node above the generic fallback.
|
|
85
96
|
*
|
|
86
97
|
* Returns true if the view was focused or inserted, false if the layout
|
|
87
98
|
* was empty or otherwise un-dockable.
|
|
@@ -274,19 +274,30 @@ function findFirstSlotPath(node, path = []) {
|
|
|
274
274
|
}
|
|
275
275
|
/**
|
|
276
276
|
* Dock a view into the currently-rendered layout without caring which
|
|
277
|
-
* root it is. Used by the Ctrl+` sh3 hotkey
|
|
278
|
-
* somewhere sensible" callers. Policy:
|
|
277
|
+
* root it is. Used by the Ctrl+` sh3 hotkey, the `open <viewId>` verb,
|
|
278
|
+
* and other "just put it somewhere sensible" callers. Policy:
|
|
279
279
|
*
|
|
280
280
|
* 1. If a tab with the same `viewId` already exists, focus it and
|
|
281
281
|
* return. Callers don't want a second instance of a singleton view
|
|
282
282
|
* every time they hit the shortcut.
|
|
283
|
-
* 2.
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
283
|
+
* 2. Prefer a tabs group whose entries are explicitly flagged
|
|
284
|
+
* `role: 'body'` — that's the canonical "main content" target when
|
|
285
|
+
* authors marked it. This step matches user intent better than
|
|
286
|
+
* "first tabs group found" in layouts that pair a sidebar tabs
|
|
287
|
+
* group with a body tabs group.
|
|
288
|
+
* 3. Otherwise, prefer a standalone slot explicitly flagged
|
|
289
|
+
* `role: 'body'` — split it horizontally and place the new entry
|
|
290
|
+
* on the right (same shape as the generic slot fallback below).
|
|
291
|
+
* 4. Otherwise, append to the first tabs group found.
|
|
292
|
+
* 5. Otherwise, split the first slot leaf horizontally. This is the
|
|
293
|
+
* "floating window" fallback described in roadmap SH9 / DF3.
|
|
294
|
+
*
|
|
295
|
+
* Steps 2-3 use a *strict* role check (`=== 'body'`, not resolveRole).
|
|
296
|
+
* An unmarked slot defaults to body via resolveRole, but treating every
|
|
297
|
+
* unmarked slot as a body anchor would make step 3 swallow every
|
|
298
|
+
* standalone slot in the tree before step 4 ever got a chance — which
|
|
299
|
+
* inverts the documented fallback order. Only explicit body markers
|
|
300
|
+
* promote a node above the generic fallback.
|
|
290
301
|
*
|
|
291
302
|
* Returns true if the view was focused or inserted, false if the layout
|
|
292
303
|
* was empty or otherwise un-dockable.
|
|
@@ -297,20 +308,64 @@ export function dockIntoActiveLayout(entry) {
|
|
|
297
308
|
// 1. Already present? Focus it.
|
|
298
309
|
if (focusView((_a = entry.viewId) !== null && _a !== void 0 ? _a : ''))
|
|
299
310
|
return true;
|
|
300
|
-
// 2.
|
|
311
|
+
// 2. A tabs group explicitly carrying body content wins.
|
|
312
|
+
const bodyTabs = findBodyTabsNode(root);
|
|
313
|
+
if (bodyTabs) {
|
|
314
|
+
bodyTabs.tabs.push(entry);
|
|
315
|
+
bodyTabs.activeTab = bodyTabs.tabs.length - 1;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
// 3. A standalone slot explicitly marked as body is next.
|
|
319
|
+
const bodySlotPath = findBodySlotPath(root);
|
|
320
|
+
if (bodySlotPath) {
|
|
321
|
+
splitNodeAtPath(root, bodySlotPath, entry, 'right');
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
// 4. Existing tabs group wins (generic fallback).
|
|
301
325
|
const tabs = findFirstTabsNode(root);
|
|
302
326
|
if (tabs) {
|
|
303
327
|
tabs.tabs.push(entry);
|
|
304
328
|
tabs.activeTab = tabs.tabs.length - 1;
|
|
305
329
|
return true;
|
|
306
330
|
}
|
|
307
|
-
//
|
|
331
|
+
// 5. Fallback: split the first valid slot leaf.
|
|
308
332
|
const slotPath = findFirstSlotPath(root);
|
|
309
333
|
if (!slotPath)
|
|
310
334
|
return false;
|
|
311
335
|
splitNodeAtPath(root, slotPath, entry, 'right');
|
|
312
336
|
return true;
|
|
313
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Find a tabs group explicitly flagged `role: 'body'`. Strict —
|
|
340
|
+
* defaulted body roles don't count, see the dockIntoActiveLayout
|
|
341
|
+
* docblock for why.
|
|
342
|
+
*/
|
|
343
|
+
function findBodyTabsNode(node) {
|
|
344
|
+
if (node.type === 'tabs') {
|
|
345
|
+
return node.role === 'body' ? node : null;
|
|
346
|
+
}
|
|
347
|
+
if (node.type === 'split') {
|
|
348
|
+
for (const c of node.children) {
|
|
349
|
+
const hit = findBodyTabsNode(c);
|
|
350
|
+
if (hit)
|
|
351
|
+
return hit;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
/** Path to the first slot leaf explicitly flagged `role: 'body'`. */
|
|
357
|
+
function findBodySlotPath(node, path = []) {
|
|
358
|
+
if (node.type === 'slot')
|
|
359
|
+
return node.role === 'body' ? path : null;
|
|
360
|
+
if (node.type === 'split') {
|
|
361
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
362
|
+
const hit = findBodySlotPath(node.children[i], [...path, i]);
|
|
363
|
+
if (hit)
|
|
364
|
+
return hit;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
314
369
|
/**
|
|
315
370
|
* Find which root a slot currently lives under in the active layout.
|
|
316
371
|
* Returns `{ kind: 'docked' }` when the slot is anywhere in the docked
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* dockIntoActiveLayout policy tests — verify the body-role preference
|
|
3
|
+
* lands before generic fallbacks. The `open <viewId>` verb routes
|
|
4
|
+
* through this function; getting the policy wrong means views land in
|
|
5
|
+
* sidebar tabs groups instead of body ones in apps that mark both.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { flushSync } from 'svelte';
|
|
9
|
+
import { attachApp, switchToApp, __resetLayoutStoreForTest, layoutStore, } from './store.svelte';
|
|
10
|
+
import { dockIntoActiveLayout } from './inspection';
|
|
11
|
+
let appCounter = 0;
|
|
12
|
+
function makeApp(initialLayout) {
|
|
13
|
+
// Unique id per call so workspace-zone storage from one test doesn't
|
|
14
|
+
// adapt-and-override another test's initialLayout via attachApp's
|
|
15
|
+
// version-gate path.
|
|
16
|
+
return {
|
|
17
|
+
manifest: { id: `test-app-${++appCounter}`, layoutVersion: 1 },
|
|
18
|
+
initialLayout,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function nextEntry(viewId) {
|
|
22
|
+
return { slotId: `s:${viewId}:${Math.random()}`, viewId, label: viewId };
|
|
23
|
+
}
|
|
24
|
+
describe('dockIntoActiveLayout — body-role preference', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
__resetLayoutStoreForTest();
|
|
27
|
+
});
|
|
28
|
+
it('appends into a tabs group flagged as body when one exists alongside a non-body tabs group', () => {
|
|
29
|
+
attachApp(makeApp({
|
|
30
|
+
type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
|
|
31
|
+
children: [
|
|
32
|
+
{
|
|
33
|
+
type: 'tabs',
|
|
34
|
+
role: 'sidebar',
|
|
35
|
+
tabs: [{ slotId: 'sb-1', viewId: 'sb:explorer', label: 'Explorer' }],
|
|
36
|
+
activeTab: 0,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'tabs',
|
|
40
|
+
role: 'body',
|
|
41
|
+
tabs: [{ slotId: 'b-1', viewId: 'body:editor', label: 'Editor' }],
|
|
42
|
+
activeTab: 0,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}));
|
|
46
|
+
switchToApp();
|
|
47
|
+
flushSync();
|
|
48
|
+
expect(dockIntoActiveLayout(nextEntry('body:newdoc'))).toBe(true);
|
|
49
|
+
const root = layoutStore.root;
|
|
50
|
+
const sidebar = root.children[0];
|
|
51
|
+
const body = root.children[1];
|
|
52
|
+
expect(sidebar.tabs.map((t) => t.viewId)).toEqual(['sb:explorer']);
|
|
53
|
+
expect(body.tabs.map((t) => t.viewId)).toEqual(['body:editor', 'body:newdoc']);
|
|
54
|
+
expect(body.activeTab).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
it('splits a standalone slot flagged as body when no body tabs group exists', () => {
|
|
57
|
+
attachApp(makeApp({
|
|
58
|
+
type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
|
|
59
|
+
children: [
|
|
60
|
+
{ type: 'slot', slotId: 'sb', viewId: 'sb:explorer', role: 'sidebar' },
|
|
61
|
+
{ type: 'slot', slotId: 'main', viewId: 'body:editor', role: 'body' },
|
|
62
|
+
],
|
|
63
|
+
}));
|
|
64
|
+
switchToApp();
|
|
65
|
+
flushSync();
|
|
66
|
+
expect(dockIntoActiveLayout(nextEntry('body:newdoc'))).toBe(true);
|
|
67
|
+
const root = layoutStore.root;
|
|
68
|
+
// The body slot got split — its position in the parent is now a split node.
|
|
69
|
+
const right = root.children[1];
|
|
70
|
+
expect(right.type).toBe('split');
|
|
71
|
+
});
|
|
72
|
+
it('falls back to the first tabs group when no body-flagged target exists', () => {
|
|
73
|
+
attachApp(makeApp({
|
|
74
|
+
type: 'tabs',
|
|
75
|
+
tabs: [{ slotId: 'a', viewId: 'view:a', label: 'A' }],
|
|
76
|
+
activeTab: 0,
|
|
77
|
+
}));
|
|
78
|
+
switchToApp();
|
|
79
|
+
flushSync();
|
|
80
|
+
expect(dockIntoActiveLayout(nextEntry('view:b'))).toBe(true);
|
|
81
|
+
const root = layoutStore.root;
|
|
82
|
+
expect(root.tabs.map((t) => t.viewId)).toEqual(['view:a', 'view:b']);
|
|
83
|
+
expect(root.activeTab).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
it('does NOT prefer a tabs group whose entries are only defaulted-to-body (strict role check)', () => {
|
|
86
|
+
// The "first tabs group" already wins the generic fallback in this
|
|
87
|
+
// shape — the assertion here is that body-preference doesn't kick in
|
|
88
|
+
// (which it would if we used resolveRole and treated unset as body)
|
|
89
|
+
// and accidentally promote a sibling tabs group above the first one.
|
|
90
|
+
attachApp(makeApp({
|
|
91
|
+
type: 'split', direction: 'horizontal', sizes: [0.5, 0.5],
|
|
92
|
+
children: [
|
|
93
|
+
{
|
|
94
|
+
type: 'tabs',
|
|
95
|
+
tabs: [{ slotId: 'l', viewId: 'view:l', label: 'L' }], // role unset
|
|
96
|
+
activeTab: 0,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'tabs',
|
|
100
|
+
tabs: [{ slotId: 'r', viewId: 'view:r', label: 'R' }], // role unset
|
|
101
|
+
activeTab: 0,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}));
|
|
105
|
+
switchToApp();
|
|
106
|
+
flushSync();
|
|
107
|
+
expect(dockIntoActiveLayout(nextEntry('view:new'))).toBe(true);
|
|
108
|
+
const root = layoutStore.root;
|
|
109
|
+
const left = root.children[0];
|
|
110
|
+
const right = root.children[1];
|
|
111
|
+
expect(left.tabs.map((t) => t.viewId)).toEqual(['view:l', 'view:new']);
|
|
112
|
+
expect(right.tabs.map((t) => t.viewId)).toEqual(['view:r']);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -29,7 +29,7 @@ describe('layout schema v4 → v5 backward compatibility', () => {
|
|
|
29
29
|
expect(slotA.role).toBeUndefined();
|
|
30
30
|
expect(v4Blob.drawers).toBeUndefined();
|
|
31
31
|
});
|
|
32
|
-
it('LAYOUT_SCHEMA_VERSION is
|
|
33
|
-
expect(LAYOUT_SCHEMA_VERSION).toBe(
|
|
32
|
+
it('LAYOUT_SCHEMA_VERSION is 7', () => {
|
|
33
|
+
expect(LAYOUT_SCHEMA_VERSION).toBe(7);
|
|
34
34
|
});
|
|
35
35
|
});
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -5,8 +5,9 @@ export type SplitDirection = 'horizontal' | 'vertical';
|
|
|
5
5
|
* viewports to derive a compact rendering (sidebars/inspectors lift into
|
|
6
6
|
* drawer surfaces, body slots fill the page).
|
|
7
7
|
*
|
|
8
|
-
* Default `'body'`. Authored on a slot or
|
|
9
|
-
* to
|
|
8
|
+
* Default `'body'`. Authored on a slot node or a tabs node (one role
|
|
9
|
+
* per tabs group, applied to every tab); if unset, falls back to the
|
|
10
|
+
* view's `defaultRole` (registered via the shard contract). See
|
|
10
11
|
* `layout/compact/resolveRole.ts`.
|
|
11
12
|
*/
|
|
12
13
|
export type SlotRole = 'body' | 'sidebar' | 'inspector';
|
|
@@ -56,11 +57,6 @@ export interface TabEntry {
|
|
|
56
57
|
label: string;
|
|
57
58
|
/** Optional icon hint (not yet rendered in phase 8). */
|
|
58
59
|
icon?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Slot-role hint for compact rendering. Default `'body'` via
|
|
61
|
-
* `resolveRole(slot, viewDefault)`. Inert on desktop.
|
|
62
|
-
*/
|
|
63
|
-
role?: SlotRole;
|
|
64
60
|
/**
|
|
65
61
|
* Caller-supplied instance data, threaded to `MountContext.meta`.
|
|
66
62
|
* Ephemeral — not serialized with the layout tree.
|
|
@@ -97,6 +93,13 @@ export interface TabsNode {
|
|
|
97
93
|
* Inert when not rendered as a carousel.
|
|
98
94
|
*/
|
|
99
95
|
wrap?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Role hint for the whole tab group in compact rendering. Default
|
|
98
|
+
* `'body'` via `resolveRole(node, viewDefault)`. Inert on desktop.
|
|
99
|
+
* Applies to every tab in the group — mixed-role tab groups are not
|
|
100
|
+
* representable by design.
|
|
101
|
+
*/
|
|
102
|
+
role?: SlotRole;
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
105
|
* A leaf layout node that holds a single mounted view. `slotId` is the stable
|
|
@@ -215,7 +218,7 @@ export type TreeRootRef = {
|
|
|
215
218
|
* the default tree takes over — phase 7 deliberately does not ship a
|
|
216
219
|
* migration framework, only the hook for one.
|
|
217
220
|
*/
|
|
218
|
-
export declare const LAYOUT_SCHEMA_VERSION =
|
|
221
|
+
export declare const LAYOUT_SCHEMA_VERSION = 7;
|
|
219
222
|
/**
|
|
220
223
|
* The wire shape of a persisted layout in the workspace state zone.
|
|
221
224
|
* One blob per sh3 (or per program, once per-program layouts exist);
|
package/dist/layout/types.js
CHANGED
|
@@ -20,7 +20,7 @@ describe('TabsNode.wrap', () => {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
describe('LAYOUT_SCHEMA_VERSION', () => {
|
|
23
|
-
it('is
|
|
24
|
-
expect(LAYOUT_SCHEMA_VERSION).toBe(
|
|
23
|
+
it('is 7 (bumped: role moved from TabEntry to TabsNode)', () => {
|
|
24
|
+
expect(LAYOUT_SCHEMA_VERSION).toBe(7);
|
|
25
25
|
});
|
|
26
26
|
});
|