sh3-core 0.17.0 → 0.19.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/Sh3.svelte +107 -39
- package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +16 -1
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +1 -21
- package/dist/app/store/version.d.ts +11 -0
- package/dist/app/store/version.js +39 -0
- package/dist/app/store/version.test.d.ts +1 -0
- package/dist/app/store/version.test.js +44 -0
- package/dist/apps/lifecycle.d.ts +6 -0
- package/dist/apps/lifecycle.js +5 -2
- package/dist/apps/lifecycle.test.js +30 -0
- package/dist/apps/types.d.ts +12 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +5 -0
- package/dist/assets/icons.svg +31 -0
- package/dist/auth/auth.svelte.js +18 -8
- package/dist/auth/types.d.ts +6 -0
- package/dist/chrome/CompactChrome.svelte +130 -0
- package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
- package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
- package/dist/chrome/CompactChrome.svelte.test.js +174 -0
- package/dist/chrome/MenuSheet.svelte +224 -0
- package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
- package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/MenuSheet.svelte.test.js +46 -0
- package/dist/createShell.d.ts +9 -0
- package/dist/createShell.js +20 -7
- package/dist/createShell.remoteAuth.test.d.ts +1 -0
- package/dist/createShell.remoteAuth.test.js +71 -0
- package/dist/documents/http-backend.js +12 -11
- package/dist/env/client.js +11 -5
- package/dist/files/types.d.ts +106 -0
- package/dist/files/types.js +1 -0
- package/dist/gestures/gestureRegistry.d.ts +6 -0
- package/dist/gestures/gestureRegistry.js +190 -0
- package/dist/gestures/gestureRegistry.test.d.ts +1 -0
- package/dist/gestures/gestureRegistry.test.js +119 -0
- package/dist/gestures/index.d.ts +6 -0
- package/dist/gestures/index.js +12 -0
- package/dist/gestures/pointerClaim.d.ts +7 -0
- package/dist/gestures/pointerClaim.js +36 -0
- package/dist/gestures/pointerClaim.test.d.ts +1 -0
- package/dist/gestures/pointerClaim.test.js +64 -0
- package/dist/gestures/types.d.ts +83 -0
- package/dist/gestures/types.js +1 -0
- package/dist/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/host-entry.d.ts +1 -0
- package/dist/host-entry.js +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +15 -3
- package/dist/layout/LayoutRenderer.svelte +27 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
- 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-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
- package/dist/layout/compact/CarouselTabs.svelte +361 -0
- package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
- package/dist/layout/compact/CompactRenderer.svelte +53 -0
- package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +157 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +197 -0
- package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
- package/dist/layout/compact/drawerStore.svelte.js +75 -0
- package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
- package/dist/layout/compact/enrichCarousels.d.ts +8 -0
- package/dist/layout/compact/enrichCarousels.js +44 -0
- package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
- package/dist/layout/compact/enrichCarousels.test.js +88 -0
- package/dist/layout/compact/resolveRole.d.ts +6 -0
- package/dist/layout/compact/resolveRole.js +13 -0
- package/dist/layout/compact/resolveRole.test.d.ts +1 -0
- package/dist/layout/compact/resolveRole.test.js +18 -0
- package/dist/layout/compact/types.d.ts +30 -0
- package/dist/layout/compact/types.js +15 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/presets.compactVariant.test.d.ts +1 -0
- package/dist/layout/presets.compactVariant.test.js +27 -0
- package/dist/layout/presets.d.ts +12 -0
- package/dist/layout/presets.js +16 -0
- package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
- package/dist/layout/store.drawers.svelte.test.js +49 -0
- package/dist/layout/store.schemaVersion.test.d.ts +1 -0
- package/dist/layout/store.schemaVersion.test.js +35 -0
- package/dist/layout/store.svelte.js +52 -2
- package/dist/layout/types.d.ts +51 -1
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.d.ts +1 -0
- package/dist/layout/types.test.js +26 -0
- package/dist/overlays/DrawerSurface.svelte +141 -0
- package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
- package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
- package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
- package/dist/overlays/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/OverlayRoots.svelte +12 -9
- package/dist/overlays/floatDismiss.js +5 -0
- package/dist/overlays/focusTrap.d.ts +11 -1
- package/dist/overlays/focusTrap.js +11 -9
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/popup.js +4 -0
- package/dist/overlays/types.d.ts +10 -1
- package/dist/primitives/Button.svelte +18 -0
- package/dist/primitives/Button.svelte.d.ts +6 -0
- package/dist/primitives/ResizableSplitter.svelte +71 -11
- package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
- package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
- package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
- package/dist/server-shard/types.d.ts +2 -1
- package/dist/sh3Api/headless.js +9 -1
- package/dist/sh3Api/headless.svelte.test.js +45 -1
- package/dist/sh3Runtime.svelte.d.ts +36 -0
- package/dist/sh3Runtime.svelte.js +33 -0
- package/dist/shards/activate.svelte.js +10 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +66 -0
- package/dist/shards/types.d.ts +22 -1
- package/dist/tokens.css +3 -2
- package/dist/transport/apiFetch.d.ts +1 -0
- package/dist/transport/apiFetch.js +65 -0
- package/dist/transport/apiFetch.test.d.ts +1 -0
- package/dist/transport/apiFetch.test.js +37 -0
- package/dist/transport/authToken.d.ts +2 -0
- package/dist/transport/authToken.js +53 -0
- package/dist/transport/authToken.test.d.ts +1 -0
- package/dist/transport/authToken.test.js +33 -0
- package/dist/verbs/types.d.ts +5 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport/classify.d.ts +8 -0
- package/dist/viewport/classify.js +20 -0
- package/dist/viewport/classify.test.d.ts +1 -0
- package/dist/viewport/classify.test.js +32 -0
- package/dist/viewport/store.browser.test.d.ts +1 -0
- package/dist/viewport/store.browser.test.js +33 -0
- package/dist/viewport/store.svelte.d.ts +9 -0
- package/dist/viewport/store.svelte.js +71 -0
- package/dist/viewport/store.svelte.test.d.ts +1 -0
- package/dist/viewport/store.svelte.test.js +54 -0
- package/dist/viewport/types.d.ts +9 -0
- package/dist/viewport/types.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DOM smoke for CarouselTabs — verifies static rendering and
|
|
3
|
+
* responsiveness to activeTab mutations. Gesture tests live in
|
|
4
|
+
* the same file (added in Task 5).
|
|
5
|
+
*
|
|
6
|
+
* happy-dom does not compute layout, so we install a deterministic
|
|
7
|
+
* ResizeObserver mock that fires once with the width parsed from the
|
|
8
|
+
* observed element's nearest ancestor that carries `data-test-width`.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { mount, unmount, flushSync } from 'svelte';
|
|
12
|
+
import CarouselTabs from './CarouselTabs.svelte';
|
|
13
|
+
import { claim, getOwner, __resetForTest as resetClaims } from '../../gestures/pointerClaim';
|
|
14
|
+
const CarouselTabsAny = CarouselTabs;
|
|
15
|
+
let mounted = null;
|
|
16
|
+
let host = null;
|
|
17
|
+
let originalRO;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
originalRO = globalThis.ResizeObserver;
|
|
20
|
+
class FakeResizeObserver {
|
|
21
|
+
constructor(cb) { this.callback = cb; }
|
|
22
|
+
observe(target) {
|
|
23
|
+
var _a;
|
|
24
|
+
let el = target;
|
|
25
|
+
let width = 0;
|
|
26
|
+
while (el) {
|
|
27
|
+
const w = (_a = el.dataset) === null || _a === void 0 ? void 0 : _a.testWidth;
|
|
28
|
+
if (w) {
|
|
29
|
+
width = parseInt(w, 10);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
el = el.parentElement;
|
|
33
|
+
}
|
|
34
|
+
this.callback([{ contentRect: { width, height: 0 }, target }], this);
|
|
35
|
+
}
|
|
36
|
+
unobserve() { }
|
|
37
|
+
disconnect() { }
|
|
38
|
+
}
|
|
39
|
+
globalThis.ResizeObserver = FakeResizeObserver;
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
if (mounted) {
|
|
43
|
+
unmount(mounted);
|
|
44
|
+
mounted = null;
|
|
45
|
+
}
|
|
46
|
+
if (host) {
|
|
47
|
+
host.remove();
|
|
48
|
+
host = null;
|
|
49
|
+
}
|
|
50
|
+
globalThis.ResizeObserver = originalRO;
|
|
51
|
+
resetClaims();
|
|
52
|
+
});
|
|
53
|
+
function makeNode(labels, activeTab = 0, wrap = false) {
|
|
54
|
+
return {
|
|
55
|
+
type: 'tabs',
|
|
56
|
+
tabs: labels.map((l, i) => ({ slotId: `s${i}`, viewId: null, label: l })),
|
|
57
|
+
activeTab,
|
|
58
|
+
wrap,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function mountCarousel(props) {
|
|
62
|
+
host = document.createElement('div');
|
|
63
|
+
host.style.width = '300px';
|
|
64
|
+
host.style.height = '400px';
|
|
65
|
+
host.style.position = 'relative';
|
|
66
|
+
host.dataset.testWidth = '300';
|
|
67
|
+
document.body.appendChild(host);
|
|
68
|
+
mounted = mount(CarouselTabsAny, { target: host, props });
|
|
69
|
+
flushSync();
|
|
70
|
+
}
|
|
71
|
+
describe('CarouselTabs (static)', () => {
|
|
72
|
+
it('renders one slide per tab', () => {
|
|
73
|
+
mountCarousel({ node: makeNode(['A', 'B', 'C']), wrap: false });
|
|
74
|
+
const slides = host.querySelectorAll('[data-sh3-slide]');
|
|
75
|
+
expect(slides.length).toBe(3);
|
|
76
|
+
});
|
|
77
|
+
it('renders pager dots when tab count > 1', () => {
|
|
78
|
+
mountCarousel({ node: makeNode(['A', 'B']), wrap: false });
|
|
79
|
+
const dots = host.querySelectorAll('[data-sh3-pager-dot]');
|
|
80
|
+
expect(dots.length).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
it('marks the active dot via aria-current="true"', () => {
|
|
83
|
+
mountCarousel({ node: makeNode(['A', 'B', 'C'], 1), wrap: false });
|
|
84
|
+
const dots = host.querySelectorAll('[data-sh3-pager-dot]');
|
|
85
|
+
expect(dots[0].getAttribute('aria-current')).toBeNull();
|
|
86
|
+
expect(dots[1].getAttribute('aria-current')).toBe('true');
|
|
87
|
+
expect(dots[2].getAttribute('aria-current')).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
it('hides pager dots for single-tab groups', () => {
|
|
90
|
+
mountCarousel({ node: makeNode(['Only']), wrap: false });
|
|
91
|
+
const dots = host.querySelectorAll('[data-sh3-pager-dot]');
|
|
92
|
+
expect(dots.length).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
it('translates the track based on activeTab', () => {
|
|
95
|
+
const node = makeNode(['A', 'B', 'C'], 1);
|
|
96
|
+
mountCarousel({ node, wrap: false });
|
|
97
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
98
|
+
expect(track).not.toBeNull();
|
|
99
|
+
expect(track.style.transform).toContain('translateX(-300px)');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
function fakePointer(type, x, y, id = 1) {
|
|
103
|
+
const ev = new Event(type, { bubbles: true, cancelable: true });
|
|
104
|
+
Object.assign(ev, { pointerId: id, clientX: x, clientY: y, pointerType: 'touch' });
|
|
105
|
+
return ev;
|
|
106
|
+
}
|
|
107
|
+
function dispatchSwipe(track, from, to, steps = 4) {
|
|
108
|
+
track.dispatchEvent(fakePointer('pointerdown', from.x, from.y));
|
|
109
|
+
for (let s = 1; s <= steps; s++) {
|
|
110
|
+
const x = from.x + ((to.x - from.x) * s) / steps;
|
|
111
|
+
const y = from.y + ((to.y - from.y) * s) / steps;
|
|
112
|
+
document.dispatchEvent(fakePointer('pointermove', x, y));
|
|
113
|
+
}
|
|
114
|
+
document.dispatchEvent(fakePointer('pointerup', to.x, to.y));
|
|
115
|
+
}
|
|
116
|
+
describe('CarouselTabs (gestures)', () => {
|
|
117
|
+
it('swipe past commit threshold advances activeTab', () => {
|
|
118
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
119
|
+
mountCarousel({ node, wrap: false });
|
|
120
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
121
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 80, y: 100 });
|
|
122
|
+
flushSync();
|
|
123
|
+
expect(node.activeTab).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
it('short swipe below commit threshold snaps back (no activeTab change)', () => {
|
|
126
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
127
|
+
mountCarousel({ node, wrap: false });
|
|
128
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
129
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 190, y: 100 });
|
|
130
|
+
flushSync();
|
|
131
|
+
expect(node.activeTab).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
it('vertical-dominant gesture does not change activeTab (axis lock releases)', () => {
|
|
134
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
135
|
+
mountCarousel({ node, wrap: false });
|
|
136
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
137
|
+
dispatchSwipe(track, { x: 150, y: 50 }, { x: 130, y: 350 });
|
|
138
|
+
flushSync();
|
|
139
|
+
expect(node.activeTab).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
it('at last slide with wrap=false: large drag does NOT advance (edge-resist)', () => {
|
|
142
|
+
const node = makeNode(['A', 'B'], 1, /* wrap */ false);
|
|
143
|
+
mountCarousel({ node, wrap: false });
|
|
144
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
145
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 50, y: 100 });
|
|
146
|
+
flushSync();
|
|
147
|
+
expect(node.activeTab).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
it('at last slide with wrap=true: large drag wraps to first', () => {
|
|
150
|
+
const node = makeNode(['A', 'B', 'C'], 2, /* wrap */ true);
|
|
151
|
+
mountCarousel({ node, wrap: true });
|
|
152
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
153
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 50, y: 100 });
|
|
154
|
+
flushSync();
|
|
155
|
+
expect(node.activeTab).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
it('at first slide with wrap=true: large drag right-to-left-back wraps to last', () => {
|
|
158
|
+
const node = makeNode(['A', 'B', 'C'], 0, /* wrap */ true);
|
|
159
|
+
mountCarousel({ node, wrap: true });
|
|
160
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
161
|
+
dispatchSwipe(track, { x: 50, y: 100 }, { x: 250, y: 100 });
|
|
162
|
+
flushSync();
|
|
163
|
+
expect(node.activeTab).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
it('pointercancel mid-drag does NOT commit, even past the threshold', () => {
|
|
166
|
+
// Repro for the Android bug where descendant elements (buttons,
|
|
167
|
+
// scroll regions) fire pointercancel after we've claimed the
|
|
168
|
+
// gesture — the previous implementation read that as a release at
|
|
169
|
+
// the current dx and instantly auto-completed the swipe with the
|
|
170
|
+
// finger still down.
|
|
171
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
172
|
+
mountCarousel({ node, wrap: false });
|
|
173
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
174
|
+
track.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
175
|
+
// Drag past the commit threshold (containerWidth=300, 40% = 120px).
|
|
176
|
+
document.dispatchEvent(fakePointer('pointermove', 200, 100));
|
|
177
|
+
document.dispatchEvent(fakePointer('pointermove', 100, 100));
|
|
178
|
+
// Cancel instead of release — must NOT commit.
|
|
179
|
+
document.dispatchEvent(fakePointer('pointercancel', 100, 100));
|
|
180
|
+
flushSync();
|
|
181
|
+
expect(node.activeTab).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
it('single-tab carousel: gesture is a no-op', () => {
|
|
184
|
+
const node = makeNode(['Only'], 0);
|
|
185
|
+
mountCarousel({ node, wrap: false });
|
|
186
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
187
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 50, y: 100 });
|
|
188
|
+
flushSync();
|
|
189
|
+
expect(node.activeTab).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
it('pointerdown on an editable target (input) does not initiate a swipe', () => {
|
|
192
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
193
|
+
mountCarousel({ node, wrap: false });
|
|
194
|
+
const slide = host.querySelector('[data-sh3-slide]');
|
|
195
|
+
const input = document.createElement('input');
|
|
196
|
+
slide.appendChild(input);
|
|
197
|
+
// pointerdown on the input bubbles to the track, but the handler bails
|
|
198
|
+
// because the target is editable.
|
|
199
|
+
input.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
200
|
+
document.dispatchEvent(fakePointer('pointermove', 80, 100));
|
|
201
|
+
document.dispatchEvent(fakePointer('pointerup', 80, 100));
|
|
202
|
+
flushSync();
|
|
203
|
+
expect(node.activeTab).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
it('pointerdown inside an overflow-x:scroll element does not initiate a swipe', () => {
|
|
206
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
207
|
+
mountCarousel({ node, wrap: false });
|
|
208
|
+
const slide = host.querySelector('[data-sh3-slide]');
|
|
209
|
+
const scroller = document.createElement('div');
|
|
210
|
+
scroller.style.overflowX = 'scroll';
|
|
211
|
+
slide.appendChild(scroller);
|
|
212
|
+
scroller.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
213
|
+
document.dispatchEvent(fakePointer('pointermove', 80, 100));
|
|
214
|
+
document.dispatchEvent(fakePointer('pointerup', 80, 100));
|
|
215
|
+
flushSync();
|
|
216
|
+
expect(node.activeTab).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
it('pointerdown inside an overflow-x:auto element does not initiate a swipe', () => {
|
|
219
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
220
|
+
mountCarousel({ node, wrap: false });
|
|
221
|
+
const slide = host.querySelector('[data-sh3-slide]');
|
|
222
|
+
const scroller = document.createElement('div');
|
|
223
|
+
scroller.style.overflowX = 'auto';
|
|
224
|
+
slide.appendChild(scroller);
|
|
225
|
+
scroller.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
226
|
+
document.dispatchEvent(fakePointer('pointermove', 80, 100));
|
|
227
|
+
document.dispatchEvent(fakePointer('pointerup', 80, 100));
|
|
228
|
+
flushSync();
|
|
229
|
+
expect(node.activeTab).toBe(0);
|
|
230
|
+
});
|
|
231
|
+
it('pointerdown inside an overflow-x:hidden element still allows swipe', () => {
|
|
232
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
233
|
+
mountCarousel({ node, wrap: false });
|
|
234
|
+
const slide = host.querySelector('[data-sh3-slide]');
|
|
235
|
+
const wrapper = document.createElement('div');
|
|
236
|
+
wrapper.style.overflowX = 'hidden';
|
|
237
|
+
slide.appendChild(wrapper);
|
|
238
|
+
wrapper.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
239
|
+
document.dispatchEvent(fakePointer('pointermove', 80, 100));
|
|
240
|
+
document.dispatchEvent(fakePointer('pointerup', 80, 100));
|
|
241
|
+
flushSync();
|
|
242
|
+
expect(node.activeTab).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
it('pointerdown on contenteditable target does not initiate a swipe', () => {
|
|
245
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
246
|
+
mountCarousel({ node, wrap: false });
|
|
247
|
+
const slide = host.querySelector('[data-sh3-slide]');
|
|
248
|
+
const editable = document.createElement('div');
|
|
249
|
+
editable.setAttribute('contenteditable', 'true');
|
|
250
|
+
slide.appendChild(editable);
|
|
251
|
+
editable.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
252
|
+
document.dispatchEvent(fakePointer('pointermove', 80, 100));
|
|
253
|
+
document.dispatchEvent(fakePointer('pointerup', 80, 100));
|
|
254
|
+
flushSync();
|
|
255
|
+
expect(node.activeTab).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
it('marks the track --dragging while a horizontal drag is in flight, and clears it on pointerup', () => {
|
|
258
|
+
const node = makeNode(['A', 'B', 'C'], 0);
|
|
259
|
+
mountCarousel({ node, wrap: false });
|
|
260
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
261
|
+
track.dispatchEvent(fakePointer('pointerdown', 250, 100));
|
|
262
|
+
document.dispatchEvent(fakePointer('pointermove', 200, 100));
|
|
263
|
+
flushSync();
|
|
264
|
+
expect(track.classList.contains('sh3-carousel-track--dragging')).toBe(true);
|
|
265
|
+
document.dispatchEvent(fakePointer('pointerup', 200, 100));
|
|
266
|
+
flushSync();
|
|
267
|
+
expect(track.classList.contains('sh3-carousel-track--dragging')).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('CarouselTabs — PointerClaim integration', () => {
|
|
271
|
+
it('does not start gesture when pointer is already claimed by an app', () => {
|
|
272
|
+
const node = makeNode(['A', 'B'], 0);
|
|
273
|
+
mountCarousel({ node, wrap: false });
|
|
274
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
275
|
+
// Pre-claim as if an app pan gesture owns this pointer
|
|
276
|
+
claim(1, { ownerId: 'app:pan', axis: 'xy', priority: 'normal', depth: 99 });
|
|
277
|
+
dispatchSwipe(track, { x: 250, y: 100 }, { x: 50, y: 100 });
|
|
278
|
+
flushSync();
|
|
279
|
+
// Gesture must have been skipped — activeTab unchanged
|
|
280
|
+
expect(node.activeTab).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
it('claims the pointer as sh3:carousel when unclaimed', () => {
|
|
283
|
+
var _a;
|
|
284
|
+
const node = makeNode(['A', 'B'], 0);
|
|
285
|
+
mountCarousel({ node, wrap: false });
|
|
286
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
287
|
+
track.dispatchEvent(fakePointer('pointerdown', 250, 100, 2));
|
|
288
|
+
expect((_a = getOwner(2)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('sh3:carousel');
|
|
289
|
+
});
|
|
290
|
+
it('releases claim on pointerup', () => {
|
|
291
|
+
var _a;
|
|
292
|
+
const node = makeNode(['A', 'B'], 0);
|
|
293
|
+
mountCarousel({ node, wrap: false });
|
|
294
|
+
const track = host.querySelector('[data-sh3-carousel-track]');
|
|
295
|
+
track.dispatchEvent(fakePointer('pointerdown', 250, 100, 3));
|
|
296
|
+
expect((_a = getOwner(3)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('sh3:carousel');
|
|
297
|
+
document.dispatchEvent(fakePointer('pointerup', 250, 100, 3));
|
|
298
|
+
expect(getOwner(3)).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* Compact rendering wrapper. Reads the active layout tree from
|
|
4
|
+
* layoutStore, runs derive() to get the CompactRendering, renders the
|
|
5
|
+
* bodyRoot via LayoutRenderer (with rootOverride), and mounts a
|
|
6
|
+
* DrawerSurface for each non-null anchor.
|
|
7
|
+
*
|
|
8
|
+
* Drawer state lives in drawerStore (Sh3.drawers backing). The surfaces
|
|
9
|
+
* paint above the body content via their own absolute-positioned
|
|
10
|
+
* frames; the drawer overlay layer (--sh3-z-layer-drawers) sits above
|
|
11
|
+
* the docked content so the surfaces stack correctly.
|
|
12
|
+
*
|
|
13
|
+
* View-default role lookup is intentionally omitted in v1 — derive()
|
|
14
|
+
* reads slot.role / tab.role directly. Apps that want non-body slots
|
|
15
|
+
* tag them at authoring time. View-default fall-through ships when the
|
|
16
|
+
* registry exposes a pre-mount lookup (deferred from this PR).
|
|
17
|
+
*/
|
|
18
|
+
import { layoutStore } from '../store.svelte';
|
|
19
|
+
import { drawerStore } from './drawerStore.svelte';
|
|
20
|
+
import { derive } from './derive';
|
|
21
|
+
import LayoutRenderer from '../LayoutRenderer.svelte';
|
|
22
|
+
import DrawerSurface from '../../overlays/DrawerSurface.svelte';
|
|
23
|
+
import type { DrawerAnchor } from './types';
|
|
24
|
+
|
|
25
|
+
const rendering = $derived(derive(layoutStore.root));
|
|
26
|
+
|
|
27
|
+
const anchors: DrawerAnchor[] = ['left', 'right', 'top'];
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="compact-body" data-sh3-region="compact-body">
|
|
31
|
+
<LayoutRenderer rootOverride={rendering.bodyRoot} carousels={rendering.carousels} />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{#each anchors as anchor (anchor)}
|
|
35
|
+
{@const spec = rendering.drawers[anchor]}
|
|
36
|
+
{#if spec}
|
|
37
|
+
<DrawerSurface
|
|
38
|
+
{anchor}
|
|
39
|
+
{spec}
|
|
40
|
+
open={drawerStore.state[anchor].open}
|
|
41
|
+
activeSlotId={drawerStore.state[anchor].activeSlotId}
|
|
42
|
+
onClose={() => drawerStore.close(anchor)}
|
|
43
|
+
onActivate={(slotId) => drawerStore.activate(anchor, slotId)}
|
|
44
|
+
/>
|
|
45
|
+
{/if}
|
|
46
|
+
{/each}
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.compact-body {
|
|
50
|
+
position: absolute;
|
|
51
|
+
inset: 0;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DOM smoke for CompactRenderer. With a sidebar+body+inspector tree
|
|
3
|
+
* attached as the active layout, the wrapper renders one drawer surface
|
|
4
|
+
* per anchor (open=false, but the drawer-region lookup happens on open
|
|
5
|
+
* — here we just verify the wrapper accepts the tree and renders the
|
|
6
|
+
* body container plus the LayoutRenderer for bodyRoot).
|
|
7
|
+
*
|
|
8
|
+
* Slot-survival on viewport flip is covered by the browser e2e in T21.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { mount, unmount, flushSync } from 'svelte';
|
|
12
|
+
import CompactRenderer from './CompactRenderer.svelte';
|
|
13
|
+
import { drawerStore } from './drawerStore.svelte';
|
|
14
|
+
import { __resetLayoutStoreForTest, attachApp, detachApp, switchToApp } from '../store.svelte';
|
|
15
|
+
function fakeApp() {
|
|
16
|
+
return {
|
|
17
|
+
manifest: { id: 'compact-test-app', layoutVersion: 5 },
|
|
18
|
+
initialLayout: {
|
|
19
|
+
type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
|
|
20
|
+
children: [
|
|
21
|
+
{ type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
|
|
22
|
+
{ type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
|
|
23
|
+
{ type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
let mounted = null;
|
|
29
|
+
let host = null;
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (mounted) {
|
|
32
|
+
unmount(mounted);
|
|
33
|
+
mounted = null;
|
|
34
|
+
}
|
|
35
|
+
if (host) {
|
|
36
|
+
host.remove();
|
|
37
|
+
host = null;
|
|
38
|
+
}
|
|
39
|
+
detachApp();
|
|
40
|
+
});
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
__resetLayoutStoreForTest();
|
|
43
|
+
drawerStore.__reset();
|
|
44
|
+
});
|
|
45
|
+
describe('CompactRenderer (dom)', () => {
|
|
46
|
+
it('renders the compact-body region for the bodyRoot', () => {
|
|
47
|
+
attachApp(fakeApp());
|
|
48
|
+
switchToApp();
|
|
49
|
+
flushSync();
|
|
50
|
+
host = document.createElement('div');
|
|
51
|
+
host.style.width = '400px';
|
|
52
|
+
host.style.height = '600px';
|
|
53
|
+
host.style.position = 'relative';
|
|
54
|
+
document.body.appendChild(host);
|
|
55
|
+
mounted = mount(CompactRenderer, { target: host });
|
|
56
|
+
flushSync();
|
|
57
|
+
expect(host.querySelector('[data-sh3-region="compact-body"]')).not.toBeNull();
|
|
58
|
+
});
|
|
59
|
+
it('opening left drawer renders one drawer-region with sidebar label', () => {
|
|
60
|
+
attachApp(fakeApp());
|
|
61
|
+
switchToApp();
|
|
62
|
+
flushSync();
|
|
63
|
+
host = document.createElement('div');
|
|
64
|
+
host.style.width = '400px';
|
|
65
|
+
host.style.height = '600px';
|
|
66
|
+
host.style.position = 'relative';
|
|
67
|
+
document.body.appendChild(host);
|
|
68
|
+
mounted = mount(CompactRenderer, { target: host });
|
|
69
|
+
flushSync();
|
|
70
|
+
drawerStore.open('left');
|
|
71
|
+
flushSync();
|
|
72
|
+
const drawer = host.querySelector('[data-sh3-region="drawer"][data-sh3-anchor="left"]');
|
|
73
|
+
expect(drawer).not.toBeNull();
|
|
74
|
+
expect(drawer.querySelector('header').textContent).toContain('sb');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('CompactRenderer — carousels', () => {
|
|
78
|
+
it('renders a CarouselTabs region for a full-width tabs body', () => {
|
|
79
|
+
const app = {
|
|
80
|
+
manifest: { id: 'carousel-app', label: 'Carousel App', layoutVersion: 6 },
|
|
81
|
+
initialLayout: {
|
|
82
|
+
type: 'tabs',
|
|
83
|
+
activeTab: 0,
|
|
84
|
+
tabs: [
|
|
85
|
+
{ slotId: 's0', viewId: null, label: 'A', role: 'body' },
|
|
86
|
+
{ slotId: 's1', viewId: null, label: 'B', role: 'body' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
attachApp(app);
|
|
91
|
+
switchToApp();
|
|
92
|
+
flushSync();
|
|
93
|
+
host = document.createElement('div');
|
|
94
|
+
document.body.appendChild(host);
|
|
95
|
+
mounted = mount(CompactRenderer, { target: host });
|
|
96
|
+
flushSync();
|
|
97
|
+
expect(host.querySelector('[data-sh3-region="carousel"]')).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
it('does NOT render a carousel for tabs that are narrower than full body width', () => {
|
|
100
|
+
const app = {
|
|
101
|
+
manifest: { id: 'narrow-app', label: 'Narrow App', layoutVersion: 6 },
|
|
102
|
+
initialLayout: {
|
|
103
|
+
type: 'split',
|
|
104
|
+
direction: 'horizontal',
|
|
105
|
+
sizes: [0.5, 0.5],
|
|
106
|
+
children: [
|
|
107
|
+
{
|
|
108
|
+
type: 'tabs',
|
|
109
|
+
activeTab: 0,
|
|
110
|
+
tabs: [{ slotId: 'l0', viewId: null, label: 'L', role: 'body' }],
|
|
111
|
+
},
|
|
112
|
+
{ type: 'slot', slotId: 'r', viewId: null, role: 'body' },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
attachApp(app);
|
|
117
|
+
switchToApp();
|
|
118
|
+
flushSync();
|
|
119
|
+
host = document.createElement('div');
|
|
120
|
+
document.body.appendChild(host);
|
|
121
|
+
mounted = mount(CompactRenderer, { target: host });
|
|
122
|
+
flushSync();
|
|
123
|
+
expect(host.querySelector('[data-sh3-region="carousel"]')).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* derive — pure transform from a canonical LayoutNode tree to a
|
|
3
|
+
* CompactRendering. See ./types.ts for the output shape.
|
|
4
|
+
*
|
|
5
|
+
* Anchor inference (from spec §4):
|
|
6
|
+
* - Horizontal split: first non-body subtree → 'left', last → 'right'.
|
|
7
|
+
* - Vertical split: first non-body subtree → 'top'.
|
|
8
|
+
* - Otherwise: sidebar → 'left', inspector → 'right'.
|
|
9
|
+
*
|
|
10
|
+
* Once an outer split has assigned an anchor for a subtree, descent
|
|
11
|
+
* locks that anchor — inner splits don't retag (a vertical split
|
|
12
|
+
* inside a left-anchored subtree keeps both children on the left).
|
|
13
|
+
*
|
|
14
|
+
* Note: this transform reads slot.role / tab.role only. View-level
|
|
15
|
+
* defaultRole resolution happens at the call site via resolveRole(),
|
|
16
|
+
* which materializes a tree with effective roles before passing to
|
|
17
|
+
* derive(). See layout/compact/CompactRenderer.svelte.
|
|
18
|
+
*/
|
|
19
|
+
import { EMPTY_BODY } from './types';
|
|
20
|
+
import { enrichCarousels } from './enrichCarousels';
|
|
21
|
+
function effectiveRole(role) {
|
|
22
|
+
return role !== null && role !== void 0 ? role : 'body';
|
|
23
|
+
}
|
|
24
|
+
function collectSlots(node) {
|
|
25
|
+
if (node.type === 'slot') {
|
|
26
|
+
return [{
|
|
27
|
+
slotId: node.slotId,
|
|
28
|
+
viewId: node.viewId,
|
|
29
|
+
label: node.slotId,
|
|
30
|
+
role: effectiveRole(node.role),
|
|
31
|
+
}];
|
|
32
|
+
}
|
|
33
|
+
if (node.type === 'tabs') {
|
|
34
|
+
return node.tabs.map((t) => ({
|
|
35
|
+
slotId: t.slotId,
|
|
36
|
+
viewId: t.viewId,
|
|
37
|
+
label: t.label,
|
|
38
|
+
icon: t.icon,
|
|
39
|
+
role: effectiveRole(t.role),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
return node.children.flatMap(collectSlots);
|
|
43
|
+
}
|
|
44
|
+
function hasAnyBody(node) {
|
|
45
|
+
return collectSlots(node).some((s) => s.role === 'body');
|
|
46
|
+
}
|
|
47
|
+
function stripNonBody(node) {
|
|
48
|
+
if (node.type === 'slot') {
|
|
49
|
+
return effectiveRole(node.role) === 'body' ? node : null;
|
|
50
|
+
}
|
|
51
|
+
if (node.type === 'tabs') {
|
|
52
|
+
const bodyTabs = node.tabs.filter((t) => effectiveRole(t.role) === 'body');
|
|
53
|
+
if (bodyTabs.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
if (bodyTabs.length === node.tabs.length)
|
|
56
|
+
return node;
|
|
57
|
+
return {
|
|
58
|
+
type: 'tabs',
|
|
59
|
+
activeTab: Math.min(node.activeTab, bodyTabs.length - 1),
|
|
60
|
+
tabs: bodyTabs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// split
|
|
64
|
+
const survivors = [];
|
|
65
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
66
|
+
const stripped = stripNonBody(node.children[i]);
|
|
67
|
+
if (stripped)
|
|
68
|
+
survivors.push({ child: stripped, size: node.sizes[i] });
|
|
69
|
+
}
|
|
70
|
+
if (survivors.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
if (survivors.length === 1)
|
|
73
|
+
return survivors[0].child;
|
|
74
|
+
return {
|
|
75
|
+
type: 'split',
|
|
76
|
+
direction: node.direction,
|
|
77
|
+
sizes: survivors.map((s) => s.size),
|
|
78
|
+
children: survivors.map((s) => s.child),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function defaultAnchor(role) {
|
|
82
|
+
return role === 'inspector' ? 'right' : 'left';
|
|
83
|
+
}
|
|
84
|
+
function partitionDrawers(node) {
|
|
85
|
+
const buckets = { left: [], right: [], top: [] };
|
|
86
|
+
function walk(n, hint) {
|
|
87
|
+
if (n.type === 'slot') {
|
|
88
|
+
const role = effectiveRole(n.role);
|
|
89
|
+
if (role === 'body')
|
|
90
|
+
return;
|
|
91
|
+
const anchor = hint !== null && hint !== void 0 ? hint : defaultAnchor(role);
|
|
92
|
+
buckets[anchor].push({
|
|
93
|
+
slotId: n.slotId, viewId: n.viewId, label: n.slotId, role,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (n.type === 'tabs') {
|
|
98
|
+
for (const t of n.tabs) {
|
|
99
|
+
const role = effectiveRole(t.role);
|
|
100
|
+
if (role === 'body')
|
|
101
|
+
continue;
|
|
102
|
+
const anchor = hint !== null && hint !== void 0 ? hint : defaultAnchor(role);
|
|
103
|
+
buckets[anchor].push({
|
|
104
|
+
slotId: t.slotId, viewId: t.viewId, label: t.label, icon: t.icon, role,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (n.direction === 'horizontal') {
|
|
110
|
+
for (let i = 0; i < n.children.length; i++) {
|
|
111
|
+
const child = n.children[i];
|
|
112
|
+
let nextHint = hint;
|
|
113
|
+
if (hint === null && !hasAnyBody(child)) {
|
|
114
|
+
if (i === 0)
|
|
115
|
+
nextHint = 'left';
|
|
116
|
+
else if (i === n.children.length - 1)
|
|
117
|
+
nextHint = 'right';
|
|
118
|
+
}
|
|
119
|
+
walk(child, nextHint);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
for (let i = 0; i < n.children.length; i++) {
|
|
124
|
+
const child = n.children[i];
|
|
125
|
+
let nextHint = hint;
|
|
126
|
+
if (hint === null && i === 0 && !hasAnyBody(child))
|
|
127
|
+
nextHint = 'top';
|
|
128
|
+
walk(child, nextHint);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
walk(node, null);
|
|
133
|
+
return buckets;
|
|
134
|
+
}
|
|
135
|
+
function toSpec(slots) {
|
|
136
|
+
if (slots.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
slots: slots.map((s) => ({
|
|
140
|
+
slotId: s.slotId, viewId: s.viewId, label: s.label, icon: s.icon, role: s.role,
|
|
141
|
+
})),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function derive(tree) {
|
|
145
|
+
const stripped = stripNonBody(tree);
|
|
146
|
+
const bodyRoot = stripped !== null && stripped !== void 0 ? stripped : EMPTY_BODY;
|
|
147
|
+
const buckets = partitionDrawers(tree);
|
|
148
|
+
return {
|
|
149
|
+
bodyRoot,
|
|
150
|
+
drawers: {
|
|
151
|
+
left: toSpec(buckets.left),
|
|
152
|
+
right: toSpec(buckets.right),
|
|
153
|
+
top: toSpec(buckets.top),
|
|
154
|
+
},
|
|
155
|
+
carousels: enrichCarousels(bodyRoot),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|