sh3-core 0.17.2 → 0.19.1
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 +59 -4
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listeners.js +12 -1
- package/dist/api.d.ts +4 -0
- 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 +54 -20
- package/dist/chrome/CompactChrome.svelte.test.js +112 -5
- 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 +120 -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/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 +16 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +2 -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-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 +362 -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 +1 -1
- package/dist/layout/compact/CompactRenderer.svelte.test.js +49 -0
- package/dist/layout/compact/derive.js +2 -0
- package/dist/layout/compact/derive.test.js +37 -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/types.d.ts +3 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/store.schemaVersion.test.js +2 -2
- package/dist/layout/types.d.ts +9 -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/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- 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 +9 -0
- 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/shards/activate.svelte.js +16 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +136 -0
- package/dist/shards/types.d.ts +29 -0
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,18 @@ import { launchApp } from '../apps/lifecycle';
|
|
|
8
8
|
import { registerView } from '../shards/registry';
|
|
9
9
|
import { makeApp, makeAppManifest, makeSplitNode, makeTabsNode, makeTabEntry, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
10
10
|
import { layoutStore } from './store.svelte';
|
|
11
|
+
import { viewportStore } from '../viewport/store.svelte';
|
|
12
|
+
// Chromium under vitest-browser runs in a 414×n viewport, which the
|
|
13
|
+
// auto-classifier reads as compact. These tests exercise desktop-only
|
|
14
|
+
// splitter behavior (drag, double-click collapse, collapse widget
|
|
15
|
+
// presence) — pin desktop in setup so the new ResizableSplitter compact
|
|
16
|
+
// gate doesn't suppress what they're asserting.
|
|
17
|
+
function setupDesktop() {
|
|
18
|
+
cleanupDOM();
|
|
19
|
+
resetFramework();
|
|
20
|
+
viewportStore.__reset();
|
|
21
|
+
viewportStore.override('desktop');
|
|
22
|
+
}
|
|
11
23
|
// ---------------------------------------------------------------------------
|
|
12
24
|
// Utilities
|
|
13
25
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +175,7 @@ describe('LayoutRenderer browser — E.2 drag tab to quadrant', () => {
|
|
|
163
175
|
// E.3 — splitter drag updates sizes
|
|
164
176
|
// ---------------------------------------------------------------------------
|
|
165
177
|
describe('LayoutRenderer browser — E.3 splitter drag', () => {
|
|
166
|
-
beforeEach(
|
|
178
|
+
beforeEach(setupDesktop);
|
|
167
179
|
it('updates split.sizes when the splitter handle is dragged', async () => {
|
|
168
180
|
stubView();
|
|
169
181
|
registerApp(makeApp({
|
|
@@ -248,7 +260,7 @@ describe('LayoutRenderer browser — E.4 close policy', () => {
|
|
|
248
260
|
// E.5 — double-click splitter toggles collapse
|
|
249
261
|
// ---------------------------------------------------------------------------
|
|
250
262
|
describe('LayoutRenderer browser — E.5 splitter collapse toggle', () => {
|
|
251
|
-
beforeEach(
|
|
263
|
+
beforeEach(setupDesktop);
|
|
252
264
|
it('toggles collapsed[i] on double-click', async () => {
|
|
253
265
|
var _a;
|
|
254
266
|
stubView();
|
|
@@ -276,7 +288,7 @@ describe('LayoutRenderer browser — E.5 splitter collapse toggle', () => {
|
|
|
276
288
|
// E.6 — fixed[] slots: no collapse widget, frozen handles
|
|
277
289
|
// ---------------------------------------------------------------------------
|
|
278
290
|
describe('LayoutRenderer browser — E.6 fixed slots', () => {
|
|
279
|
-
beforeEach(
|
|
291
|
+
beforeEach(setupDesktop);
|
|
280
292
|
it('hides the collapse widget on a fixed pane but keeps it on panes with a non-fixed neighbor', async () => {
|
|
281
293
|
stubView();
|
|
282
294
|
registerApp(makeApp({
|
|
@@ -28,10 +28,13 @@
|
|
|
28
28
|
|
|
29
29
|
import type { TabsNode, LayoutNode, TreeRootRef } from './types';
|
|
30
30
|
import ResizableSplitter from '../primitives/ResizableSplitter.svelte';
|
|
31
|
+
import { viewportStore } from '../viewport/store.svelte';
|
|
31
32
|
import TabbedPanel, { type TabDragController } from '../primitives/TabbedPanel.svelte';
|
|
32
33
|
import SlotContainer from './SlotContainer.svelte';
|
|
33
34
|
import SlotDropZone from './SlotDropZone.svelte';
|
|
34
35
|
import Self from './LayoutRenderer.svelte';
|
|
36
|
+
import CarouselTabs from './compact/CarouselTabs.svelte';
|
|
37
|
+
import { pathKey, type NodePath, type CarouselInfo } from './compact/enrichCarousels';
|
|
35
38
|
import { layoutStore } from './store.svelte';
|
|
36
39
|
import { nodeAtPath } from './ops';
|
|
37
40
|
import { isSlotClosable, isSlotDirty } from './slotHostPool.svelte';
|
|
@@ -48,7 +51,13 @@
|
|
|
48
51
|
path = [],
|
|
49
52
|
rootRef = { kind: 'docked' } as TreeRootRef,
|
|
50
53
|
rootOverride,
|
|
51
|
-
|
|
54
|
+
carousels,
|
|
55
|
+
}: {
|
|
56
|
+
path?: number[];
|
|
57
|
+
rootRef?: TreeRootRef;
|
|
58
|
+
rootOverride?: LayoutNode;
|
|
59
|
+
carousels?: Map<NodePath, CarouselInfo>;
|
|
60
|
+
} = $props();
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
63
|
* Resolve the current node by walking `layoutStore.root` along the
|
|
@@ -183,6 +192,7 @@
|
|
|
183
192
|
fixed={split.fixed}
|
|
184
193
|
count={split.children.length}
|
|
185
194
|
pane={splitPane}
|
|
195
|
+
compact={viewportStore.current.class === 'compact'}
|
|
186
196
|
onResize={(i, v) => (split.sizes[i] = v)}
|
|
187
197
|
onCollapseToggle={(i, v) => {
|
|
188
198
|
if (!split.collapsed) split.collapsed = split.children.map(() => false);
|
|
@@ -190,11 +200,14 @@
|
|
|
190
200
|
}}
|
|
191
201
|
/>
|
|
192
202
|
{#snippet splitPane(i: number)}
|
|
193
|
-
<Self {rootRef} path={[...path, i]} />
|
|
203
|
+
<Self {rootRef} path={[...path, i]} {rootOverride} {carousels} />
|
|
194
204
|
{/snippet}
|
|
195
205
|
{:else if node.type === 'tabs'}
|
|
196
206
|
{@const tabs = asTabs(node)}
|
|
197
|
-
{
|
|
207
|
+
{@const carouselInfo = carousels?.get(pathKey(path))}
|
|
208
|
+
{#if carouselInfo && tabs && tabs.tabs.length > 0}
|
|
209
|
+
<CarouselTabs node={tabs} wrap={carouselInfo.wrap} {rootRef} {path} />
|
|
210
|
+
{:else if tabs && tabs.tabs.length > 0}
|
|
198
211
|
{@const controller = makeController(tabs)}
|
|
199
212
|
<TabbedPanel
|
|
200
213
|
labels={tabs.tabs.map((t) => t.label)}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { LayoutNode, TreeRootRef } from './types';
|
|
2
|
+
import { type NodePath, type CarouselInfo } from './compact/enrichCarousels';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
path?: number[];
|
|
4
5
|
rootRef?: TreeRootRef;
|
|
5
6
|
rootOverride?: LayoutNode;
|
|
7
|
+
carousels?: Map<NodePath, CarouselInfo>;
|
|
6
8
|
};
|
|
7
9
|
declare const LayoutRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
10
|
type LayoutRenderer = ReturnType<typeof LayoutRenderer>;
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* CarouselTabs — compact-mode rendering for a TabsNode that the
|
|
4
|
+
* framework has classified as full-width-in-bodyRoot.
|
|
5
|
+
*
|
|
6
|
+
* Renders a horizontal track containing one slide per tab (each slide
|
|
7
|
+
* is the carousel's full width). All slides stay mounted to match
|
|
8
|
+
* existing TabsNode background-mount semantics. activeTab is read
|
|
9
|
+
* from the node and committed via direct mutation (the same pattern
|
|
10
|
+
* the standard tab strip uses today — see LayoutRenderer.svelte).
|
|
11
|
+
*
|
|
12
|
+
* Pager dots render at the bottom for multi-tab carousels. Single-tab
|
|
13
|
+
* carousels render the slide only (no dots, no gesture meaning).
|
|
14
|
+
*
|
|
15
|
+
* Gestures (Task 5) attach to the track element and use PointerEvents
|
|
16
|
+
* with axis lock + commit threshold + edge-resist or wrap.
|
|
17
|
+
*/
|
|
18
|
+
import type { TabsNode, TabEntry, TreeRootRef } from '../types';
|
|
19
|
+
import SlotContainer from '../SlotContainer.svelte';
|
|
20
|
+
import SlotDropZone from '../SlotDropZone.svelte';
|
|
21
|
+
import { claim, revoke, isOwner } from '../../gestures/pointerClaim';
|
|
22
|
+
import { ancestorCount } from '../../gestures';
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
node,
|
|
26
|
+
wrap,
|
|
27
|
+
rootRef = { kind: 'docked' } as TreeRootRef,
|
|
28
|
+
path = [] as number[],
|
|
29
|
+
}: {
|
|
30
|
+
node: TabsNode;
|
|
31
|
+
wrap: boolean;
|
|
32
|
+
rootRef?: TreeRootRef;
|
|
33
|
+
path?: number[];
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
let containerEl = $state<HTMLElement | null>(null);
|
|
37
|
+
let containerWidth = $state(0);
|
|
38
|
+
|
|
39
|
+
$effect(() => {
|
|
40
|
+
if (!containerEl) return;
|
|
41
|
+
// Seed width immediately — happy-dom and similar non-layout DOMs
|
|
42
|
+
// may never deliver a ResizeObserver entry.
|
|
43
|
+
const initialRect = containerEl.getBoundingClientRect();
|
|
44
|
+
if (initialRect.width > 0) {
|
|
45
|
+
containerWidth = initialRect.width;
|
|
46
|
+
} else if (containerEl.parentElement) {
|
|
47
|
+
const parentRect = containerEl.parentElement.getBoundingClientRect();
|
|
48
|
+
if (parentRect.width > 0) containerWidth = parentRect.width;
|
|
49
|
+
}
|
|
50
|
+
const ro = new ResizeObserver((entries) => {
|
|
51
|
+
for (const e of entries) containerWidth = e.contentRect.width;
|
|
52
|
+
});
|
|
53
|
+
ro.observe(containerEl);
|
|
54
|
+
return () => ro.disconnect();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const tabCount = $derived(node.tabs.length);
|
|
58
|
+
const showDots = $derived(tabCount > 1);
|
|
59
|
+
|
|
60
|
+
// ---- Gesture state ----
|
|
61
|
+
let dragDelta = $state(0);
|
|
62
|
+
let dragging = $state(false);
|
|
63
|
+
let claimed = $state(false);
|
|
64
|
+
let activePointerId: number | null = null;
|
|
65
|
+
|
|
66
|
+
type PointerSnapshot = { id: number; x: number; y: number; t: number };
|
|
67
|
+
let downSnap: PointerSnapshot | null = null;
|
|
68
|
+
let lastSnap: PointerSnapshot | null = null;
|
|
69
|
+
|
|
70
|
+
const AXIS_LOCK_DOMINANCE = 1.2;
|
|
71
|
+
const AXIS_LOCK_MIN_DX = 6;
|
|
72
|
+
const COMMIT_FRACTION = 0.4;
|
|
73
|
+
const COMMIT_VELOCITY = 500;
|
|
74
|
+
const RUBBER_BAND_MAX_FRACTION = 0.3;
|
|
75
|
+
/**
|
|
76
|
+
* If the pointer takes longer than this to cross the axis-lock threshold,
|
|
77
|
+
* treat the gesture as text-selection intent (slow drag over text) and
|
|
78
|
+
* release control back to the platform. A swipe-intent gesture always
|
|
79
|
+
* crosses 6px well within 250ms.
|
|
80
|
+
*/
|
|
81
|
+
const SLOW_DRAG_TIMEOUT_MS = 250;
|
|
82
|
+
|
|
83
|
+
function hasNativeHorizontalScroll(target: EventTarget | null): boolean {
|
|
84
|
+
let el = target as HTMLElement | null;
|
|
85
|
+
while (el && el !== containerEl) {
|
|
86
|
+
const ox = getComputedStyle(el).overflowX;
|
|
87
|
+
if (ox === 'auto' || ox === 'scroll') return true;
|
|
88
|
+
el = el.parentElement;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
94
|
+
const el = target as HTMLElement | null;
|
|
95
|
+
if (!el || typeof el.tagName !== 'string') return false;
|
|
96
|
+
const tag = el.tagName;
|
|
97
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
98
|
+
// `isContentEditable` reflects the rendered state; closest('[contenteditable]')
|
|
99
|
+
// catches the authored attribute even before the browser computes it.
|
|
100
|
+
if (el.isContentEditable === true) return true;
|
|
101
|
+
const ce = el.closest?.('[contenteditable=""], [contenteditable="true"]');
|
|
102
|
+
return ce !== null && ce !== undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function rubberBand(d: number, width: number): number {
|
|
106
|
+
if (width === 0) return 0;
|
|
107
|
+
const max = width * RUBBER_BAND_MAX_FRACTION;
|
|
108
|
+
return Math.sign(d) * max * (1 - Math.exp(-Math.abs(d) / max));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clampedDragDelta(rawDx: number): number {
|
|
112
|
+
if (!wrap) {
|
|
113
|
+
const atFirst = node.activeTab === 0 && rawDx > 0;
|
|
114
|
+
const atLast = node.activeTab === tabCount - 1 && rawDx < 0;
|
|
115
|
+
if (atFirst || atLast) return rubberBand(rawDx, containerWidth);
|
|
116
|
+
}
|
|
117
|
+
return rawDx;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onPointerDown(ev: PointerEvent) {
|
|
121
|
+
if (isEditableTarget(ev.target)) return;
|
|
122
|
+
if (hasNativeHorizontalScroll(ev.target)) return;
|
|
123
|
+
if (tabCount < 2) return;
|
|
124
|
+
if (containerWidth === 0) return;
|
|
125
|
+
// Multi-touch (pinch-zoom etc.) is not a swipe — bail.
|
|
126
|
+
if (ev.isPrimary === false) return;
|
|
127
|
+
const depth = containerEl ? ancestorCount(containerEl) : 0;
|
|
128
|
+
const claimGranted = claim(ev.pointerId, { ownerId: 'sh3:carousel', axis: 'x', priority: 'normal', depth });
|
|
129
|
+
if (!claimGranted) return;
|
|
130
|
+
activePointerId = ev.pointerId;
|
|
131
|
+
downSnap = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, t: performance.now() };
|
|
132
|
+
lastSnap = { ...downSnap };
|
|
133
|
+
dragging = false;
|
|
134
|
+
claimed = false;
|
|
135
|
+
dragDelta = 0;
|
|
136
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
137
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
138
|
+
document.addEventListener('pointercancel', onPointerCancel);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function onPointerMove(ev: PointerEvent) {
|
|
142
|
+
if (!downSnap || ev.pointerId !== downSnap.id) return;
|
|
143
|
+
if (!isOwner(ev.pointerId, 'sh3:carousel')) { endGesture(); return; }
|
|
144
|
+
const dx = ev.clientX - downSnap.x;
|
|
145
|
+
const dy = ev.clientY - downSnap.y;
|
|
146
|
+
if (!claimed) {
|
|
147
|
+
const elapsed = performance.now() - downSnap.t;
|
|
148
|
+
const horizDominates = Math.abs(dx) > Math.abs(dy) * AXIS_LOCK_DOMINANCE && Math.abs(dx) > AXIS_LOCK_MIN_DX;
|
|
149
|
+
const vertDominates = Math.abs(dy) > Math.abs(dx) * AXIS_LOCK_DOMINANCE && Math.abs(dy) > AXIS_LOCK_MIN_DX;
|
|
150
|
+
if (vertDominates) {
|
|
151
|
+
endGesture();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const movedPastThreshold = Math.abs(dx) > AXIS_LOCK_MIN_DX || Math.abs(dy) > AXIS_LOCK_MIN_DX;
|
|
155
|
+
if (movedPastThreshold && elapsed > SLOW_DRAG_TIMEOUT_MS) {
|
|
156
|
+
endGesture();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!horizDominates) return;
|
|
160
|
+
claimed = true;
|
|
161
|
+
dragging = true;
|
|
162
|
+
// Don't call setPointerCapture: transferring capture from a
|
|
163
|
+
// descendant that had implicit capture (a button the finger
|
|
164
|
+
// touched) makes Android fire pointercancel on the original
|
|
165
|
+
// target, which our document-level cancel listener would then
|
|
166
|
+
// treat as an abort. The pointercancel-aborts-no-commit logic
|
|
167
|
+
// is enough on its own to fix the auto-commit bug.
|
|
168
|
+
// Clear any selection that began during the pre-claim window so
|
|
169
|
+
// it doesn't visually streak across the slide while dragging.
|
|
170
|
+
if (typeof window !== 'undefined') {
|
|
171
|
+
window.getSelection()?.removeAllRanges();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (typeof ev.preventDefault === 'function') ev.preventDefault();
|
|
175
|
+
dragDelta = clampedDragDelta(dx);
|
|
176
|
+
lastSnap = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, t: performance.now() };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function onPointerUp(ev: PointerEvent) {
|
|
180
|
+
if (!downSnap || ev.pointerId !== downSnap.id) {
|
|
181
|
+
endGesture();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const dx = ev.clientX - downSnap.x;
|
|
185
|
+
let velocity = 0;
|
|
186
|
+
if (lastSnap && lastSnap.t !== downSnap.t) {
|
|
187
|
+
velocity = (ev.clientX - lastSnap.x) / Math.max(1, performance.now() - lastSnap.t) * 1000;
|
|
188
|
+
}
|
|
189
|
+
commitOrSnap(dx, velocity);
|
|
190
|
+
endGesture();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Pointer cancellation differs from release: the platform (or a
|
|
195
|
+
* descendant element claiming a competing gesture) revoked the
|
|
196
|
+
* pointer. Treat as abort — never commit. Without this distinction,
|
|
197
|
+
* a button or scroll region inside a slide that fires pointercancel
|
|
198
|
+
* mid-drag would be read as a release at the current dx and trip
|
|
199
|
+
* the commit threshold, "auto-completing" the swipe with the
|
|
200
|
+
* finger still down.
|
|
201
|
+
*/
|
|
202
|
+
function onPointerCancel(_ev: PointerEvent) {
|
|
203
|
+
endGesture();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function endGesture() {
|
|
207
|
+
if (activePointerId !== null) {
|
|
208
|
+
revoke(activePointerId, 'sh3:carousel');
|
|
209
|
+
activePointerId = null;
|
|
210
|
+
}
|
|
211
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
212
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
213
|
+
document.removeEventListener('pointercancel', onPointerCancel);
|
|
214
|
+
downSnap = null;
|
|
215
|
+
lastSnap = null;
|
|
216
|
+
dragging = false;
|
|
217
|
+
claimed = false;
|
|
218
|
+
dragDelta = 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function commitOrSnap(dx: number, velocity: number) {
|
|
222
|
+
if (!claimed || containerWidth === 0) return;
|
|
223
|
+
const distancePassed = Math.abs(dx) > containerWidth * COMMIT_FRACTION;
|
|
224
|
+
const velocityPassed = Math.abs(velocity) > COMMIT_VELOCITY;
|
|
225
|
+
if (!(distancePassed || velocityPassed)) return;
|
|
226
|
+
|
|
227
|
+
const direction = dx < 0 ? +1 : -1;
|
|
228
|
+
let next = node.activeTab + direction;
|
|
229
|
+
if (next < 0) {
|
|
230
|
+
if (wrap) next = tabCount - 1;
|
|
231
|
+
else return;
|
|
232
|
+
} else if (next >= tabCount) {
|
|
233
|
+
if (wrap) next = 0;
|
|
234
|
+
else return;
|
|
235
|
+
}
|
|
236
|
+
node.activeTab = next;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const trackTransform = $derived.by(() => {
|
|
240
|
+
if (containerWidth === 0) return 'translateX(0px)';
|
|
241
|
+
return `translateX(${-node.activeTab * containerWidth + dragDelta}px)`;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const trackTransition = $derived(dragging ? 'none' : '');
|
|
245
|
+
|
|
246
|
+
function entryKey(entry: TabEntry): string {
|
|
247
|
+
return entry.slotId;
|
|
248
|
+
}
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
<div
|
|
252
|
+
class="sh3-carousel"
|
|
253
|
+
data-sh3-region="carousel"
|
|
254
|
+
bind:this={containerEl}
|
|
255
|
+
>
|
|
256
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
257
|
+
<div
|
|
258
|
+
class="sh3-carousel-track"
|
|
259
|
+
class:sh3-carousel-track--dragging={dragging}
|
|
260
|
+
data-sh3-carousel-track
|
|
261
|
+
style="transform: {trackTransform}; width: {tabCount * 100}%; {trackTransition ? `transition: ${trackTransition};` : ''}"
|
|
262
|
+
onpointerdown={onPointerDown}
|
|
263
|
+
>
|
|
264
|
+
{#each node.tabs as entry, i (entryKey(entry))}
|
|
265
|
+
<div
|
|
266
|
+
class="sh3-carousel-slide"
|
|
267
|
+
data-sh3-slide
|
|
268
|
+
data-sh3-slide-index={i}
|
|
269
|
+
style="width: {containerWidth}px;"
|
|
270
|
+
>
|
|
271
|
+
<SlotContainer
|
|
272
|
+
node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }}
|
|
273
|
+
label={entry.label}
|
|
274
|
+
meta={entry.meta}
|
|
275
|
+
/>
|
|
276
|
+
<SlotDropZone {rootRef} path={path} />
|
|
277
|
+
</div>
|
|
278
|
+
{/each}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{#if showDots}
|
|
282
|
+
<div class="sh3-carousel-pager" data-sh3-pager>
|
|
283
|
+
{#each node.tabs as _entry, i (`dot-${i}`)}
|
|
284
|
+
<span
|
|
285
|
+
class="sh3-carousel-dot"
|
|
286
|
+
class:sh3-carousel-dot--active={i === node.activeTab}
|
|
287
|
+
data-sh3-pager-dot
|
|
288
|
+
aria-current={i === node.activeTab ? 'true' : undefined}
|
|
289
|
+
></span>
|
|
290
|
+
{/each}
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<style>
|
|
296
|
+
.sh3-carousel {
|
|
297
|
+
position: absolute;
|
|
298
|
+
inset: 0;
|
|
299
|
+
overflow: hidden;
|
|
300
|
+
touch-action: pan-y;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.sh3-carousel-track {
|
|
304
|
+
position: absolute;
|
|
305
|
+
inset: 0;
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-direction: row;
|
|
308
|
+
transition: transform 240ms cubic-bezier(0.2, 0, 0, 1);
|
|
309
|
+
will-change: transform;
|
|
310
|
+
}
|
|
311
|
+
/*
|
|
312
|
+
* While a horizontal drag is claimed, suppress selection on the slide
|
|
313
|
+
* content. user-select cascades to descendants, so this covers the
|
|
314
|
+
* whole carousel content. Inputs/textareas/contenteditables are not
|
|
315
|
+
* captured at all (see isEditableTarget in the script), so they are
|
|
316
|
+
* never inside a claimed drag.
|
|
317
|
+
*/
|
|
318
|
+
.sh3-carousel-track--dragging {
|
|
319
|
+
user-select: none;
|
|
320
|
+
-webkit-user-select: none;
|
|
321
|
+
cursor: grabbing;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sh3-carousel-slide {
|
|
325
|
+
position: relative;
|
|
326
|
+
flex: 0 0 auto;
|
|
327
|
+
height: 100%;
|
|
328
|
+
min-width: 0;
|
|
329
|
+
min-height: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.sh3-carousel-pager {
|
|
333
|
+
position: absolute;
|
|
334
|
+
left: 0;
|
|
335
|
+
right: 0;
|
|
336
|
+
bottom: var(--sh3-pad-md);
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
gap: var(--sh3-pad-sm);
|
|
340
|
+
pointer-events: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.sh3-carousel-dot {
|
|
344
|
+
width: 6px;
|
|
345
|
+
height: 6px;
|
|
346
|
+
border-radius: 50%;
|
|
347
|
+
background: var(--sh3-fg-muted);
|
|
348
|
+
opacity: 0.5;
|
|
349
|
+
transition: opacity 120ms, transform 120ms;
|
|
350
|
+
}
|
|
351
|
+
.sh3-carousel-dot--active {
|
|
352
|
+
opacity: 1;
|
|
353
|
+
transform: scale(1.4);
|
|
354
|
+
background: var(--sh3-fg);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@media (prefers-reduced-motion: reduce) {
|
|
358
|
+
.sh3-carousel-track {
|
|
359
|
+
transition: none;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TabsNode, TreeRootRef } from '../types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
node: TabsNode;
|
|
4
|
+
wrap: boolean;
|
|
5
|
+
rootRef?: TreeRootRef;
|
|
6
|
+
path?: number[];
|
|
7
|
+
};
|
|
8
|
+
declare const CarouselTabs: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type CarouselTabs = ReturnType<typeof CarouselTabs>;
|
|
10
|
+
export default CarouselTabs;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|