sh3-core 0.7.3 → 0.8.0
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/__test__/fixtures.d.ts +12 -0
- package/dist/__test__/fixtures.js +62 -0
- package/dist/__test__/render.d.ts +3 -0
- package/dist/__test__/render.js +11 -0
- package/dist/__test__/reset.d.ts +14 -0
- package/dist/__test__/reset.js +34 -0
- package/dist/__test__/setup-dom.d.ts +1 -0
- package/dist/__test__/setup-dom.js +26 -0
- package/dist/__test__/smoke.test.d.ts +1 -0
- package/dist/__test__/smoke.test.js +28 -0
- package/dist/api.d.ts +15 -2
- package/dist/api.js +13 -1
- package/dist/app/store/StoreView.svelte +36 -7
- package/dist/app/store/storeShard.svelte.js +9 -3
- package/dist/app/store/verbs.js +8 -2
- package/dist/apps/lifecycle.d.ts +11 -0
- package/dist/apps/lifecycle.js +48 -11
- package/dist/apps/lifecycle.test.d.ts +1 -0
- package/dist/apps/lifecycle.test.js +309 -0
- package/dist/apps/registry.svelte.d.ts +2 -0
- package/dist/apps/registry.svelte.js +5 -0
- package/dist/apps/types.d.ts +24 -2
- package/dist/createShell.d.ts +2 -0
- package/dist/createShell.js +9 -7
- package/dist/documents/handle.js +5 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/documents/journal-hook.d.ts +6 -0
- package/dist/documents/journal-hook.js +16 -0
- package/dist/documents/sync/activate-integration.test.d.ts +1 -0
- package/dist/documents/sync/activate-integration.test.js +37 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +99 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +15 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte +70 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +12 -0
- package/dist/documents/sync/conflicts.d.ts +30 -0
- package/dist/documents/sync/conflicts.js +77 -0
- package/dist/documents/sync/conflicts.test.d.ts +1 -0
- package/dist/documents/sync/conflicts.test.js +71 -0
- package/dist/documents/sync/engine.d.ts +19 -0
- package/dist/documents/sync/engine.js +188 -0
- package/dist/documents/sync/engine.test.d.ts +1 -0
- package/dist/documents/sync/engine.test.js +169 -0
- package/dist/documents/sync/handle.d.ts +11 -0
- package/dist/documents/sync/handle.js +79 -0
- package/dist/documents/sync/handle.test.d.ts +1 -0
- package/dist/documents/sync/handle.test.js +56 -0
- package/dist/documents/sync/hash.d.ts +1 -0
- package/dist/documents/sync/hash.js +13 -0
- package/dist/documents/sync/hash.test.d.ts +1 -0
- package/dist/documents/sync/hash.test.js +20 -0
- package/dist/documents/sync/index.d.ts +6 -0
- package/dist/documents/sync/index.js +12 -0
- package/dist/documents/sync/journal.d.ts +30 -0
- package/dist/documents/sync/journal.js +179 -0
- package/dist/documents/sync/journal.test.d.ts +1 -0
- package/dist/documents/sync/journal.test.js +87 -0
- package/dist/documents/sync/registry.d.ts +10 -0
- package/dist/documents/sync/registry.js +66 -0
- package/dist/documents/sync/registry.test.d.ts +1 -0
- package/dist/documents/sync/registry.test.js +42 -0
- package/dist/documents/sync/serialization.d.ts +5 -0
- package/dist/documents/sync/serialization.js +24 -0
- package/dist/documents/sync/serialization.test.d.ts +1 -0
- package/dist/documents/sync/serialization.test.js +26 -0
- package/dist/documents/sync/singleton.d.ts +11 -0
- package/dist/documents/sync/singleton.js +26 -0
- package/dist/documents/sync/tombstones.d.ts +19 -0
- package/dist/documents/sync/tombstones.js +58 -0
- package/dist/documents/sync/tombstones.test.d.ts +1 -0
- package/dist/documents/sync/tombstones.test.js +37 -0
- package/dist/documents/sync/types.d.ts +116 -0
- package/dist/documents/sync/types.js +27 -0
- package/dist/documents/sync/write-hook.test.d.ts +1 -0
- package/dist/documents/sync/write-hook.test.js +36 -0
- package/dist/env/client.d.ts +10 -5
- package/dist/env/client.js +12 -4
- package/dist/layout/LayoutRenderer.browser.test.d.ts +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +274 -0
- package/dist/layout/LayoutRenderer.svelte +2 -1
- package/dist/layout/LayoutRenderer.test.d.ts +1 -0
- package/dist/layout/LayoutRenderer.test.js +143 -0
- package/dist/layout/SlotContainer.svelte +8 -2
- package/dist/layout/SlotDropZone.svelte +19 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-1-drag-tab-between-groups-moves-a-tab-from-one-tabs-group-to-another-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-2-drag-tab-to-quadrant-creates-a-split-when-dropping-a-tab-on-a-quadrant-drop-zone-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-4-close-policy-removes-closable-tabs--keeps-non-closable--and-awaits-canClose-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
- package/dist/layout/drag.svelte.d.ts +5 -0
- package/dist/layout/drag.svelte.js +15 -0
- package/dist/layout/slotHostPool.svelte.d.ts +16 -1
- package/dist/layout/slotHostPool.svelte.js +123 -5
- package/dist/layout/slotHostPool.test.d.ts +1 -0
- package/dist/layout/slotHostPool.test.js +104 -0
- package/dist/layout/store.svelte.d.ts +22 -0
- package/dist/layout/store.svelte.js +78 -16
- package/dist/layout/tree-walk.d.ts +2 -0
- package/dist/layout/tree-walk.js +1 -1
- package/dist/layout/types.d.ts +5 -0
- package/dist/overlays/float.d.ts +2 -0
- package/dist/overlays/float.js +4 -1
- package/dist/overlays/float.test.js +102 -1
- package/dist/primitives/ResizableSplitter.svelte +2 -0
- package/dist/primitives/TabbedPanel.svelte +4 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +2 -0
- package/dist/registry/installer.d.ts +10 -7
- package/dist/registry/installer.js +39 -35
- package/dist/registry/register.d.ts +17 -0
- package/dist/registry/register.js +22 -0
- package/dist/registry/register.test.d.ts +1 -0
- package/dist/registry/register.test.js +28 -0
- package/dist/shards/activate.svelte.d.ts +6 -0
- package/dist/shards/activate.svelte.js +33 -2
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +18 -0
- package/dist/shards/types.d.ts +16 -1
- package/dist/shell-shard/Terminal.svelte +140 -33
- package/dist/shell-shard/Terminal.svelte.d.ts +3 -0
- package/dist/shell-shard/auto-relocate.d.ts +12 -0
- package/dist/shell-shard/auto-relocate.js +20 -0
- package/dist/shell-shard/auto-relocate.test.d.ts +1 -0
- package/dist/shell-shard/auto-relocate.test.js +35 -0
- package/dist/shell-shard/dispatch.d.ts +15 -0
- package/dist/shell-shard/dispatch.js +56 -0
- package/dist/shell-shard/modes/builtin.d.ts +5 -0
- package/dist/shell-shard/modes/builtin.js +18 -0
- package/dist/shell-shard/modes/prefs.d.ts +5 -0
- package/dist/shell-shard/modes/prefs.js +31 -0
- package/dist/shell-shard/modes/prefs.test.d.ts +1 -0
- package/dist/shell-shard/modes/prefs.test.js +46 -0
- package/dist/shell-shard/modes/registry.d.ts +7 -0
- package/dist/shell-shard/modes/registry.js +27 -0
- package/dist/shell-shard/modes/registry.test.d.ts +1 -0
- package/dist/shell-shard/modes/registry.test.js +35 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/modes/types.js +1 -0
- package/dist/shell-shard/protocol.d.ts +6 -0
- package/dist/shell-shard/shellShard.svelte.js +5 -1
- package/dist/shell-shard/tenant-fs-client.d.ts +24 -0
- package/dist/shell-shard/tenant-fs-client.js +44 -0
- package/dist/shell-shard/tenant-fs-client.test.d.ts +1 -0
- package/dist/shell-shard/tenant-fs-client.test.js +49 -0
- package/dist/shell-shard/terminal-dispatch.test.d.ts +1 -0
- package/dist/shell-shard/terminal-dispatch.test.js +53 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte +62 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte +28 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte.d.ts +7 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +102 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte +17 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte.d.ts +6 -0
- package/dist/shell-shard/toolbar/slots.d.ts +17 -0
- package/dist/shell-shard/toolbar/slots.js +26 -0
- package/dist/shell-shard/toolbar/slots.test.d.ts +1 -0
- package/dist/shell-shard/toolbar/slots.test.js +28 -0
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +34 -0
- package/dist/shell-shard/verbs/cd.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cd.test.js +56 -0
- package/dist/shell-shard/verbs/env.d.ts +2 -0
- package/dist/shell-shard/verbs/env.js +14 -0
- package/dist/shell-shard/verbs/index.js +6 -1
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +29 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +49 -0
- package/dist/shell-shard/verbs/session.d.ts +0 -1
- package/dist/shell-shard/verbs/session.js +58 -26
- package/dist/verbs/types.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { resetFramework } from '../__test__/reset';
|
|
3
|
+
import { makeApp, makeShard, makeAppManifest, makeShardManifest, makeTabsNode, makeTabEntry, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
4
|
+
import { launchApp, returnToHome, unregisterApp } from './lifecycle';
|
|
5
|
+
import { registerApp } from './registry.svelte';
|
|
6
|
+
import { registerShard } from '../shards/activate.svelte';
|
|
7
|
+
import { presetManager } from '../overlays/presets';
|
|
8
|
+
import { layoutStore } from '../layout/store.svelte';
|
|
9
|
+
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
10
|
+
import { renderWithShell } from '../__test__/render';
|
|
11
|
+
import { registerView } from '../shards/registry';
|
|
12
|
+
import { tick } from 'svelte';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Scenario A.1 — step order
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
describe('launchApp — scenario A.1 step order', () => {
|
|
17
|
+
beforeEach(resetFramework);
|
|
18
|
+
it('runs attachApp → activate shards → acquireAppSlotHolds → app.activate → switchToApp → onAppReady', async () => {
|
|
19
|
+
const order = [];
|
|
20
|
+
const shard = makeShard({
|
|
21
|
+
manifest: makeShardManifest({ id: 'shard-A' }),
|
|
22
|
+
activate: () => {
|
|
23
|
+
order.push('shard.activate');
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
registerShard(shard);
|
|
27
|
+
const app = makeApp({
|
|
28
|
+
manifest: makeAppManifest({
|
|
29
|
+
id: 'app-1',
|
|
30
|
+
requiredShards: ['shard-A'],
|
|
31
|
+
}),
|
|
32
|
+
activate: () => {
|
|
33
|
+
order.push('app.activate');
|
|
34
|
+
},
|
|
35
|
+
onAppReady: () => {
|
|
36
|
+
order.push('app.onAppReady');
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
registerApp(app);
|
|
40
|
+
await launchApp('app-1');
|
|
41
|
+
const iShard = order.indexOf('shard.activate');
|
|
42
|
+
const iApp = order.indexOf('app.activate');
|
|
43
|
+
const iReady = order.indexOf('app.onAppReady');
|
|
44
|
+
expect(iShard).toBeGreaterThanOrEqual(0);
|
|
45
|
+
expect(iApp).toBeGreaterThan(iShard);
|
|
46
|
+
expect(iReady).toBeGreaterThan(iApp);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Scenario A.2 — shard activate failure rolls back attach
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
describe('launchApp — scenario A.2 shard failure', () => {
|
|
53
|
+
beforeEach(resetFramework);
|
|
54
|
+
it('detaches the app and re-throws when a required shard throws during activate', async () => {
|
|
55
|
+
const badShard = makeShard({
|
|
56
|
+
manifest: makeShardManifest({ id: 'bad' }),
|
|
57
|
+
activate: () => {
|
|
58
|
+
throw new Error('boom');
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
registerShard(badShard);
|
|
62
|
+
const app = makeApp({
|
|
63
|
+
manifest: makeAppManifest({ id: 'app-2', requiredShards: ['bad'] }),
|
|
64
|
+
});
|
|
65
|
+
registerApp(app);
|
|
66
|
+
const { getAttachedAppId } = await import('../layout/store.svelte');
|
|
67
|
+
await expect(launchApp('app-2')).rejects.toThrow('boom');
|
|
68
|
+
expect(getAttachedAppId()).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Scenario A.3 — re-entry from home uses resume, skips shard re-activation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
describe('launchApp — scenario A.3 re-entry from home', () => {
|
|
75
|
+
beforeEach(resetFramework);
|
|
76
|
+
it('fires resume and onAppReady on re-entry, does not re-activate shards', async () => {
|
|
77
|
+
const shardActivate = vi.fn();
|
|
78
|
+
const shard = makeShard({
|
|
79
|
+
manifest: makeShardManifest({ id: 'shard-R' }),
|
|
80
|
+
activate: shardActivate,
|
|
81
|
+
});
|
|
82
|
+
registerShard(shard);
|
|
83
|
+
const appResume = vi.fn();
|
|
84
|
+
const appReady = vi.fn();
|
|
85
|
+
const app = makeApp({
|
|
86
|
+
manifest: makeAppManifest({ id: 'app-3', requiredShards: ['shard-R'] }),
|
|
87
|
+
resume: appResume,
|
|
88
|
+
onAppReady: appReady,
|
|
89
|
+
});
|
|
90
|
+
registerApp(app);
|
|
91
|
+
await launchApp('app-3');
|
|
92
|
+
expect(shardActivate).toHaveBeenCalledTimes(1);
|
|
93
|
+
await returnToHome();
|
|
94
|
+
await launchApp('app-3');
|
|
95
|
+
expect(shardActivate).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(appResume).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(appReady).toHaveBeenCalledTimes(2);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Scenario A.4 — returnToHome then launch(same) fast path
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
describe('launchApp — scenario A.4 fast path', () => {
|
|
104
|
+
beforeEach(resetFramework);
|
|
105
|
+
it('does not call shard.deactivate or shard.activate when relaunching the same app from home', async () => {
|
|
106
|
+
const shardActivate = vi.fn();
|
|
107
|
+
const shardDeactivate = vi.fn();
|
|
108
|
+
registerShard(makeShard({
|
|
109
|
+
manifest: makeShardManifest({ id: 'shard-F' }),
|
|
110
|
+
activate: shardActivate,
|
|
111
|
+
deactivate: shardDeactivate,
|
|
112
|
+
}));
|
|
113
|
+
registerApp(makeApp({
|
|
114
|
+
manifest: makeAppManifest({ id: 'app-4', requiredShards: ['shard-F'] }),
|
|
115
|
+
}));
|
|
116
|
+
await launchApp('app-4');
|
|
117
|
+
await returnToHome();
|
|
118
|
+
await launchApp('app-4');
|
|
119
|
+
expect(shardActivate).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(shardDeactivate).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Scenario A.5 — missing required shard fails fast
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
describe('launchApp — scenario A.5 missing shard', () => {
|
|
127
|
+
beforeEach(resetFramework);
|
|
128
|
+
it('throws before attachApp when a required shard is not registered', async () => {
|
|
129
|
+
registerApp(makeApp({
|
|
130
|
+
manifest: makeAppManifest({ id: 'app-5', requiredShards: ['missing'] }),
|
|
131
|
+
}));
|
|
132
|
+
const { getAttachedAppId } = await import('../layout/store.svelte');
|
|
133
|
+
await expect(launchApp('app-5')).rejects.toThrow(/missing/);
|
|
134
|
+
expect(getAttachedAppId()).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Scenario B.1 — presets.switch mutates synchronously
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
describe('presets — scenario B.1 sync mutation', () => {
|
|
141
|
+
beforeEach(resetFramework);
|
|
142
|
+
it('mutates the active-preset blob synchronously on switch', async () => {
|
|
143
|
+
const app = makeApp({
|
|
144
|
+
manifest: makeAppManifest({ id: 'presets-1' }),
|
|
145
|
+
initialLayout: [
|
|
146
|
+
{ name: 'one', tree: makeTree(makeTabsNode([makeTabEntry({ slotId: 'a', label: 'A' })])) },
|
|
147
|
+
{ name: 'two', tree: makeTree(makeTabsNode([makeTabEntry({ slotId: 'b', label: 'B' })])) },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
registerApp(app);
|
|
151
|
+
await launchApp('presets-1');
|
|
152
|
+
expect(presetManager.active()).toBe('one');
|
|
153
|
+
presetManager.switch('two');
|
|
154
|
+
expect(presetManager.active()).toBe('two');
|
|
155
|
+
expect(layoutStore.root).toMatchObject({ type: 'tabs' });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Scenario B.2 — shard.activate can call presets.switch
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
describe('presets — scenario B.2 switch from shard.activate', () => {
|
|
162
|
+
beforeEach(resetFramework);
|
|
163
|
+
it('does not throw "no app attached" when a shard calls presets.switch from activate', async () => {
|
|
164
|
+
registerShard(makeShard({
|
|
165
|
+
manifest: makeShardManifest({ id: 'switcher' }),
|
|
166
|
+
activate: () => {
|
|
167
|
+
presetManager.switch('alt');
|
|
168
|
+
},
|
|
169
|
+
}));
|
|
170
|
+
registerApp(makeApp({
|
|
171
|
+
manifest: makeAppManifest({ id: 'presets-2', requiredShards: ['switcher'] }),
|
|
172
|
+
initialLayout: [
|
|
173
|
+
{ name: 'default', tree: makeTree(makeSlotNode('x')) },
|
|
174
|
+
{ name: 'alt', tree: makeTree(makeSlotNode('y')) },
|
|
175
|
+
],
|
|
176
|
+
}));
|
|
177
|
+
await expect(launchApp('presets-2')).resolves.not.toThrow();
|
|
178
|
+
expect(presetManager.active()).toBe('alt');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Scenario B.3 — unknown preset throws
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
describe('presets — scenario B.3 unknown name', () => {
|
|
185
|
+
beforeEach(resetFramework);
|
|
186
|
+
it('throws with a useful message when switching to an unknown preset', async () => {
|
|
187
|
+
registerApp(makeApp({
|
|
188
|
+
manifest: makeAppManifest({ id: 'presets-3' }),
|
|
189
|
+
initialLayout: [{ name: 'only', tree: makeTree(makeSlotNode('x')) }],
|
|
190
|
+
}));
|
|
191
|
+
await launchApp('presets-3');
|
|
192
|
+
expect(() => presetManager.switch('nope')).toThrow(/nope/);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Scenario B.4 — round-trip preserves customization
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
describe('presets — scenario B.4 round-trip preserves customization', () => {
|
|
199
|
+
beforeEach(resetFramework);
|
|
200
|
+
it('preserves per-preset sizes and activeTab across switch A → B → A', async () => {
|
|
201
|
+
var _a;
|
|
202
|
+
registerApp(makeApp({
|
|
203
|
+
manifest: makeAppManifest({ id: 'presets-4' }),
|
|
204
|
+
initialLayout: [
|
|
205
|
+
{
|
|
206
|
+
name: 'A',
|
|
207
|
+
tree: makeTree(makeTabsNode([
|
|
208
|
+
makeTabEntry({ slotId: 't1', label: 'T1' }),
|
|
209
|
+
makeTabEntry({ slotId: 't2', label: 'T2' }),
|
|
210
|
+
])),
|
|
211
|
+
},
|
|
212
|
+
{ name: 'B', tree: makeTree(makeSlotNode('solo')) },
|
|
213
|
+
],
|
|
214
|
+
}));
|
|
215
|
+
await launchApp('presets-4');
|
|
216
|
+
const rootA = layoutStore.root;
|
|
217
|
+
expect(rootA === null || rootA === void 0 ? void 0 : rootA.type).toBe('tabs');
|
|
218
|
+
if ((rootA === null || rootA === void 0 ? void 0 : rootA.type) === 'tabs')
|
|
219
|
+
rootA.activeTab = 1;
|
|
220
|
+
presetManager.switch('B');
|
|
221
|
+
expect((_a = layoutStore.root) === null || _a === void 0 ? void 0 : _a.type).toBe('slot');
|
|
222
|
+
presetManager.switch('A');
|
|
223
|
+
const back = layoutStore.root;
|
|
224
|
+
expect(back === null || back === void 0 ? void 0 : back.type).toBe('tabs');
|
|
225
|
+
if ((back === null || back === void 0 ? void 0 : back.type) === 'tabs')
|
|
226
|
+
expect(back.activeTab).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Scenario B.5 — post-launch switch re-renders (TDD + fix)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
describe('presets — scenario B.5 post-launch switch re-renders', () => {
|
|
233
|
+
beforeEach(resetFramework);
|
|
234
|
+
it('updates the rendered DOM when presets.switch is called after launchApp', async () => {
|
|
235
|
+
registerView('test:slot-view', {
|
|
236
|
+
mount(container, ctx) {
|
|
237
|
+
const span = document.createElement('span');
|
|
238
|
+
span.dataset.viewFor = ctx.slotId;
|
|
239
|
+
span.textContent = ctx.slotId;
|
|
240
|
+
container.appendChild(span);
|
|
241
|
+
return { unmount: () => span.remove() };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
registerApp(makeApp({
|
|
245
|
+
manifest: makeAppManifest({ id: 'presets-5' }),
|
|
246
|
+
initialLayout: [
|
|
247
|
+
{ name: 'one', tree: makeTree(makeSlotNode('slot-one', 'test:slot-view')) },
|
|
248
|
+
{ name: 'two', tree: makeTree(makeSlotNode('slot-two', 'test:slot-view')) },
|
|
249
|
+
],
|
|
250
|
+
}));
|
|
251
|
+
await launchApp('presets-5');
|
|
252
|
+
const { container } = renderWithShell(LayoutRenderer, { path: [] });
|
|
253
|
+
await tick();
|
|
254
|
+
expect(container.querySelector('[data-view-for="slot-one"]')).toBeTruthy();
|
|
255
|
+
presetManager.switch('two');
|
|
256
|
+
await tick();
|
|
257
|
+
expect(container.querySelector('[data-view-for="slot-two"]')).toBeTruthy();
|
|
258
|
+
expect(container.querySelector('[data-view-for="slot-one"]')).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// unregisterApp — android-style force-close + removal
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
describe('unregisterApp', () => {
|
|
265
|
+
beforeEach(resetFramework);
|
|
266
|
+
it('removes an inactive app from the registry', async () => {
|
|
267
|
+
const { registeredApps } = await import('./registry.svelte');
|
|
268
|
+
const app = makeApp({ manifest: makeAppManifest({ id: 'app-u1' }) });
|
|
269
|
+
registerApp(app);
|
|
270
|
+
expect(registeredApps.has('app-u1')).toBe(true);
|
|
271
|
+
unregisterApp('app-u1');
|
|
272
|
+
expect(registeredApps.has('app-u1')).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
it('force-closes to home if the app being unregistered is active', async () => {
|
|
275
|
+
const { activeApp, registeredApps } = await import('./registry.svelte');
|
|
276
|
+
const shard = makeShard({ manifest: makeShardManifest({ id: 'shard-U' }) });
|
|
277
|
+
registerShard(shard);
|
|
278
|
+
const app = makeApp({
|
|
279
|
+
manifest: makeAppManifest({ id: 'app-u2', requiredShards: ['shard-U'] }),
|
|
280
|
+
});
|
|
281
|
+
registerApp(app);
|
|
282
|
+
await launchApp('app-u2');
|
|
283
|
+
expect(activeApp.id).toBe('app-u2');
|
|
284
|
+
unregisterApp('app-u2');
|
|
285
|
+
expect(activeApp.id).toBeNull();
|
|
286
|
+
expect(registeredApps.has('app-u2')).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
it('is a no-op for unknown ids', async () => {
|
|
289
|
+
expect(() => unregisterApp('does-not-exist')).not.toThrow();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// installPackage — evict-before-register (via registerLoadedBundle)
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
describe('installPackage evict-before-register (simulated via registerLoadedBundle)', () => {
|
|
296
|
+
beforeEach(resetFramework);
|
|
297
|
+
it('replaces an existing shard entry when a new version is registered', async () => {
|
|
298
|
+
var _a, _b;
|
|
299
|
+
const { registerLoadedBundle } = await import('../registry/register');
|
|
300
|
+
const { deactivateShard, registeredShards } = await import('../shards/activate.svelte');
|
|
301
|
+
const s1 = makeShard({ manifest: makeShardManifest({ id: 'S', version: '' }) });
|
|
302
|
+
registerLoadedBundle({ shards: [s1], apps: [] }, { version: '1.0.0', sourceRegistry: '', contractVersion: '1' });
|
|
303
|
+
expect((_a = registeredShards.get('S')) === null || _a === void 0 ? void 0 : _a.manifest.version).toBe('1.0.0');
|
|
304
|
+
deactivateShard('S');
|
|
305
|
+
const s2 = makeShard({ manifest: makeShardManifest({ id: 'S', version: '' }) });
|
|
306
|
+
registerLoadedBundle({ shards: [s2], apps: [] }, { version: '1.0.1', sourceRegistry: '', contractVersion: '1' });
|
|
307
|
+
expect((_b = registeredShards.get('S')) === null || _b === void 0 ? void 0 : _b.manifest.version).toBe('1.0.1');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -38,3 +38,5 @@ export declare function getActiveApp(): AppManifest | null;
|
|
|
38
38
|
* activate hook. Not re-exported through `api.ts`.
|
|
39
39
|
*/
|
|
40
40
|
export declare function getRegisteredApp(id: string): App | undefined;
|
|
41
|
+
/** Test-only reset: clear registered apps and the active-app pointer. */
|
|
42
|
+
export declare function __resetAppRegistryForTest(): void;
|
|
@@ -57,3 +57,8 @@ export function getActiveApp() {
|
|
|
57
57
|
export function getRegisteredApp(id) {
|
|
58
58
|
return registeredApps.get(id);
|
|
59
59
|
}
|
|
60
|
+
/** Test-only reset: clear registered apps and the active-app pointer. */
|
|
61
|
+
export function __resetAppRegistryForTest() {
|
|
62
|
+
registeredApps.clear();
|
|
63
|
+
activeApp.id = null;
|
|
64
|
+
}
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -39,8 +39,9 @@ export interface AppManifest {
|
|
|
39
39
|
/**
|
|
40
40
|
* Optional permissions this app requests beyond the default sandbox.
|
|
41
41
|
* Declared in the manifest and surfaced to the user at install time
|
|
42
|
-
* by the store app. Currently recognized:
|
|
43
|
-
* shard zone access.
|
|
42
|
+
* by the store app. Currently recognized:
|
|
43
|
+
* - 'state:manage' — cross-shard zone access.
|
|
44
|
+
* - 'documents:sync' — cross-shard document sync API.
|
|
44
45
|
*/
|
|
45
46
|
permissions?: string[];
|
|
46
47
|
}
|
|
@@ -61,6 +62,10 @@ export interface AppContext {
|
|
|
61
62
|
* Cross-shard zone management API. Only present when the app's
|
|
62
63
|
* manifest declares the `'state:manage'` permission. Check with
|
|
63
64
|
* `if (ctx.zones)` before use.
|
|
65
|
+
*
|
|
66
|
+
* Related permissions also recognized by the framework:
|
|
67
|
+
* - 'documents:sync' — cross-shard document sync API (exposed on
|
|
68
|
+
* shard contexts as `ctx.sync()`, not on app contexts).
|
|
64
69
|
*/
|
|
65
70
|
zones?: ZoneManager;
|
|
66
71
|
}
|
|
@@ -96,6 +101,23 @@ export interface App {
|
|
|
96
101
|
* same `AppContext` that `activate` received.
|
|
97
102
|
*/
|
|
98
103
|
resume?(ctx: AppContext): void | Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Called after the framework has switched the rendered root to this
|
|
106
|
+
* app's layout (i.e. `activeLayout()` now returns the app's active
|
|
107
|
+
* preset tree). Fires on both first launch (after `activate`) and
|
|
108
|
+
* on re-entry from home (after `resume`). This is the earliest hook
|
|
109
|
+
* from which layout-mutation APIs like `spliceIntoActiveLayout`,
|
|
110
|
+
* `focusTab`, `dockIntoActiveLayout` reliably target the app's tree.
|
|
111
|
+
*
|
|
112
|
+
* Use this for boot UX that needs to act on the rendered layout —
|
|
113
|
+
* e.g. reopening a last-used document, restoring tab state, or
|
|
114
|
+
* inserting a "welcome" tab into the current preset. Setup that
|
|
115
|
+
* does not touch the rendered layout (registering views, hydrating
|
|
116
|
+
* state, starting bus subscriptions) belongs in `activate`.
|
|
117
|
+
*
|
|
118
|
+
* See ADR-014.
|
|
119
|
+
*/
|
|
120
|
+
onAppReady?(ctx: AppContext): void | Promise<void>;
|
|
99
121
|
}
|
|
100
122
|
/**
|
|
101
123
|
* Source-declared shape of an app manifest — what external package authors
|
package/dist/createShell.d.ts
CHANGED
package/dist/createShell.js
CHANGED
|
@@ -15,8 +15,10 @@ import { __setEnvServerUrl } from './env/index';
|
|
|
15
15
|
import { __setTenantId } from './documents/config';
|
|
16
16
|
import { initFromBoot } from './auth/index';
|
|
17
17
|
import SignInWall from './auth/SignInWall.svelte';
|
|
18
|
+
import { loadBundleModule } from './registry/loader';
|
|
19
|
+
import { registerLoadedBundle } from './registry/register';
|
|
18
20
|
export async function createShell(config) {
|
|
19
|
-
var _a, _b, _c;
|
|
21
|
+
var _a, _b, _c, _d, _e;
|
|
20
22
|
const sUrl = (_a = config === null || config === void 0 ? void 0 : config.serverUrl) !== null && _a !== void 0 ? _a : '';
|
|
21
23
|
// 1. Platform detection
|
|
22
24
|
const platform = await resolvePlatform();
|
|
@@ -45,7 +47,7 @@ export async function createShell(config) {
|
|
|
45
47
|
bootConfig = await res.json();
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
catch (
|
|
50
|
+
catch (_f) {
|
|
49
51
|
// Server unreachable — boot without auth (offline mode)
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -73,7 +75,6 @@ export async function createShell(config) {
|
|
|
73
75
|
}
|
|
74
76
|
// 5. Load server-discovered packages
|
|
75
77
|
if ((_c = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _c === void 0 ? void 0 : _c.length) {
|
|
76
|
-
const { loadBundleModule } = await import('./registry/loader');
|
|
77
78
|
for (const pkg of config.discoveredPackages) {
|
|
78
79
|
try {
|
|
79
80
|
const res = await fetch(pkg.bundleUrl);
|
|
@@ -83,10 +84,11 @@ export async function createShell(config) {
|
|
|
83
84
|
}
|
|
84
85
|
const bytes = await res.arrayBuffer();
|
|
85
86
|
const loaded = await loadBundleModule(bytes);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
registerLoadedBundle(loaded, {
|
|
88
|
+
version: pkg.version,
|
|
89
|
+
sourceRegistry: (_d = pkg.sourceRegistry) !== null && _d !== void 0 ? _d : '',
|
|
90
|
+
contractVersion: (_e = pkg.contractVersion) !== null && _e !== void 0 ? _e : '',
|
|
91
|
+
});
|
|
90
92
|
console.log(`[sh3] Loaded discovered package: ${pkg.id}`);
|
|
91
93
|
}
|
|
92
94
|
catch (err) {
|
package/dist/documents/handle.js
CHANGED
|
@@ -18,6 +18,8 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
18
18
|
};
|
|
19
19
|
var _AutosaveControllerImpl_instances, _AutosaveControllerImpl_handle, _AutosaveControllerImpl_path, _AutosaveControllerImpl_debounceMs, _AutosaveControllerImpl_pending, _AutosaveControllerImpl_timer, _AutosaveControllerImpl_dirty, _AutosaveControllerImpl_disposed, _AutosaveControllerImpl_scheduleFlush, _AutosaveControllerImpl_clearTimer;
|
|
20
20
|
import { documentChanges } from './notifications';
|
|
21
|
+
import { notifyJournal } from './journal-hook';
|
|
22
|
+
import { hashContent } from './sync/hash';
|
|
21
23
|
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
22
24
|
/**
|
|
23
25
|
* Create a document handle scoped to a tenant, shard, and file filter.
|
|
@@ -53,10 +55,13 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
|
|
|
53
55
|
const existed = await backend.exists(tenantId, shardId, path);
|
|
54
56
|
await backend.write(tenantId, shardId, path, content);
|
|
55
57
|
emitChange(existed ? 'update' : 'create', path);
|
|
58
|
+
const hash = await hashContent(content);
|
|
59
|
+
await notifyJournal({ shardId, path, op: 'upsert', hash });
|
|
56
60
|
},
|
|
57
61
|
async delete(path) {
|
|
58
62
|
await backend.delete(tenantId, shardId, path);
|
|
59
63
|
emitChange('delete', path);
|
|
64
|
+
await notifyJournal({ shardId, path, op: 'delete', hash: null });
|
|
60
65
|
},
|
|
61
66
|
async exists(path) {
|
|
62
67
|
return backend.exists(tenantId, shardId, path);
|
|
@@ -4,3 +4,4 @@ export { HttpDocumentBackend } from './http-backend';
|
|
|
4
4
|
export { createDocumentHandle } from './handle';
|
|
5
5
|
export { documentChanges } from './notifications';
|
|
6
6
|
export { getTenantId, getDocumentBackend, __setTenantId, __setDocumentBackend, } from './config';
|
|
7
|
+
export * from './sync';
|
package/dist/documents/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { HttpDocumentBackend } from './http-backend';
|
|
|
6
6
|
export { createDocumentHandle } from './handle';
|
|
7
7
|
export { documentChanges } from './notifications';
|
|
8
8
|
export { getTenantId, getDocumentBackend, __setTenantId, __setDocumentBackend, } from './config';
|
|
9
|
+
export * from './sync';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { JournalEntry } from './sync/types';
|
|
2
|
+
type Appender = (entry: Omit<JournalEntry, 'seq' | 'ts'>) => Promise<void>;
|
|
3
|
+
export declare function setJournalAppender(fn: Appender): void;
|
|
4
|
+
export declare function clearJournalAppender(): void;
|
|
5
|
+
export declare function notifyJournal(entry: Omit<JournalEntry, 'seq' | 'ts'>): Promise<void>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Journal appender hook — lets the sync engine subscribe to regular
|
|
3
|
+
* shard writes/deletes without creating an import cycle between the
|
|
4
|
+
* document handle and the sync subsystem.
|
|
5
|
+
*/
|
|
6
|
+
let appender = null;
|
|
7
|
+
export function setJournalAppender(fn) {
|
|
8
|
+
appender = fn;
|
|
9
|
+
}
|
|
10
|
+
export function clearJournalAppender() {
|
|
11
|
+
appender = null;
|
|
12
|
+
}
|
|
13
|
+
export async function notifyJournal(entry) {
|
|
14
|
+
if (appender)
|
|
15
|
+
await appender(entry);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../config';
|
|
4
|
+
import { __resetShardRegistryForTest, registerShard, activateShard } from '../../shards/activate.svelte';
|
|
5
|
+
import { __resetSyncBundlesForTest } from './singleton';
|
|
6
|
+
import { PERMISSION_DOCUMENTS_SYNC } from './types';
|
|
7
|
+
describe('ctx.sync() gating', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
__resetShardRegistryForTest();
|
|
10
|
+
__resetSyncBundlesForTest();
|
|
11
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
12
|
+
__setTenantId('tenant-a');
|
|
13
|
+
});
|
|
14
|
+
it('is undefined without documents:sync permission', async () => {
|
|
15
|
+
let captured;
|
|
16
|
+
const shard = {
|
|
17
|
+
manifest: { id: 's-none', version: '0', views: [] },
|
|
18
|
+
activate: async (ctx) => { captured = ctx; },
|
|
19
|
+
};
|
|
20
|
+
registerShard(shard);
|
|
21
|
+
await activateShard('s-none');
|
|
22
|
+
expect(captured.sync).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
it('is a function when documents:sync is declared', async () => {
|
|
25
|
+
let captured;
|
|
26
|
+
const shard = {
|
|
27
|
+
manifest: { id: 's-sync', version: '0', views: [], permissions: [PERMISSION_DOCUMENTS_SYNC] },
|
|
28
|
+
activate: async (ctx) => { captured = ctx; },
|
|
29
|
+
};
|
|
30
|
+
registerShard(shard);
|
|
31
|
+
await activateShard('s-sync');
|
|
32
|
+
expect(typeof captured.sync).toBe('function');
|
|
33
|
+
const h = captured.sync();
|
|
34
|
+
expect(h.connectorId).toBe('s-sync');
|
|
35
|
+
expect(await h.grantedScopes()).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import SyncGrantPicker from './SyncGrantPicker.svelte';
|
|
4
|
+
import { createSyncRegistry, type SyncRegistry } from '../registry';
|
|
5
|
+
import { getDocumentBackend, getTenantId } from '../../config';
|
|
6
|
+
import type { GrantRecord, SyncScope, ConflictResolution } from '../types';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
10
|
+
connectorId?: string;
|
|
11
|
+
/** Shard IDs whose conflict artifacts should be listed. */
|
|
12
|
+
conflictShardIds?: string[];
|
|
13
|
+
/** Pending grant request, if any — embeds the picker when set. */
|
|
14
|
+
pendingRequest?: { connectorId: string; scope: SyncScope };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { connectorId, conflictShardIds = [], pendingRequest }: Props = $props();
|
|
18
|
+
|
|
19
|
+
let registry: SyncRegistry | null = $state(null);
|
|
20
|
+
let grants: GrantRecord[] = $state([]);
|
|
21
|
+
let conflicts: ConflictResolution[] = $state([]);
|
|
22
|
+
|
|
23
|
+
async function refresh() {
|
|
24
|
+
if (!registry) return;
|
|
25
|
+
grants = await registry.list(connectorId);
|
|
26
|
+
const all: ConflictResolution[] = [];
|
|
27
|
+
for (const shardId of conflictShardIds) {
|
|
28
|
+
all.push(...await registry.listConflicts(shardId));
|
|
29
|
+
}
|
|
30
|
+
conflicts = all;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onMount(async () => {
|
|
34
|
+
registry = createSyncRegistry(getDocumentBackend(), getTenantId());
|
|
35
|
+
await refresh();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function revoke(record: GrantRecord) {
|
|
39
|
+
if (!registry) return;
|
|
40
|
+
await registry.revoke(record.connectorId, record.scope);
|
|
41
|
+
await refresh();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function describeScope(s: SyncScope): string {
|
|
45
|
+
if (s.kind === 'tenant') return 'entire tenant';
|
|
46
|
+
if (s.kind === 'shard') return `shard:${s.shardId}`;
|
|
47
|
+
return `shard:${s.shardId}/${s.prefix}`;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<section class="document-sync-explorer" part="container">
|
|
52
|
+
<h2 part="title">Document Sync</h2>
|
|
53
|
+
|
|
54
|
+
{#if pendingRequest}
|
|
55
|
+
<SyncGrantPicker
|
|
56
|
+
connectorId={pendingRequest.connectorId}
|
|
57
|
+
scope={pendingRequest.scope}
|
|
58
|
+
onGranted={refresh}
|
|
59
|
+
/>
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
62
|
+
<h3 part="subtitle">Granted scopes</h3>
|
|
63
|
+
{#if grants.length === 0}
|
|
64
|
+
<p part="empty">No scopes granted yet.</p>
|
|
65
|
+
{:else}
|
|
66
|
+
<ul part="grants">
|
|
67
|
+
{#each grants as g}
|
|
68
|
+
<li>
|
|
69
|
+
<span part="grant-connector">{g.connectorId}</span>
|
|
70
|
+
<span part="grant-scope">{describeScope(g.scope)}</span>
|
|
71
|
+
<button type="button" onclick={() => revoke(g)} part="revoke">Revoke</button>
|
|
72
|
+
</li>
|
|
73
|
+
{/each}
|
|
74
|
+
</ul>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
<h3 part="subtitle">Conflicts</h3>
|
|
78
|
+
{#if conflicts.length === 0}
|
|
79
|
+
<p part="empty">No active conflicts.</p>
|
|
80
|
+
{:else}
|
|
81
|
+
<ul part="conflicts">
|
|
82
|
+
{#each conflicts as c}
|
|
83
|
+
<li>
|
|
84
|
+
<code part="conflict-path">{c.shardId}:{c.path}</code>
|
|
85
|
+
<small part="conflict-artifact">{c.conflictArtifactPath}</small>
|
|
86
|
+
</li>
|
|
87
|
+
{/each}
|
|
88
|
+
</ul>
|
|
89
|
+
{/if}
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.document-sync-explorer {
|
|
94
|
+
display: grid;
|
|
95
|
+
gap: 0.75rem;
|
|
96
|
+
}
|
|
97
|
+
ul { list-style: none; padding: 0; margin: 0; }
|
|
98
|
+
li { display: flex; gap: 0.5rem; align-items: center; }
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SyncScope } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
4
|
+
connectorId?: string;
|
|
5
|
+
/** Shard IDs whose conflict artifacts should be listed. */
|
|
6
|
+
conflictShardIds?: string[];
|
|
7
|
+
/** Pending grant request, if any — embeds the picker when set. */
|
|
8
|
+
pendingRequest?: {
|
|
9
|
+
connectorId: string;
|
|
10
|
+
scope: SyncScope;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
declare const DocumentSyncExplorer: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type DocumentSyncExplorer = ReturnType<typeof DocumentSyncExplorer>;
|
|
15
|
+
export default DocumentSyncExplorer;
|