@sovereignfs/ui 0.54.0 → 0.83.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/Accordion.module.css +1 -1
- package/dist/Alert.module.css +1 -1
- package/dist/AppsLauncher.module.css +96 -0
- package/dist/Avatar.module.css +1 -1
- package/dist/Badge.module.css +30 -7
- package/dist/BalanceChip.module.css +1 -1
- package/dist/Button.module.css +18 -2
- package/dist/Card.module.css +1 -1
- package/dist/CardTile.module.css +181 -0
- package/dist/CodeTextarea.module.css +1 -1
- package/dist/Collapsible.module.css +1 -1
- package/dist/ColorPicker.module.css +132 -0
- package/dist/Combobox.module.css +2 -2
- package/dist/Command.module.css +1 -1
- package/dist/ConfirmDialog.module.css +2 -2
- package/dist/ConsentPrompt.module.css +21 -0
- package/dist/DatePicker.module.css +1 -1
- package/dist/Dialog.module.css +130 -21
- package/dist/Drawer.module.css +36 -7
- package/dist/Header.module.css +62 -0
- package/dist/HeaderFooterLayout.module.css +21 -0
- package/dist/IconPicker.module.css +1 -1
- package/dist/Input.module.css +1 -1
- package/dist/Kbd.module.css +1 -1
- package/dist/Markdown.module.css +24 -0
- package/dist/Menu.module.css +1 -0
- package/dist/Message.module.css +1 -1
- package/dist/MessageScroller.module.css +12 -1
- package/dist/MobileAppsDrawer.module.css +10 -9
- package/dist/MobileFooter.module.css +1 -1
- package/dist/MobileHeader.module.css +14 -1
- package/dist/NavList.module.css +130 -0
- package/dist/NavTabs.module.css +1 -1
- package/dist/NotificationsPanel.module.css +258 -0
- package/dist/OverlayHeader.module.css +1 -1
- package/dist/PageContainer.module.css +62 -0
- package/dist/PageHeader.module.css +54 -0
- package/dist/PageLayout.module.css +51 -0
- package/dist/Popover.module.css +1 -1
- package/dist/QuantityStepper.module.css +1 -1
- package/dist/Resizable.module.css +3 -3
- package/dist/RootLayout.module.css +12 -0
- package/dist/SegmentedControl.module.css +1 -1
- package/dist/Select.module.css +1 -1
- package/dist/Sheet.module.css +4 -1
- package/dist/SplitPane.module.css +4 -4
- package/dist/StatusBadge.module.css +1 -1
- package/dist/StepStrip.module.css +53 -0
- package/dist/SuggestionInput.module.css +2 -2
- package/dist/SwipableMobileCarousel.module.css +12 -3
- package/dist/SwipableMobileCarouselDots.module.css +15 -0
- package/dist/SwipeStack.module.css +101 -0
- package/dist/SystemBanner.module.css +10 -0
- package/dist/Table.module.css +2 -2
- package/dist/Tabs.module.css +1 -1
- package/dist/TagInput.module.css +2 -2
- package/dist/Textarea.module.css +9 -1
- package/dist/ThreeColumnLayout.module.css +35 -0
- package/dist/Toast.module.css +62 -3
- package/dist/Tooltip.module.css +1 -1
- package/dist/UserMenu.module.css +90 -0
- package/dist/index.d.ts +1047 -45
- package/dist/index.js +3908 -1456
- package/dist/tokens/primitives.css +11 -0
- package/dist/tokens/semantic.css +32 -0
- package/dist/tokens/theme-presets.css +33 -0
- package/package.json +25 -9
- package/src/__tests__/package-json.test.ts +8 -0
- package/src/components/Accordion/Accordion.module.css +1 -1
- package/src/components/Alert/Alert.module.css +1 -1
- package/src/components/AppsLauncher/AppsLauncher.module.css +96 -0
- package/src/components/AppsLauncher/AppsLauncher.stories.tsx +58 -0
- package/src/components/AppsLauncher/AppsLauncher.tsx +117 -0
- package/src/components/AppsLauncher/__tests__/AppsLauncher.test.tsx +52 -0
- package/src/components/Avatar/Avatar.module.css +1 -1
- package/src/components/Badge/Badge.module.css +30 -7
- package/src/components/Badge/Badge.stories.tsx +16 -4
- package/src/components/Badge/Badge.tsx +7 -4
- package/src/components/Badge/__tests__/Badge.test.tsx +4 -2
- package/src/components/BalanceChip/BalanceChip.module.css +1 -1
- package/src/components/Button/Button.module.css +18 -2
- package/src/components/Card/Card.module.css +1 -1
- package/src/components/CardTile/CardTile.module.css +181 -0
- package/src/components/CardTile/CardTile.stories.tsx +86 -0
- package/src/components/CardTile/CardTile.tsx +128 -0
- package/src/components/CardTile/__tests__/CardTile.test.tsx +56 -0
- package/src/components/CodeTextarea/CodeTextarea.module.css +1 -1
- package/src/components/Collapsible/Collapsible.module.css +1 -1
- package/src/components/ColorPicker/ColorPicker.module.css +132 -0
- package/src/components/ColorPicker/ColorPicker.stories.tsx +80 -0
- package/src/components/ColorPicker/ColorPicker.tsx +157 -0
- package/src/components/ColorPicker/__tests__/ColorPicker.test.tsx +117 -0
- package/src/components/Combobox/Combobox.module.css +2 -2
- package/src/components/Command/Command.module.css +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +2 -2
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +28 -2
- package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +71 -3
- package/src/components/ConsentPrompt/ConsentPrompt.module.css +21 -0
- package/src/components/ConsentPrompt/ConsentPrompt.stories.tsx +31 -0
- package/src/components/ConsentPrompt/ConsentPrompt.tsx +66 -0
- package/src/components/ContextMenu/ContextMenu.tsx +1 -1
- package/src/components/DatePicker/DatePicker.module.css +1 -1
- package/src/components/Dialog/Dialog.module.css +130 -21
- package/src/components/Dialog/Dialog.stories.tsx +179 -2
- package/src/components/Dialog/Dialog.tsx +129 -35
- package/src/components/Dialog/__tests__/Dialog.test.tsx +190 -9
- package/src/components/DragHandleRow/DragHandleRow.tsx +2 -21
- package/src/components/Drawer/Drawer.module.css +36 -7
- package/src/components/Drawer/Drawer.tsx +9 -10
- package/src/components/Drawer/__tests__/Drawer.test.tsx +33 -6
- package/src/components/GripIcon/GripIcon.tsx +27 -0
- package/src/components/Header/Header.module.css +62 -0
- package/src/components/Header/Header.stories.tsx +452 -0
- package/src/components/Header/Header.tsx +102 -0
- package/src/components/Header/__tests__/Header.test.tsx +104 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.module.css +21 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.stories.tsx +111 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.tsx +54 -0
- package/src/components/HeaderFooterLayout/__tests__/HeaderFooterLayout.test.tsx +58 -0
- package/src/components/Icon/icons/arrow-left-right.tsx +23 -0
- package/src/components/Icon/icons/bold.tsx +20 -0
- package/src/components/Icon/icons/calendar.tsx +4 -4
- package/src/components/Icon/icons/carrot.tsx +3 -3
- package/src/components/Icon/icons/circle-chevron-left.tsx +21 -0
- package/src/components/Icon/icons/code.tsx +21 -0
- package/src/components/Icon/icons/copy.tsx +21 -0
- package/src/components/Icon/icons/credit-card.tsx +21 -0
- package/src/components/Icon/icons/database.tsx +22 -0
- package/src/components/Icon/icons/download.tsx +22 -0
- package/src/components/Icon/icons/file-text.tsx +24 -0
- package/src/components/Icon/icons/folder-closed.tsx +21 -0
- package/src/components/Icon/icons/folder-open.tsx +20 -0
- package/src/components/Icon/icons/folder.tsx +20 -0
- package/src/components/Icon/icons/folders.tsx +21 -0
- package/src/components/Icon/icons/hat-glasses.tsx +24 -0
- package/src/components/Icon/icons/history.tsx +22 -0
- package/src/components/Icon/icons/inbox.tsx +21 -0
- package/src/components/Icon/icons/index.ts +161 -1
- package/src/components/Icon/icons/italic.tsx +22 -0
- package/src/components/Icon/icons/layers.tsx +22 -0
- package/src/components/Icon/icons/layout-dashboard.tsx +23 -0
- package/src/components/Icon/icons/layout-grid.tsx +23 -0
- package/src/components/Icon/icons/link.tsx +21 -0
- package/src/components/Icon/icons/list-ordered.tsx +25 -0
- package/src/components/Icon/icons/list.tsx +25 -0
- package/src/components/Icon/icons/luggage.tsx +24 -0
- package/src/components/Icon/icons/map-pin.tsx +21 -0
- package/src/components/Icon/icons/minus.tsx +20 -0
- package/src/components/Icon/icons/paint-bucket.tsx +23 -0
- package/src/components/Icon/icons/panel-left.tsx +21 -0
- package/src/components/Icon/icons/pin.tsx +21 -0
- package/src/components/Icon/icons/rectangle-ellipsis.tsx +23 -0
- package/src/components/Icon/icons/refresh-cw.tsx +23 -0
- package/src/components/Icon/icons/rotate-cw.tsx +21 -0
- package/src/components/Icon/icons/route.tsx +22 -0
- package/src/components/Icon/icons/send.tsx +21 -0
- package/src/components/Icon/icons/share-2.tsx +24 -0
- package/src/components/Icon/icons/sheet.tsx +24 -0
- package/src/components/Icon/icons/square-kanban.tsx +23 -0
- package/src/components/Icon/icons/table.tsx +23 -0
- package/src/components/Icon/icons/user-round-plus.tsx +23 -0
- package/src/components/Icon/icons/users.tsx +23 -0
- package/src/components/IconPicker/IconPicker.module.css +1 -1
- package/src/components/Input/Input.module.css +1 -1
- package/src/components/Input/Input.tsx +8 -2
- package/src/components/Kbd/Kbd.module.css +1 -1
- package/src/components/Markdown/Markdown.module.css +24 -0
- package/src/components/Markdown/Markdown.stories.tsx +10 -0
- package/src/components/Markdown/Markdown.tsx +166 -29
- package/src/components/Markdown/__tests__/Markdown.test.tsx +144 -0
- package/src/components/Menu/Menu.module.css +1 -0
- package/src/components/Menu/Menu.tsx +72 -22
- package/src/components/Menu/__tests__/Menu.test.tsx +30 -0
- package/src/components/Menubar/Menubar.tsx +1 -1
- package/src/components/Message/Message.module.css +1 -1
- package/src/components/MessageScroller/MessageScroller.module.css +12 -1
- package/src/components/MessageScroller/MessageScroller.tsx +12 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +10 -9
- package/src/components/MobileFooter/MobileFooter.module.css +1 -1
- package/src/components/MobileFooter/MobileFooter.tsx +15 -2
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +58 -0
- package/src/components/MobileHeader/MobileHeader.module.css +14 -1
- package/src/components/MobileHeader/MobileHeader.tsx +15 -2
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +56 -1
- package/src/components/NavList/NavList.module.css +130 -0
- package/src/components/NavList/NavList.tsx +136 -0
- package/src/components/NavList/__tests__/NavList.test.tsx +109 -0
- package/src/components/NavTabs/NavTabs.module.css +1 -1
- package/src/components/NotificationsPanel/NotificationsPanel.module.css +258 -0
- package/src/components/NotificationsPanel/NotificationsPanel.stories.tsx +61 -0
- package/src/components/NotificationsPanel/NotificationsPanel.tsx +187 -0
- package/src/components/NotificationsPanel/__tests__/NotificationsPanel.test.tsx +78 -0
- package/src/components/OverlayHeader/OverlayHeader.module.css +1 -1
- package/src/components/OverlayHeader/OverlayHeader.tsx +19 -4
- package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +18 -0
- package/src/components/PageContainer/PageContainer.module.css +62 -0
- package/src/components/PageContainer/PageContainer.tsx +37 -4
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +41 -8
- package/src/components/PageHeader/PageHeader.module.css +54 -0
- package/src/components/PageHeader/PageHeader.tsx +146 -5
- package/src/components/PageHeader/__tests__/PageHeader.test.tsx +103 -0
- package/src/components/PageLayout/PageLayout.module.css +51 -0
- package/src/components/PageLayout/PageLayout.stories.tsx +73 -0
- package/src/components/PageLayout/PageLayout.tsx +51 -0
- package/src/components/PageLayout/__tests__/PageLayout.test.tsx +34 -0
- package/src/components/Popover/Popover.module.css +1 -1
- package/src/components/Popover/Popover.tsx +86 -4
- package/src/components/Popover/__tests__/Popover.test.tsx +165 -0
- package/src/components/QuantityStepper/QuantityStepper.module.css +1 -1
- package/src/components/Resizable/Resizable.module.css +3 -3
- package/src/components/RootLayout/RootLayout.module.css +12 -0
- package/src/components/RootLayout/RootLayout.stories.tsx +125 -0
- package/src/components/RootLayout/RootLayout.tsx +185 -0
- package/src/components/RootLayout/__tests__/RootLayout.test.tsx +161 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +1 -1
- package/src/components/Select/Select.module.css +1 -1
- package/src/components/Sheet/Sheet.module.css +4 -1
- package/src/components/Sheet/Sheet.tsx +5 -5
- package/src/components/SplitPane/SplitPane.module.css +4 -4
- package/src/components/StatusBadge/StatusBadge.module.css +1 -1
- package/src/components/StepStrip/StepStrip.module.css +53 -0
- package/src/components/StepStrip/StepStrip.tsx +74 -0
- package/src/components/SuggestionInput/SuggestionInput.module.css +2 -2
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +12 -3
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +25 -9
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +81 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +15 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +12 -1
- package/src/components/SwipeStack/SwipeStack.module.css +101 -0
- package/src/components/SwipeStack/SwipeStack.tsx +199 -0
- package/src/components/SwipeStack/SwipeStackCard.tsx +22 -0
- package/src/components/SwipeStack/__tests__/SwipeStack.test.tsx +169 -0
- package/src/components/SystemBanner/SystemBanner.module.css +10 -0
- package/src/components/Table/Table.module.css +2 -2
- package/src/components/Tabs/Tabs.module.css +1 -1
- package/src/components/TagInput/TagInput.module.css +2 -2
- package/src/components/Textarea/Textarea.module.css +9 -1
- package/src/components/Textarea/Textarea.tsx +28 -5
- package/src/components/Textarea/__tests__/Textarea.test.tsx +23 -1
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.module.css +35 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.stories.tsx +152 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.tsx +87 -0
- package/src/components/ThreeColumnLayout/__tests__/ThreeColumnLayout.test.tsx +78 -0
- package/src/components/Toast/Toast.module.css +62 -3
- package/src/components/Toast/Toast.stories.tsx +1 -1
- package/src/components/Tooltip/Tooltip.module.css +1 -1
- package/src/components/UserMenu/UserMenu.module.css +90 -0
- package/src/components/UserMenu/UserMenu.stories.tsx +49 -0
- package/src/components/UserMenu/UserMenu.tsx +132 -0
- package/src/components/UserMenu/__tests__/UserMenu.test.tsx +77 -0
- package/src/hooks/__tests__/usePublishShellChromeHeight.test.tsx +118 -0
- package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +82 -0
- package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/usePublishShellChromeHeight.ts +96 -0
- package/src/hooks/useReorderSensors.ts +88 -0
- package/src/hooks/useSnapCarousel.ts +211 -6
- package/src/hooks/useSwipeStack.ts +235 -0
- package/src/index.ts +61 -1
- package/src/overlay-shell.ts +65 -0
- package/src/stories/DesignSystemOverview.stories.tsx +617 -11
- package/src/stories/GripIcon.stories.tsx +29 -0
- package/src/stories/LayoutSystem.stories.tsx +639 -0
- package/src/stories/MenuEntries.stories.tsx +86 -0
- package/src/stories/NavList.stories.tsx +147 -0
- package/src/stories/PageContainer.stories.tsx +52 -7
- package/src/stories/PageHeader.stories.tsx +54 -1
- package/src/stories/StepStrip.stories.tsx +76 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +58 -0
- package/src/stories/SwipeStack.stories.tsx +200 -0
- package/src/stories/Textarea.stories.tsx +12 -0
- package/src/stories/TokenGallery.stories.tsx +50 -0
- package/src/tokens/primitives.css +11 -0
- package/src/tokens/semantic.css +32 -0
- package/src/tokens/theme-presets.css +33 -0
- package/src/tokens/theme-presets.ts +72 -0
|
@@ -131,17 +131,18 @@ export function SwipableMobileCarousel({
|
|
|
131
131
|
const count = slides.length;
|
|
132
132
|
const clampedActiveIndex = count === 0 ? 0 : Math.max(0, Math.min(count - 1, activeIndex));
|
|
133
133
|
|
|
134
|
-
const { scrollRef, scrollToIndex } = useSnapCarousel({
|
|
135
|
-
itemCount: count,
|
|
136
|
-
onSettle,
|
|
137
|
-
debounceMs: settleDebounceMs,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
134
|
// Initial scroll position, once — subsequent activeIndex changes come
|
|
141
135
|
// either from the user's own swipe (already reflected in DOM scroll
|
|
142
136
|
// position; re-syncing to the same index below is a harmless no-op) or an
|
|
143
137
|
// external navigation, which the effect below scrolls to explicitly.
|
|
144
138
|
const initialActiveIndexRef = useRef(clampedActiveIndex);
|
|
139
|
+
|
|
140
|
+
const { scrollRef, scrollToIndex, liveIndex } = useSnapCarousel({
|
|
141
|
+
itemCount: count,
|
|
142
|
+
onSettle,
|
|
143
|
+
debounceMs: settleDebounceMs,
|
|
144
|
+
initialIndex: initialActiveIndexRef.current,
|
|
145
|
+
});
|
|
145
146
|
useLayoutEffect(() => {
|
|
146
147
|
scrollToIndex(initialActiveIndexRef.current, 'instant');
|
|
147
148
|
// Runs once on mount only, matching both existing plugins' identical pattern.
|
|
@@ -199,7 +200,6 @@ export function SwipableMobileCarousel({
|
|
|
199
200
|
renderIndicator({ count, activeIndex: clampedActiveIndex, labels, onJump: handleJump })
|
|
200
201
|
) : (
|
|
201
202
|
<SwipableMobileCarouselDots
|
|
202
|
-
className={styles.dots}
|
|
203
203
|
count={count}
|
|
204
204
|
activeIndex={clampedActiveIndex}
|
|
205
205
|
labels={labels}
|
|
@@ -214,14 +214,30 @@ export function SwipableMobileCarousel({
|
|
|
214
214
|
{slides.map((slide, i) => (
|
|
215
215
|
<div className={styles.slide} key={slide.key}>
|
|
216
216
|
<CarouselSlideMountContext.Provider
|
|
217
|
-
value={{
|
|
217
|
+
value={{
|
|
218
|
+
// Union of the settled index and the live (real-time,
|
|
219
|
+
// pre-settle) scroll position — see useSnapCarousel's own
|
|
220
|
+
// liveIndex doc comment. A single fast flick can carry
|
|
221
|
+
// native scroll-snap momentum past an intermediate slide to
|
|
222
|
+
// one two-or-more away before settle-detection's debounce
|
|
223
|
+
// window ever reports it; without also checking liveIndex,
|
|
224
|
+
// the slide the container had already visually scrolled to
|
|
225
|
+
// stays unmounted (rendering nothing at all, not even a
|
|
226
|
+
// loading skeleton) for that entire window.
|
|
227
|
+
isMounted:
|
|
228
|
+
Math.abs(i - clampedActiveIndex) <= prefetchDistance ||
|
|
229
|
+
Math.abs(i - liveIndex) <= prefetchDistance,
|
|
230
|
+
}}
|
|
218
231
|
>
|
|
219
232
|
{slide.element}
|
|
220
233
|
</CarouselSlideMountContext.Provider>
|
|
221
234
|
</div>
|
|
222
235
|
))}
|
|
223
236
|
</div>
|
|
224
|
-
{
|
|
237
|
+
{/* Positioning (overlay the bottom of .wrap) lives here, applied around
|
|
238
|
+
whatever renderIndicator returns — see .indicatorSlot's own doc
|
|
239
|
+
comment for why this can't be delegated to the indicator itself. */}
|
|
240
|
+
{indicator && <div className={styles.indicatorSlot}>{indicator}</div>}
|
|
225
241
|
</div>
|
|
226
242
|
);
|
|
227
243
|
}
|
|
@@ -79,6 +79,7 @@ describe('SwipableMobileCarousel', () => {
|
|
|
79
79
|
const onSettle = vi.fn();
|
|
80
80
|
render(<FiveSlides activeIndex={0} onSettle={onSettle} />);
|
|
81
81
|
const scroller = screen.getByRole('region', { name: 'Test slides' });
|
|
82
|
+
fireEvent.touchStart(scroller);
|
|
82
83
|
setLayout(scroller, 300, 300); // scrollLeft 300 / width 300 -> index 1
|
|
83
84
|
fireEvent.scroll(scroller);
|
|
84
85
|
act(() => void vi.advanceTimersByTime(120));
|
|
@@ -96,6 +97,43 @@ describe('SwipableMobileCarousel', () => {
|
|
|
96
97
|
expect(onSettle).toHaveBeenCalledWith(3);
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
it('positions both the default and a custom renderIndicator inside the same overlay slot', () => {
|
|
101
|
+
// Regression test: the overlay-positioning class used to be applied
|
|
102
|
+
// directly to the default SwipableMobileCarouselDots instance, so any
|
|
103
|
+
// caller-supplied renderIndicator rendered as a plain static element
|
|
104
|
+
// instead — pushed out of view below the full-height scroller. The
|
|
105
|
+
// carousel must own positioning for whatever renderIndicator returns.
|
|
106
|
+
const { unmount } = render(<FiveSlides activeIndex={0} onSettle={() => {}} />);
|
|
107
|
+
const defaultTablist = screen.getByRole('tablist');
|
|
108
|
+
expect(defaultTablist.parentElement?.className).toMatch(/indicatorSlot/);
|
|
109
|
+
unmount();
|
|
110
|
+
|
|
111
|
+
render(
|
|
112
|
+
<SwipableMobileCarousel
|
|
113
|
+
activeIndex={0}
|
|
114
|
+
onSettle={() => {}}
|
|
115
|
+
aria-label="Test slides"
|
|
116
|
+
renderIndicator={({ count, activeIndex, onJump }) => (
|
|
117
|
+
<div role="tablist" aria-label="Custom">
|
|
118
|
+
{Array.from({ length: count }, (_, i) => (
|
|
119
|
+
<button key={i} type="button" role="tab" onClick={() => onJump(i)}>
|
|
120
|
+
{i === activeIndex ? 'active' : 'inactive'}
|
|
121
|
+
</button>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{KEYS.map((key) => (
|
|
127
|
+
<SwipableMobileCarouselSlide key={key} slideKey={key} label={key}>
|
|
128
|
+
{`slide-${key}`}
|
|
129
|
+
</SwipableMobileCarouselSlide>
|
|
130
|
+
))}
|
|
131
|
+
</SwipableMobileCarousel>,
|
|
132
|
+
);
|
|
133
|
+
const customTablist = screen.getByRole('tablist', { name: 'Custom' });
|
|
134
|
+
expect(customTablist.parentElement?.className).toMatch(/indicatorSlot/);
|
|
135
|
+
});
|
|
136
|
+
|
|
99
137
|
it('re-snaps and reports the new index when the active slide moves position (reorder-jump fix)', () => {
|
|
100
138
|
function Reorderable({ order, onSettle }: { order: string[]; onSettle: (i: number) => void }) {
|
|
101
139
|
return (
|
|
@@ -121,6 +159,49 @@ describe('SwipableMobileCarousel', () => {
|
|
|
121
159
|
expect(spy).toHaveBeenCalledWith({ left: 0, behavior: 'instant' });
|
|
122
160
|
});
|
|
123
161
|
|
|
162
|
+
it('mounts a slide reached by a fast multi-slide flick before settle-detection reports it', () => {
|
|
163
|
+
// Regression test, found via a user screen recording: a fast flick can
|
|
164
|
+
// carry native scroll-snap momentum straight past an intermediate slide
|
|
165
|
+
// to one two-or-more away in a single continuous gesture. Settle-
|
|
166
|
+
// detection deliberately still waits out the full debounce window before
|
|
167
|
+
// calling onSettle (to distinguish a real settle from drift — see
|
|
168
|
+
// useSnapCarousel's chainTrustedRef comment), but the mount window used
|
|
169
|
+
// to be keyed *only* off that settled index — so the slide the container
|
|
170
|
+
// had already scrolled to, visually, sat completely unmounted (not even
|
|
171
|
+
// a loading skeleton, per SwipableMobileCarouselSlide's `return null`)
|
|
172
|
+
// for the entire debounce window. liveIndex closes that gap.
|
|
173
|
+
const onSettle = vi.fn();
|
|
174
|
+
const KEYS8 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
|
175
|
+
function EightSlides({ onSettle }: { onSettle: (i: number) => void }) {
|
|
176
|
+
return (
|
|
177
|
+
<SwipableMobileCarousel activeIndex={0} onSettle={onSettle} aria-label="Test slides">
|
|
178
|
+
{KEYS8.map((key) => (
|
|
179
|
+
<SwipableMobileCarouselSlide key={key} slideKey={key} label={key}>
|
|
180
|
+
<SwipableMobileCarouselSlideHeader>{`header-${key}`}</SwipableMobileCarouselSlideHeader>
|
|
181
|
+
</SwipableMobileCarouselSlide>
|
|
182
|
+
))}
|
|
183
|
+
</SwipableMobileCarousel>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
render(<EightSlides onSettle={onSettle} />);
|
|
187
|
+
const scroller = screen.getByRole('region', { name: 'Test slides' });
|
|
188
|
+
// Slide 'e' (index 4) is well outside the default prefetchDistance=1
|
|
189
|
+
// window around the settled index 0.
|
|
190
|
+
expect(screen.queryByText('header-e')).toBeNull();
|
|
191
|
+
|
|
192
|
+
fireEvent.touchStart(scroller);
|
|
193
|
+
setLayout(scroller, 300, 1200); // scrollLeft 1200 / width 300 -> index 4
|
|
194
|
+
fireEvent.scroll(scroller);
|
|
195
|
+
// Well short of the 120ms settle debounce — onSettle must not have fired
|
|
196
|
+
// yet, but the rAF-throttled liveIndex update has had time to run.
|
|
197
|
+
act(() => void vi.advanceTimersByTime(20));
|
|
198
|
+
expect(onSettle).not.toHaveBeenCalled();
|
|
199
|
+
expect(screen.getByText('header-e')).toBeDefined();
|
|
200
|
+
|
|
201
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
202
|
+
expect(onSettle).toHaveBeenCalledWith(4);
|
|
203
|
+
});
|
|
204
|
+
|
|
124
205
|
it('does not re-snap when slide order is unchanged', () => {
|
|
125
206
|
function Reorderable({ onSettle }: { onSettle: (i: number) => void }) {
|
|
126
207
|
return <FiveSlides activeIndex={2} onSettle={onSettle} />;
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
gap: var(--sv-space-2);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/* Opt-in — see the `density` prop doc comment. Shrinks both the gap and each
|
|
9
|
+
dot's own hit target (20px -> 16px) — for a carousel with enough slides
|
|
10
|
+
that even the space-1 gap alone still read as too wide. The visible 6px
|
|
11
|
+
dot is unchanged; only the surrounding tap-target box shrinks, via the
|
|
12
|
+
descendant override below rather than a second dot variant, so this stays
|
|
13
|
+
a pure CSS opt-in with no JS/prop plumbing beyond `density` itself. */
|
|
14
|
+
.dotsCompact {
|
|
15
|
+
gap: var(--sv-space-1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.dotsCompact .dot {
|
|
19
|
+
width: 16px;
|
|
20
|
+
height: 16px;
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
.dot {
|
|
9
24
|
/* A 6px visible dot is too small a hit target on its own — the button's
|
|
10
25
|
real box is larger (20px) with the dot itself painted by ::before and
|
|
@@ -12,6 +12,14 @@ export interface SwipableMobileCarouselDotsProps {
|
|
|
12
12
|
/** Labels the whole group — required, no generic fallback (mirrors
|
|
13
13
|
* SwipableMobileCarousel's own required aria-label). */
|
|
14
14
|
'aria-label': string;
|
|
15
|
+
/** `'compact'` halves the gap between dots (`--sv-space-2` → `--sv-space-1`)
|
|
16
|
+
* and shrinks each dot's own hit target (20px → 16px) — for a carousel
|
|
17
|
+
* with enough slides that even the reduced gap alone still reads as too
|
|
18
|
+
* wide on a narrow viewport (e.g. a many-list mobile app). The visible
|
|
19
|
+
* 6px dot itself is unchanged; only its surrounding tap-target box
|
|
20
|
+
* shrinks. Defaults to `'default'` — every existing consumer's spacing is
|
|
21
|
+
* unaffected unless it opts in. */
|
|
22
|
+
density?: 'default' | 'compact';
|
|
15
23
|
className?: string;
|
|
16
24
|
}
|
|
17
25
|
|
|
@@ -34,13 +42,16 @@ export function SwipableMobileCarouselDots({
|
|
|
34
42
|
onJump,
|
|
35
43
|
labels,
|
|
36
44
|
'aria-label': ariaLabel,
|
|
45
|
+
density = 'default',
|
|
37
46
|
className,
|
|
38
47
|
}: SwipableMobileCarouselDotsProps) {
|
|
39
48
|
return (
|
|
40
49
|
<div
|
|
41
50
|
role="tablist"
|
|
42
51
|
aria-label={ariaLabel}
|
|
43
|
-
className={[styles.dots,
|
|
52
|
+
className={[styles.dots, density === 'compact' ? styles.dotsCompact : '', className]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(' ')}
|
|
44
55
|
>
|
|
45
56
|
{Array.from({ length: count }, (_, i) => {
|
|
46
57
|
const active = i === activeIndex;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
.wrap {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--sv-space-3);
|
|
5
|
+
height: 100%;
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.stack {
|
|
10
|
+
position: relative;
|
|
11
|
+
flex: 1 1 auto;
|
|
12
|
+
min-height: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.peek {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: 0;
|
|
18
|
+
transform: scale(0.94) translateY(var(--sv-space-3));
|
|
19
|
+
opacity: 0.6;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.cardSlot {
|
|
24
|
+
position: absolute;
|
|
25
|
+
inset: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Styled directly rather than wrapping the Card component: Card doesn't
|
|
29
|
+
forward a ref, and this element must be the exact node useSwipeStack
|
|
30
|
+
translates/rotates as one unit — a Card wrapping an inner dragged div
|
|
31
|
+
would leave its own border/shadow visually static while only the inner
|
|
32
|
+
div moved. */
|
|
33
|
+
.card {
|
|
34
|
+
position: relative;
|
|
35
|
+
height: 100%;
|
|
36
|
+
width: 100%;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
background: var(--sv-color-surface-raised);
|
|
39
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
40
|
+
border-radius: var(--sv-radius-lg);
|
|
41
|
+
box-shadow: var(--sv-shadow-card);
|
|
42
|
+
touch-action: none; /* always overridden inline with useSwipeStack's computed value */
|
|
43
|
+
user-select: none;
|
|
44
|
+
cursor: grab;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card:active {
|
|
48
|
+
cursor: grabbing;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.stamp {
|
|
52
|
+
position: absolute;
|
|
53
|
+
z-index: 1;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--sv-space-1);
|
|
57
|
+
padding: var(--sv-space-1) var(--sv-space-3);
|
|
58
|
+
border: 2px solid var(--sv-color-text-primary);
|
|
59
|
+
border-radius: var(--sv-radius-md);
|
|
60
|
+
background: var(--sv-color-surface-raised);
|
|
61
|
+
color: var(--sv-color-text-primary);
|
|
62
|
+
font-family: var(--sv-font-family);
|
|
63
|
+
font-size: var(--sv-font-size-sm);
|
|
64
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stampUp {
|
|
69
|
+
top: var(--sv-space-4);
|
|
70
|
+
left: 50%;
|
|
71
|
+
transform: translateX(-50%);
|
|
72
|
+
opacity: var(--sv-stack-progress-up, 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stampDown {
|
|
76
|
+
bottom: var(--sv-space-4);
|
|
77
|
+
left: 50%;
|
|
78
|
+
transform: translateX(-50%);
|
|
79
|
+
opacity: var(--sv-stack-progress-down, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stampLeft {
|
|
83
|
+
top: var(--sv-space-4);
|
|
84
|
+
left: var(--sv-space-4);
|
|
85
|
+
transform: rotate(-8deg);
|
|
86
|
+
opacity: var(--sv-stack-progress-left, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.stampRight {
|
|
90
|
+
top: var(--sv-space-4);
|
|
91
|
+
right: var(--sv-space-4);
|
|
92
|
+
transform: rotate(8deg);
|
|
93
|
+
opacity: var(--sv-stack-progress-right, 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.actions {
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
gap: var(--sv-space-2);
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Children, isValidElement, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
5
|
+
import { Button } from '../Button/Button';
|
|
6
|
+
import { Icon, type IconName } from '../Icon/Icon';
|
|
7
|
+
import { useSwipeStack, SWIPE_STACK_TRANSITION_MS } from '../../hooks/useSwipeStack';
|
|
8
|
+
import type { SwipeDirection } from '../../hooks/useSwipeStack';
|
|
9
|
+
import { SwipeStackCard } from './SwipeStackCard';
|
|
10
|
+
import styles from './SwipeStack.module.css';
|
|
11
|
+
|
|
12
|
+
export interface SwipeDirectionMeta {
|
|
13
|
+
/** Visible text on the fallback button and the in-drag stamp. */
|
|
14
|
+
label: string;
|
|
15
|
+
icon?: IconName;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SwipeStackProps {
|
|
19
|
+
/** Required — labels the swipeable region for assistive tech. No generic
|
|
20
|
+
* fallback, since one would be wrong for every actual consumer. */
|
|
21
|
+
'aria-label': string;
|
|
22
|
+
/** Which of the 4 directions are live, and what each is called. A
|
|
23
|
+
* direction absent here cannot be dragged toward at all (useSwipeStack
|
|
24
|
+
* treats it as a wall) and gets no fallback button or stamp. SwipeStack
|
|
25
|
+
* itself has no opinion on what a direction *means* — that vocabulary
|
|
26
|
+
* belongs entirely to the caller. */
|
|
27
|
+
directions: Partial<Record<SwipeDirection, SwipeDirectionMeta>>;
|
|
28
|
+
/** Fires once a drag (or a fallback button) commits to a direction. Every
|
|
29
|
+
* configured direction behaves the same way: the card leaves for good —
|
|
30
|
+
* there is no reversible "go back" state. SwipeStack has no opinion on
|
|
31
|
+
* where card data lives; the caller is responsible for eventually no
|
|
32
|
+
* longer including this cardId in children (e.g. once a mutation lands). */
|
|
33
|
+
onSwipe: (direction: SwipeDirection, cardId: string) => void;
|
|
34
|
+
/** Effective drag distance (px) required to commit. Passed straight
|
|
35
|
+
* through to useSwipeStack. */
|
|
36
|
+
threshold?: number;
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Must be SwipeStackCard elements (nullish/boolean children are safely
|
|
39
|
+
* skipped for conditional cards). A dev-mode warning is logged for any
|
|
40
|
+
* other child type — see resolveCards below. */
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ResolvedCard {
|
|
45
|
+
key: string;
|
|
46
|
+
element: ReactElement;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DIRECTION_KEYS: readonly SwipeDirection[] = ['up', 'down', 'left', 'right'];
|
|
50
|
+
|
|
51
|
+
const STAMP_CLASS: Record<SwipeDirection, string> = {
|
|
52
|
+
up: styles.stampUp as string,
|
|
53
|
+
down: styles.stampDown as string,
|
|
54
|
+
left: styles.stampLeft as string,
|
|
55
|
+
right: styles.stampRight as string,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function describeChild(child: unknown): string {
|
|
59
|
+
if (isValidElement(child)) {
|
|
60
|
+
const type = child.type;
|
|
61
|
+
if (typeof type === 'string') return `<${type}>`;
|
|
62
|
+
if (typeof type === 'function') {
|
|
63
|
+
const named = type as { displayName?: string; name?: string };
|
|
64
|
+
return `<${named.displayName ?? named.name ?? 'Component'}>`;
|
|
65
|
+
}
|
|
66
|
+
return '<unknown>';
|
|
67
|
+
}
|
|
68
|
+
if (typeof child === 'string') return `string "${child}"`;
|
|
69
|
+
return String(child);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveCards(children: ReactNode): ResolvedCard[] {
|
|
73
|
+
const cards: ResolvedCard[] = [];
|
|
74
|
+
Children.forEach(children, (child, index) => {
|
|
75
|
+
if (child === null || child === undefined || child === false || child === true) return;
|
|
76
|
+
if (!isValidElement(child) || child.type !== SwipeStackCard) {
|
|
77
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
78
|
+
console.error(
|
|
79
|
+
'[SwipeStack] Every child must be a <SwipeStackCard>. ' +
|
|
80
|
+
`Found ${describeChild(child)} at position ${index}.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const cardProps = child.props as { cardId: string };
|
|
86
|
+
cards.push({ key: cardProps.cardId, element: child });
|
|
87
|
+
});
|
|
88
|
+
return cards;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* SwipeStack — a compound component for triaging one card at a time by
|
|
93
|
+
* dragging it left/right/up/down (wrapping useSwipeStack), plus an
|
|
94
|
+
* always-visible non-gesture fallback: a button per configured direction,
|
|
95
|
+
* wired to the identical commit path and exit animation as a real drag.
|
|
96
|
+
*
|
|
97
|
+
* Every direction is independently optional and behaves the same way once
|
|
98
|
+
* triggered — the card leaves for good, there is no reversible "go back".
|
|
99
|
+
* SwipeStack owns rendering, drag physics, and the stamp/fallback UI only;
|
|
100
|
+
* it has no opinion on where card data lives or what a direction means —
|
|
101
|
+
* see SwipeStackProps.onSwipe.
|
|
102
|
+
*/
|
|
103
|
+
export function SwipeStack({
|
|
104
|
+
'aria-label': ariaLabel,
|
|
105
|
+
directions,
|
|
106
|
+
onSwipe,
|
|
107
|
+
threshold,
|
|
108
|
+
className,
|
|
109
|
+
children,
|
|
110
|
+
}: SwipeStackProps) {
|
|
111
|
+
const cards = resolveCards(children);
|
|
112
|
+
const [dismissed, setDismissed] = useState<ReadonlySet<string>>(() => new Set());
|
|
113
|
+
const [exiting, setExiting] = useState<ResolvedCard | null>(null);
|
|
114
|
+
const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
115
|
+
|
|
116
|
+
useEffect(
|
|
117
|
+
() => () => {
|
|
118
|
+
if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
|
|
119
|
+
},
|
|
120
|
+
[],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const available = cards.filter((c) => !dismissed.has(c.key));
|
|
124
|
+
const nextUp = available[0];
|
|
125
|
+
const current = exiting ?? nextUp ?? null;
|
|
126
|
+
const peek = exiting ? nextUp : available[1];
|
|
127
|
+
|
|
128
|
+
const liveDirections = DIRECTION_KEYS.filter((d) => directions[d]);
|
|
129
|
+
|
|
130
|
+
const { cardRef, wrapRef, handlers, touchAction, triggerCommit } = useSwipeStack({
|
|
131
|
+
directions: liveDirections,
|
|
132
|
+
threshold,
|
|
133
|
+
disabled: !!exiting || !nextUp,
|
|
134
|
+
onCommit(direction) {
|
|
135
|
+
const committed = current;
|
|
136
|
+
if (!committed) return;
|
|
137
|
+
setDismissed((prev) => new Set(prev).add(committed.key));
|
|
138
|
+
setExiting(committed);
|
|
139
|
+
if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
|
|
140
|
+
exitTimerRef.current = setTimeout(() => {
|
|
141
|
+
setExiting((prev) => (prev?.key === committed.key ? null : prev));
|
|
142
|
+
}, SWIPE_STACK_TRANSITION_MS);
|
|
143
|
+
onSwipe(direction, committed.key);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={[styles.wrap, className].filter(Boolean).join(' ')}>
|
|
149
|
+
<div className={styles.stack} role="region" aria-label={ariaLabel}>
|
|
150
|
+
{peek && (
|
|
151
|
+
<div className={styles.peek} key={peek.key} aria-hidden="true">
|
|
152
|
+
{peek.element}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
{current && (
|
|
156
|
+
<div className={styles.cardSlot} key={current.key} ref={wrapRef}>
|
|
157
|
+
{liveDirections.map((d) => {
|
|
158
|
+
const meta = directions[d];
|
|
159
|
+
if (!meta) return null;
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
key={d}
|
|
163
|
+
className={[styles.stamp, STAMP_CLASS[d]].join(' ')}
|
|
164
|
+
aria-hidden="true"
|
|
165
|
+
>
|
|
166
|
+
{meta.icon && <Icon name={meta.icon} size="sm" aria-hidden />}
|
|
167
|
+
<span>{meta.label}</span>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
<div className={styles.card} ref={cardRef} style={{ touchAction }} {...handlers}>
|
|
172
|
+
{current.element}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
{liveDirections.length > 0 && (
|
|
178
|
+
<div className={styles.actions} role="group" aria-label={`${ariaLabel} actions`}>
|
|
179
|
+
{liveDirections.map((d) => {
|
|
180
|
+
const meta = directions[d];
|
|
181
|
+
if (!meta) return null;
|
|
182
|
+
return (
|
|
183
|
+
<Button
|
|
184
|
+
key={d}
|
|
185
|
+
variant="secondary"
|
|
186
|
+
size="sm"
|
|
187
|
+
disabled={!current}
|
|
188
|
+
onClick={() => triggerCommit(d)}
|
|
189
|
+
>
|
|
190
|
+
{meta.icon && <Icon name={meta.icon} size="sm" aria-hidden />}
|
|
191
|
+
{meta.label}
|
|
192
|
+
</Button>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SwipeStackCardProps {
|
|
4
|
+
/** Stable identity for this card, independent of its position among
|
|
5
|
+
* siblings — required. This is what SwipeStack reads (via child.props,
|
|
6
|
+
* not React's own `key`, which a component can't see on itself) to know
|
|
7
|
+
* which card is on top and what id to report through onSwipe. Reuse
|
|
8
|
+
* whatever id the caller already keys their `.map()` on. */
|
|
9
|
+
cardId: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SwipeStackCard — a single card of a SwipeStack. Unlike
|
|
15
|
+
* SwipableMobileCarouselSlide, this has no mount-window logic of its own:
|
|
16
|
+
* SwipeStack only ever renders the top two cards' elements at all (there is
|
|
17
|
+
* no scroll-position reason to keep every sibling in the DOM the way the
|
|
18
|
+
* carousel's scroll-snap track does), so there is nothing here to gate.
|
|
19
|
+
*/
|
|
20
|
+
export function SwipeStackCard({ children }: SwipeStackCardProps) {
|
|
21
|
+
return <>{children}</>;
|
|
22
|
+
}
|