sh3-core 0.17.2 → 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.
Files changed (97) hide show
  1. package/dist/Sh3.svelte +59 -4
  2. package/dist/actions/CommandPalette.svelte +1 -2
  3. package/dist/actions/listeners.js +12 -1
  4. package/dist/api.d.ts +4 -0
  5. package/dist/app/store/storeShard.svelte.js +1 -21
  6. package/dist/app/store/version.d.ts +11 -0
  7. package/dist/app/store/version.js +39 -0
  8. package/dist/app/store/version.test.d.ts +1 -0
  9. package/dist/app/store/version.test.js +44 -0
  10. package/dist/apps/lifecycle.d.ts +6 -0
  11. package/dist/apps/lifecycle.js +5 -2
  12. package/dist/apps/lifecycle.test.js +30 -0
  13. package/dist/apps/types.d.ts +12 -0
  14. package/dist/assets/iconIds.generated.d.ts +1 -1
  15. package/dist/assets/iconIds.generated.js +5 -0
  16. package/dist/assets/icons.svg +31 -0
  17. package/dist/auth/auth.svelte.js +18 -8
  18. package/dist/auth/types.d.ts +6 -0
  19. package/dist/chrome/CompactChrome.svelte +54 -20
  20. package/dist/chrome/CompactChrome.svelte.test.js +112 -5
  21. package/dist/createShell.d.ts +9 -0
  22. package/dist/createShell.js +20 -7
  23. package/dist/createShell.remoteAuth.test.d.ts +1 -0
  24. package/dist/createShell.remoteAuth.test.js +71 -0
  25. package/dist/documents/http-backend.js +12 -11
  26. package/dist/env/client.js +11 -5
  27. package/dist/files/types.d.ts +106 -0
  28. package/dist/files/types.js +1 -0
  29. package/dist/gestures/gestureRegistry.d.ts +6 -0
  30. package/dist/gestures/gestureRegistry.js +190 -0
  31. package/dist/gestures/gestureRegistry.test.d.ts +1 -0
  32. package/dist/gestures/gestureRegistry.test.js +119 -0
  33. package/dist/gestures/index.d.ts +6 -0
  34. package/dist/gestures/index.js +12 -0
  35. package/dist/gestures/pointerClaim.d.ts +7 -0
  36. package/dist/gestures/pointerClaim.js +36 -0
  37. package/dist/gestures/pointerClaim.test.d.ts +1 -0
  38. package/dist/gestures/pointerClaim.test.js +64 -0
  39. package/dist/gestures/types.d.ts +83 -0
  40. package/dist/gestures/types.js +1 -0
  41. package/dist/host-entry.d.ts +1 -0
  42. package/dist/host-entry.js +1 -0
  43. package/dist/layout/LayoutRenderer.browser.test.js +15 -3
  44. package/dist/layout/LayoutRenderer.svelte +16 -3
  45. package/dist/layout/LayoutRenderer.svelte.d.ts +2 -0
  46. 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
  47. 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
  48. 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
  49. package/dist/layout/compact/CarouselTabs.svelte +361 -0
  50. package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
  51. package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
  52. package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
  53. package/dist/layout/compact/CompactRenderer.svelte +1 -1
  54. package/dist/layout/compact/CompactRenderer.svelte.test.js +49 -0
  55. package/dist/layout/compact/derive.js +2 -0
  56. package/dist/layout/compact/derive.test.js +37 -0
  57. package/dist/layout/compact/enrichCarousels.d.ts +8 -0
  58. package/dist/layout/compact/enrichCarousels.js +44 -0
  59. package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
  60. package/dist/layout/compact/enrichCarousels.test.js +88 -0
  61. package/dist/layout/compact/types.d.ts +3 -0
  62. package/dist/layout/drag.svelte.js +13 -0
  63. package/dist/layout/store.schemaVersion.test.js +2 -2
  64. package/dist/layout/types.d.ts +9 -1
  65. package/dist/layout/types.js +1 -1
  66. package/dist/layout/types.test.d.ts +1 -0
  67. package/dist/layout/types.test.js +26 -0
  68. package/dist/overlays/ModalFrame.svelte +3 -1
  69. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  70. package/dist/overlays/floatDismiss.js +5 -0
  71. package/dist/overlays/focusTrap.d.ts +11 -1
  72. package/dist/overlays/focusTrap.js +11 -9
  73. package/dist/overlays/modal.js +1 -0
  74. package/dist/overlays/popup.js +4 -0
  75. package/dist/overlays/types.d.ts +9 -0
  76. package/dist/primitives/Button.svelte +18 -0
  77. package/dist/primitives/Button.svelte.d.ts +6 -0
  78. package/dist/primitives/ResizableSplitter.svelte +71 -11
  79. package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
  80. package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
  81. package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
  82. package/dist/server-shard/types.d.ts +2 -1
  83. package/dist/shards/activate.svelte.js +10 -0
  84. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  85. package/dist/shards/ctx-fetch.test.js +66 -0
  86. package/dist/shards/types.d.ts +13 -0
  87. package/dist/transport/apiFetch.d.ts +1 -0
  88. package/dist/transport/apiFetch.js +65 -0
  89. package/dist/transport/apiFetch.test.d.ts +1 -0
  90. package/dist/transport/apiFetch.test.js +37 -0
  91. package/dist/transport/authToken.d.ts +2 -0
  92. package/dist/transport/authToken.js +53 -0
  93. package/dist/transport/authToken.test.d.ts +1 -0
  94. package/dist/transport/authToken.test.js +33 -0
  95. package/dist/version.d.ts +1 -1
  96. package/dist/version.js +1 -1
  97. 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(() => { cleanupDOM(); resetFramework(); });
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(() => { cleanupDOM(); resetFramework(); });
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(() => { cleanupDOM(); resetFramework(); });
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
- }: { path?: number[]; rootRef?: TreeRootRef; rootOverride?: LayoutNode } = $props();
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
- {#if tabs && tabs.tabs.length > 0}
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>;
@@ -0,0 +1,361 @@
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 } 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: 'edge', 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
+ const dx = ev.clientX - downSnap.x;
144
+ const dy = ev.clientY - downSnap.y;
145
+ if (!claimed) {
146
+ const elapsed = performance.now() - downSnap.t;
147
+ const horizDominates = Math.abs(dx) > Math.abs(dy) * AXIS_LOCK_DOMINANCE && Math.abs(dx) > AXIS_LOCK_MIN_DX;
148
+ const vertDominates = Math.abs(dy) > Math.abs(dx) * AXIS_LOCK_DOMINANCE && Math.abs(dy) > AXIS_LOCK_MIN_DX;
149
+ if (vertDominates) {
150
+ endGesture();
151
+ return;
152
+ }
153
+ const movedPastThreshold = Math.abs(dx) > AXIS_LOCK_MIN_DX || Math.abs(dy) > AXIS_LOCK_MIN_DX;
154
+ if (movedPastThreshold && elapsed > SLOW_DRAG_TIMEOUT_MS) {
155
+ endGesture();
156
+ return;
157
+ }
158
+ if (!horizDominates) return;
159
+ claimed = true;
160
+ dragging = true;
161
+ // Don't call setPointerCapture: transferring capture from a
162
+ // descendant that had implicit capture (a button the finger
163
+ // touched) makes Android fire pointercancel on the original
164
+ // target, which our document-level cancel listener would then
165
+ // treat as an abort. The pointercancel-aborts-no-commit logic
166
+ // is enough on its own to fix the auto-commit bug.
167
+ // Clear any selection that began during the pre-claim window so
168
+ // it doesn't visually streak across the slide while dragging.
169
+ if (typeof window !== 'undefined') {
170
+ window.getSelection()?.removeAllRanges();
171
+ }
172
+ }
173
+ if (typeof ev.preventDefault === 'function') ev.preventDefault();
174
+ dragDelta = clampedDragDelta(dx);
175
+ lastSnap = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, t: performance.now() };
176
+ }
177
+
178
+ function onPointerUp(ev: PointerEvent) {
179
+ if (!downSnap || ev.pointerId !== downSnap.id) {
180
+ endGesture();
181
+ return;
182
+ }
183
+ const dx = ev.clientX - downSnap.x;
184
+ let velocity = 0;
185
+ if (lastSnap && lastSnap.t !== downSnap.t) {
186
+ velocity = (ev.clientX - lastSnap.x) / Math.max(1, performance.now() - lastSnap.t) * 1000;
187
+ }
188
+ commitOrSnap(dx, velocity);
189
+ endGesture();
190
+ }
191
+
192
+ /**
193
+ * Pointer cancellation differs from release: the platform (or a
194
+ * descendant element claiming a competing gesture) revoked the
195
+ * pointer. Treat as abort — never commit. Without this distinction,
196
+ * a button or scroll region inside a slide that fires pointercancel
197
+ * mid-drag would be read as a release at the current dx and trip
198
+ * the commit threshold, "auto-completing" the swipe with the
199
+ * finger still down.
200
+ */
201
+ function onPointerCancel(_ev: PointerEvent) {
202
+ endGesture();
203
+ }
204
+
205
+ function endGesture() {
206
+ if (activePointerId !== null) {
207
+ revoke(activePointerId, 'sh3:carousel');
208
+ activePointerId = null;
209
+ }
210
+ document.removeEventListener('pointermove', onPointerMove);
211
+ document.removeEventListener('pointerup', onPointerUp);
212
+ document.removeEventListener('pointercancel', onPointerCancel);
213
+ downSnap = null;
214
+ lastSnap = null;
215
+ dragging = false;
216
+ claimed = false;
217
+ dragDelta = 0;
218
+ }
219
+
220
+ function commitOrSnap(dx: number, velocity: number) {
221
+ if (!claimed || containerWidth === 0) return;
222
+ const distancePassed = Math.abs(dx) > containerWidth * COMMIT_FRACTION;
223
+ const velocityPassed = Math.abs(velocity) > COMMIT_VELOCITY;
224
+ if (!(distancePassed || velocityPassed)) return;
225
+
226
+ const direction = dx < 0 ? +1 : -1;
227
+ let next = node.activeTab + direction;
228
+ if (next < 0) {
229
+ if (wrap) next = tabCount - 1;
230
+ else return;
231
+ } else if (next >= tabCount) {
232
+ if (wrap) next = 0;
233
+ else return;
234
+ }
235
+ node.activeTab = next;
236
+ }
237
+
238
+ const trackTransform = $derived.by(() => {
239
+ if (containerWidth === 0) return 'translateX(0px)';
240
+ return `translateX(${-node.activeTab * containerWidth + dragDelta}px)`;
241
+ });
242
+
243
+ const trackTransition = $derived(dragging ? 'none' : '');
244
+
245
+ function entryKey(entry: TabEntry): string {
246
+ return entry.slotId;
247
+ }
248
+ </script>
249
+
250
+ <div
251
+ class="sh3-carousel"
252
+ data-sh3-region="carousel"
253
+ bind:this={containerEl}
254
+ >
255
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
256
+ <div
257
+ class="sh3-carousel-track"
258
+ class:sh3-carousel-track--dragging={dragging}
259
+ data-sh3-carousel-track
260
+ style="transform: {trackTransform}; width: {tabCount * 100}%; {trackTransition ? `transition: ${trackTransition};` : ''}"
261
+ onpointerdown={onPointerDown}
262
+ >
263
+ {#each node.tabs as entry, i (entryKey(entry))}
264
+ <div
265
+ class="sh3-carousel-slide"
266
+ data-sh3-slide
267
+ data-sh3-slide-index={i}
268
+ style="width: {containerWidth}px;"
269
+ >
270
+ <SlotContainer
271
+ node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }}
272
+ label={entry.label}
273
+ meta={entry.meta}
274
+ />
275
+ <SlotDropZone {rootRef} path={path} />
276
+ </div>
277
+ {/each}
278
+ </div>
279
+
280
+ {#if showDots}
281
+ <div class="sh3-carousel-pager" data-sh3-pager>
282
+ {#each node.tabs as _entry, i (`dot-${i}`)}
283
+ <span
284
+ class="sh3-carousel-dot"
285
+ class:sh3-carousel-dot--active={i === node.activeTab}
286
+ data-sh3-pager-dot
287
+ aria-current={i === node.activeTab ? 'true' : undefined}
288
+ ></span>
289
+ {/each}
290
+ </div>
291
+ {/if}
292
+ </div>
293
+
294
+ <style>
295
+ .sh3-carousel {
296
+ position: absolute;
297
+ inset: 0;
298
+ overflow: hidden;
299
+ touch-action: pan-y;
300
+ }
301
+
302
+ .sh3-carousel-track {
303
+ position: absolute;
304
+ inset: 0;
305
+ display: flex;
306
+ flex-direction: row;
307
+ transition: transform 240ms cubic-bezier(0.2, 0, 0, 1);
308
+ will-change: transform;
309
+ }
310
+ /*
311
+ * While a horizontal drag is claimed, suppress selection on the slide
312
+ * content. user-select cascades to descendants, so this covers the
313
+ * whole carousel content. Inputs/textareas/contenteditables are not
314
+ * captured at all (see isEditableTarget in the script), so they are
315
+ * never inside a claimed drag.
316
+ */
317
+ .sh3-carousel-track--dragging {
318
+ user-select: none;
319
+ -webkit-user-select: none;
320
+ cursor: grabbing;
321
+ }
322
+
323
+ .sh3-carousel-slide {
324
+ position: relative;
325
+ flex: 0 0 auto;
326
+ height: 100%;
327
+ min-width: 0;
328
+ min-height: 0;
329
+ }
330
+
331
+ .sh3-carousel-pager {
332
+ position: absolute;
333
+ left: 0;
334
+ right: 0;
335
+ bottom: var(--sh3-pad-md);
336
+ display: flex;
337
+ justify-content: center;
338
+ gap: var(--sh3-pad-sm);
339
+ pointer-events: none;
340
+ }
341
+
342
+ .sh3-carousel-dot {
343
+ width: 6px;
344
+ height: 6px;
345
+ border-radius: 50%;
346
+ background: var(--sh3-fg-muted);
347
+ opacity: 0.5;
348
+ transition: opacity 120ms, transform 120ms;
349
+ }
350
+ .sh3-carousel-dot--active {
351
+ opacity: 1;
352
+ transform: scale(1.4);
353
+ background: var(--sh3-fg);
354
+ }
355
+
356
+ @media (prefers-reduced-motion: reduce) {
357
+ .sh3-carousel-track {
358
+ transition: none;
359
+ }
360
+ }
361
+ </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 {};