@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import type { RefObject } from 'react';
|
|
5
5
|
|
|
6
6
|
export interface UseSnapCarouselOptions {
|
|
@@ -14,6 +14,14 @@ export interface UseSnapCarouselOptions {
|
|
|
14
14
|
* extracted from. Debounced rather than keyed to the `scrollend` event,
|
|
15
15
|
* which pre-17.4 iOS Safari/WKWebView doesn't support. */
|
|
16
16
|
debounceMs?: number;
|
|
17
|
+
/** The index to seed `liveIndex` with before any scroll event has fired —
|
|
18
|
+
* must match whatever the caller initially scrolls/renders to (typically
|
|
19
|
+
* the same value used for `SwipableMobileCarousel`'s `activeIndex`).
|
|
20
|
+
* Without this, `liveIndex` would start at 0 regardless of where the
|
|
21
|
+
* carousel actually opens (e.g. a deep link straight to slide 8),
|
|
22
|
+
* briefly unmounting every slide except the first few until the first
|
|
23
|
+
* scroll event arrives to correct it. */
|
|
24
|
+
initialIndex?: number;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface UseSnapCarouselResult {
|
|
@@ -21,6 +29,11 @@ export interface UseSnapCarouselResult {
|
|
|
21
29
|
scrollRef: RefObject<HTMLDivElement | null>;
|
|
22
30
|
/** Imperatively scrolls the container to a slide index. */
|
|
23
31
|
scrollToIndex: (index: number, behavior?: ScrollBehavior) => void;
|
|
32
|
+
/** The slide index nearest the container's *current* scroll position,
|
|
33
|
+
* updated synchronously on every `scroll` event — not debounced, and not
|
|
34
|
+
* the same thing as the last-reported `onSettle` index. See its own
|
|
35
|
+
* assignment below for why a consumer needs both. */
|
|
36
|
+
liveIndex: number;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
@@ -35,21 +48,128 @@ export function useSnapCarousel({
|
|
|
35
48
|
itemCount,
|
|
36
49
|
onSettle,
|
|
37
50
|
debounceMs = 120,
|
|
51
|
+
initialIndex = 0,
|
|
38
52
|
}: UseSnapCarouselOptions): UseSnapCarouselResult {
|
|
39
53
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
40
54
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
55
|
const lastIndexRef = useRef<number | null>(null);
|
|
56
|
+
// Cleanup for scrollToIndex's scroll-snap-suspension workaround (see that
|
|
57
|
+
// function's own comment) — lets an overlapping call cancel a still-
|
|
58
|
+
// pending restore from an earlier one instead of two racing to re-enable
|
|
59
|
+
// snap at the wrong time.
|
|
60
|
+
const restoreSnapRef = useRef<(() => void) | null>(null);
|
|
61
|
+
// Real-time (synchronous, not debounced) read of "which slide is the
|
|
62
|
+
// container's scroll position nearest right now." A fast flick can carry
|
|
63
|
+
// native scroll-snap momentum straight past an intermediate slide to one
|
|
64
|
+
// two or more away in a single continuous gesture — the settle-detection
|
|
65
|
+
// above deliberately still waits out the full debounce window before
|
|
66
|
+
// reporting that via onSettle (so it can tell a real settle from drift,
|
|
67
|
+
// see chainTrustedRef's own comment). But `SwipableMobileCarousel`'s mount
|
|
68
|
+
// window is keyed off the *settled* index, so during that debounce window
|
|
69
|
+
// the slide the container had already scrolled to — visually, right now —
|
|
70
|
+
// was never in the mount window and had never been rendered at all: a
|
|
71
|
+
// real, reproduced-on-video gap where the carousel shows nothing (not even
|
|
72
|
+
// a loading skeleton) for the length of one full debounce cycle after a
|
|
73
|
+
// multi-slide flick. `liveIndex` exists so the mount window can track
|
|
74
|
+
// wherever the container currently visually is, independent of whether
|
|
75
|
+
// that position has been confirmed as a real settle yet.
|
|
76
|
+
const [liveIndex, setLiveIndex] = useState(initialIndex);
|
|
42
77
|
// Kept in a ref so the scroll listener always calls the latest callback
|
|
43
78
|
// without needing to be torn down and re-attached on every render.
|
|
44
79
|
const onSettleRef = useRef(onSettle);
|
|
45
80
|
onSettleRef.current = onSettle;
|
|
81
|
+
// Timestamp (Date.now()) of the most recent real input (touch, wheel/
|
|
82
|
+
// trackpad, or a mouse drag) — see the doc comment above `noteGesture`
|
|
83
|
+
// below for why settle-detection is gated on this instead of firing for
|
|
84
|
+
// any `scroll` event.
|
|
85
|
+
const lastGestureAtRef = useRef(0);
|
|
86
|
+
// Whether the *current* run of scroll events (since the last time the
|
|
87
|
+
// container went quiet for debounceMs) was kicked off by real input. Set
|
|
88
|
+
// at the start of each run by checking lastGestureAtRef, then held for the
|
|
89
|
+
// whole run regardless of how long it continues — see `handleScroll`'s own
|
|
90
|
+
// comment for why this can't just be "was there a gesture event within the
|
|
91
|
+
// last debounceMs" re-checked at settle time: native momentum/snap-
|
|
92
|
+
// correction after a real swipe can keep producing scroll events for
|
|
93
|
+
// longer than any fixed slack window without a single further touch event,
|
|
94
|
+
// and a first version of this fix that compared elapsed time against a
|
|
95
|
+
// fixed `debounceMs * 2` slack dropped exactly those settles — silently
|
|
96
|
+
// leaving activeIndex stuck on the old slide while the container had
|
|
97
|
+
// already visually scrolled to the new one, which is worse than the bug
|
|
98
|
+
// being fixed (the mount window then unmounts whatever's actually on
|
|
99
|
+
// screen, since it's keyed off the now-stale index — a blank, un-
|
|
100
|
+
// recoverable-without-reload carousel). `noteGesture` can also upgrade
|
|
101
|
+
// trust mid-run, for a real gesture that starts while an untrusted run's
|
|
102
|
+
// debounce timer is still pending.
|
|
103
|
+
const chainTrustedRef = useRef(false);
|
|
46
104
|
|
|
47
105
|
useEffect(() => {
|
|
48
106
|
const el = scrollRef.current;
|
|
49
107
|
if (!el) return;
|
|
108
|
+
// A `scroll-snap-type: mandatory` container has no legitimate reason to
|
|
109
|
+
// move on its own — every real position change is either (a) driven by
|
|
110
|
+
// the finger/wheel/mouse still in contact, (b) the browser's own
|
|
111
|
+
// momentum/snap-correction settling immediately after release, or (c)
|
|
112
|
+
// our own scrollToIndex() call, which already updates lastIndexRef
|
|
113
|
+
// synchronously so settle-detection for it is a no-op regardless. A
|
|
114
|
+
// `scroll` event that arrives with none of the above recently active is
|
|
115
|
+
// drift, not a settle — most commonly a slide's own content changing
|
|
116
|
+
// size while it's still loading, which can nudge scrollLeft by a
|
|
117
|
+
// fraction of a slide width. Previously any such event that happened to
|
|
118
|
+
// round to a different index than the currently active one was treated
|
|
119
|
+
// as a genuine settle, silently "auto-swiping" the carousel back to
|
|
120
|
+
// whatever slide that rounding landed on — reproduced live via a user
|
|
121
|
+
// screen recording (swiping forward to a still-loading slide, then the
|
|
122
|
+
// carousel snapping back to the previous one ~150ms later with no
|
|
123
|
+
// further touch input) and confirmed via a unit test showing a second,
|
|
124
|
+
// unrelated `scroll` event alone was enough to trigger a second,
|
|
125
|
+
// contradicting `onSettle` call.
|
|
126
|
+
function noteGesture() {
|
|
127
|
+
lastGestureAtRef.current = Date.now();
|
|
128
|
+
// Upgrades the currently in-flight run too, not just the next one —
|
|
129
|
+
// covers a real gesture starting while an untrusted run's debounce
|
|
130
|
+
// timer (started by an earlier, unrelated drift scroll event) is
|
|
131
|
+
// still pending.
|
|
132
|
+
chainTrustedRef.current = true;
|
|
133
|
+
}
|
|
134
|
+
function handlePointerMove(event: PointerEvent) {
|
|
135
|
+
// Ignore hover — only a held button (mouse drag) counts as input.
|
|
136
|
+
if (event.buttons) noteGesture();
|
|
137
|
+
}
|
|
138
|
+
function updateLiveIndexNow() {
|
|
139
|
+
const current = scrollRef.current;
|
|
140
|
+
if (!current) return;
|
|
141
|
+
const width = current.clientWidth;
|
|
142
|
+
if (!width) return;
|
|
143
|
+
const idx = Math.max(0, Math.min(itemCount - 1, Math.round(current.scrollLeft / width)));
|
|
144
|
+
// Bails out (same reference, no re-render) when the index hasn't
|
|
145
|
+
// actually changed — a scroll container fires many `scroll` events
|
|
146
|
+
// while passing through the same slide's territory, not just at its
|
|
147
|
+
// boundaries. Synchronous rather than rAF-throttled: jsdom (this
|
|
148
|
+
// hook's own test environment) doesn't reliably run
|
|
149
|
+
// requestAnimationFrame callbacks under fake timers, and browsers
|
|
150
|
+
// already coalesce native scroll events to roughly once per frame on
|
|
151
|
+
// their own, so a second throttle here bought nothing but test
|
|
152
|
+
// fragility.
|
|
153
|
+
setLiveIndex((prev) => (prev === idx ? prev : idx));
|
|
154
|
+
}
|
|
50
155
|
function handleScroll() {
|
|
51
|
-
|
|
156
|
+
updateLiveIndexNow();
|
|
157
|
+
if (timerRef.current) {
|
|
158
|
+
clearTimeout(timerRef.current);
|
|
159
|
+
} else {
|
|
160
|
+
// No debounce timer pending means the container had been fully
|
|
161
|
+
// quiet — this scroll event starts a new run. Trust it only if real
|
|
162
|
+
// input happened recently; once granted, trust holds for the whole
|
|
163
|
+
// run (see chainTrustedRef's own doc comment above) no matter how
|
|
164
|
+
// long momentum/snap-correction keeps producing scroll events after
|
|
165
|
+
// this point with no further input.
|
|
166
|
+
chainTrustedRef.current = Date.now() - lastGestureAtRef.current <= debounceMs;
|
|
167
|
+
}
|
|
52
168
|
timerRef.current = setTimeout(() => {
|
|
169
|
+
timerRef.current = null;
|
|
170
|
+
const trusted = chainTrustedRef.current;
|
|
171
|
+
chainTrustedRef.current = false;
|
|
172
|
+
if (!trusted) return;
|
|
53
173
|
const current = scrollRef.current;
|
|
54
174
|
if (!current) return;
|
|
55
175
|
const width = current.clientWidth;
|
|
@@ -62,18 +182,103 @@ export function useSnapCarousel({
|
|
|
62
182
|
}, debounceMs);
|
|
63
183
|
}
|
|
64
184
|
el.addEventListener('scroll', handleScroll, { passive: true });
|
|
185
|
+
el.addEventListener('touchstart', noteGesture, { passive: true });
|
|
186
|
+
el.addEventListener('touchmove', noteGesture, { passive: true });
|
|
187
|
+
el.addEventListener('wheel', noteGesture, { passive: true });
|
|
188
|
+
el.addEventListener('pointerdown', noteGesture);
|
|
189
|
+
el.addEventListener('pointermove', handlePointerMove);
|
|
65
190
|
return () => {
|
|
66
191
|
el.removeEventListener('scroll', handleScroll);
|
|
192
|
+
el.removeEventListener('touchstart', noteGesture);
|
|
193
|
+
el.removeEventListener('touchmove', noteGesture);
|
|
194
|
+
el.removeEventListener('wheel', noteGesture);
|
|
195
|
+
el.removeEventListener('pointerdown', noteGesture);
|
|
196
|
+
el.removeEventListener('pointermove', handlePointerMove);
|
|
67
197
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
68
198
|
};
|
|
69
199
|
}, [itemCount, debounceMs]);
|
|
70
200
|
|
|
71
|
-
|
|
201
|
+
// Memoized so its reference stays stable across renders — SwipableMobile
|
|
202
|
+
// Carousel depends on it in a useEffect deps array alongside
|
|
203
|
+
// clampedActiveIndex, and an unmemoized function recreated on every
|
|
204
|
+
// render (e.g. every time liveIndex itself changes below) would make that
|
|
205
|
+
// effect re-fire on every scroll, calling scrollToIndex again and — since
|
|
206
|
+
// it also resets liveIndex to whatever index it was told to scroll to —
|
|
207
|
+
// immediately stomping the very liveIndex update that triggered the
|
|
208
|
+
// re-render in the first place. Caught by this hook's own test suite
|
|
209
|
+
// (a live-index test that failed until this was memoized), not just
|
|
210
|
+
// reasoned about — worth keeping as a regression guard, not just a
|
|
211
|
+
// performance nicety.
|
|
212
|
+
const scrollToIndex = useCallback((index: number, behavior: ScrollBehavior = 'smooth') => {
|
|
72
213
|
const el = scrollRef.current;
|
|
73
214
|
if (!el) return;
|
|
74
215
|
lastIndexRef.current = index;
|
|
75
|
-
|
|
76
|
-
|
|
216
|
+
// Belt-and-suspenders: a real scrollTo() also fires its own 'scroll'
|
|
217
|
+
// event, which updates liveIndex synchronously too — but jsdom's mocked
|
|
218
|
+
// scrollTo (this hook's own test environment) doesn't, and the initial
|
|
219
|
+
// mount scroll / reorder-jump fix both need the target slide mounted
|
|
220
|
+
// immediately regardless.
|
|
221
|
+
setLiveIndex(index);
|
|
222
|
+
|
|
223
|
+
// Cancel any still-pending restore from an earlier smooth scroll before
|
|
224
|
+
// starting this one, so they don't race to re-enable snap at the wrong
|
|
225
|
+
// time (e.g. a dot-jump fired while a previous jump's restore timer is
|
|
226
|
+
// still pending).
|
|
227
|
+
if (restoreSnapRef.current) {
|
|
228
|
+
restoreSnapRef.current();
|
|
229
|
+
restoreSnapRef.current = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const target = index * el.clientWidth;
|
|
233
|
+
|
|
234
|
+
if (behavior === 'smooth') {
|
|
235
|
+
// A programmatic `scrollTo({ behavior: 'smooth' })` on this element
|
|
236
|
+
// can silently do nothing at all — not a jank/timing issue, the
|
|
237
|
+
// container just never moves, with no error and no scroll events.
|
|
238
|
+
// Reproduced live via the design system's own footer/dot-jump
|
|
239
|
+
// navigation in sovereign-tasks (routes external activeIndex changes
|
|
240
|
+
// through this exact path): the identical call with
|
|
241
|
+
// `behavior: 'instant'` moved the container immediately, `'smooth'`
|
|
242
|
+
// did nothing — reproduced both with `scroll-snap-type: mandatory` in
|
|
243
|
+
// place *and*, isolating further, on a plain scroll-snap-free element
|
|
244
|
+
// in the same environment, so `scroll-snap-type` alone doesn't fully
|
|
245
|
+
// explain it. Two independent mitigations, since the exact trigger
|
|
246
|
+
// isn't nailed down: (1) suspend snap for the duration of the
|
|
247
|
+
// animated scroll on the chance it *is* a snap/animation conflict in
|
|
248
|
+
// some engines; (2) below, a correctness-net instant scrollTo if the
|
|
249
|
+
// container still isn't at the target once the animation should have
|
|
250
|
+
// finished — cheap and idempotent when the smooth scroll *did* work
|
|
251
|
+
// (already at the target, the check is a no-op), and the difference
|
|
252
|
+
// between a graceful animation and an abrupt correction if it didn't,
|
|
253
|
+
// rather than a carousel that silently desyncs forever with nothing
|
|
254
|
+
// to ever correct it.
|
|
255
|
+
// Narrowed, stable binding for the closures below — TS can't carry
|
|
256
|
+
// the `if (!el) return` narrowing of the outer `el` into a nested
|
|
257
|
+
// function declaration.
|
|
258
|
+
const scrollEl = el;
|
|
259
|
+
const prevSnapType = scrollEl.style.scrollSnapType;
|
|
260
|
+
scrollEl.style.scrollSnapType = 'none';
|
|
261
|
+
let settled = false;
|
|
262
|
+
// Matches typical browser smooth-scroll duration; also the fallback
|
|
263
|
+
// for engines with no `scrollend` (pre-17.4 iOS Safari/WKWebView).
|
|
264
|
+
const timeoutId = setTimeout(finish, 500);
|
|
265
|
+
function finish() {
|
|
266
|
+
if (settled) return;
|
|
267
|
+
settled = true;
|
|
268
|
+
clearTimeout(timeoutId);
|
|
269
|
+
scrollEl.removeEventListener('scrollend', finish);
|
|
270
|
+
scrollEl.style.scrollSnapType = prevSnapType;
|
|
271
|
+
restoreSnapRef.current = null;
|
|
272
|
+
if (Math.abs(scrollEl.scrollLeft - target) > 1) {
|
|
273
|
+
scrollEl.scrollTo({ left: target, behavior: 'instant' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
scrollEl.addEventListener('scrollend', finish, { once: true });
|
|
277
|
+
restoreSnapRef.current = finish;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
el.scrollTo({ left: target, behavior });
|
|
281
|
+
}, []);
|
|
77
282
|
|
|
78
|
-
return { scrollRef, scrollToIndex };
|
|
283
|
+
return { scrollRef, scrollToIndex, liveIndex };
|
|
79
284
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import type { PointerEvent as ReactPointerEvent, RefObject } from 'react';
|
|
5
|
+
|
|
6
|
+
export type SwipeDirection = 'left' | 'right' | 'up' | 'down';
|
|
7
|
+
|
|
8
|
+
/** Minimum effective drag distance (px) before a direction is read at all —
|
|
9
|
+
* keeps a tap or a tiny jitter from registering as a swipe. Matches
|
|
10
|
+
* useSwipeReveal's own deadzone. */
|
|
11
|
+
const DEADZONE_PX = 8;
|
|
12
|
+
|
|
13
|
+
/** Horizontal drag distance (px) that reaches the full tilt cap. */
|
|
14
|
+
const ROTATION_RANGE_PX = 240;
|
|
15
|
+
const ROTATION_MAX_DEG = 12;
|
|
16
|
+
|
|
17
|
+
/** Duration (ms) of both the commit fling-out and the spring-back-under-
|
|
18
|
+
* threshold transition. Exported so SwipeStack can hold a just-committed
|
|
19
|
+
* card in place for exactly this long before advancing to the next one. */
|
|
20
|
+
export const SWIPE_STACK_TRANSITION_MS = 220;
|
|
21
|
+
|
|
22
|
+
const ALL_DIRECTIONS: readonly SwipeDirection[] = ['left', 'right', 'up', 'down'];
|
|
23
|
+
|
|
24
|
+
/** Unit vector per direction — reused for both the fling-out offset and the
|
|
25
|
+
* tilt sign (horizontal directions tilt, vertical ones don't: x is ±1/0). */
|
|
26
|
+
const UNIT_VECTOR: Record<SwipeDirection, readonly [number, number]> = {
|
|
27
|
+
left: [-1, 0],
|
|
28
|
+
right: [1, 0],
|
|
29
|
+
up: [0, -1],
|
|
30
|
+
down: [0, 1],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface UseSwipeStackOptions {
|
|
34
|
+
/** Directions this drag can move toward and resolve to. A direction left
|
|
35
|
+
* out of this list is a wall, not just an unreachable outcome — the card
|
|
36
|
+
* will not visually move that way at all (see clampToLiveDirections). */
|
|
37
|
+
directions: readonly SwipeDirection[];
|
|
38
|
+
/** Effective distance (px) a drag must cross before release commits to
|
|
39
|
+
* that direction instead of springing back. No velocity component — a
|
|
40
|
+
* fast short flick under this distance still springs back. */
|
|
41
|
+
threshold?: number;
|
|
42
|
+
/** Fires once, synchronously, when a drag (or triggerCommit) resolves to
|
|
43
|
+
* a live direction past threshold. This hook has already started the
|
|
44
|
+
* fling-out animation on cardRef's element by the time this is called —
|
|
45
|
+
* the caller only needs to react to the outcome, not drive the motion. */
|
|
46
|
+
onCommit: (direction: SwipeDirection) => void;
|
|
47
|
+
/** Disables both the drag and triggerCommit — e.g. while there is no
|
|
48
|
+
* current card to act on. */
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UseSwipeStackHandlers {
|
|
53
|
+
onPointerDown: (e: ReactPointerEvent) => void;
|
|
54
|
+
onPointerMove: (e: ReactPointerEvent) => void;
|
|
55
|
+
onPointerUp: (e: ReactPointerEvent) => void;
|
|
56
|
+
onPointerCancel: (e: ReactPointerEvent) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface UseSwipeStackResult {
|
|
60
|
+
/** Attach to the card element that should translate/rotate with the drag. */
|
|
61
|
+
cardRef: RefObject<HTMLDivElement | null>;
|
|
62
|
+
/** Attach to the ancestor of both the card and any direction-stamp
|
|
63
|
+
* overlays. The hook writes live drag progress here as CSS custom
|
|
64
|
+
* properties (--sv-stack-progress-left/right/up/down, each 0–1) so
|
|
65
|
+
* stamps — siblings of the card, not the card itself — can react without
|
|
66
|
+
* a re-render per pointermove. */
|
|
67
|
+
wrapRef: RefObject<HTMLDivElement | null>;
|
|
68
|
+
handlers: UseSwipeStackHandlers;
|
|
69
|
+
/** CSS touch-action for the card, derived from which directions are live
|
|
70
|
+
* — an axis with nothing configured on it stays native (pan-x/pan-y) so
|
|
71
|
+
* a page can still scroll through a stack that only uses the other axis;
|
|
72
|
+
* `none` only once both axes are actually needed. */
|
|
73
|
+
touchAction: string;
|
|
74
|
+
/** Programmatically commits a direction — the same fling animation and
|
|
75
|
+
* onCommit call a real drag past threshold would produce. Backs the
|
|
76
|
+
* non-gesture fallback buttons: they are not a separate code path, just
|
|
77
|
+
* a different way to call this. No-ops if the direction isn't live or
|
|
78
|
+
* the hook is disabled. */
|
|
79
|
+
triggerCommit: (direction: SwipeDirection) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function clamp(value: number, min: number, max: number): number {
|
|
83
|
+
return Math.min(max, Math.max(min, value));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Zeroes out movement toward any direction not in `live` — the card
|
|
87
|
+
* behaves as if a wall exists there instead of just being an unreachable
|
|
88
|
+
* commit outcome, so e.g. a left/right-only stack never visually drifts
|
|
89
|
+
* vertically. */
|
|
90
|
+
function clampToLiveDirections(
|
|
91
|
+
dx: number,
|
|
92
|
+
dy: number,
|
|
93
|
+
live: ReadonlySet<SwipeDirection>,
|
|
94
|
+
): [number, number] {
|
|
95
|
+
const x = dx > 0 ? (live.has('right') ? dx : 0) : live.has('left') ? dx : 0;
|
|
96
|
+
const y = dy > 0 ? (live.has('down') ? dy : 0) : live.has('up') ? dy : 0;
|
|
97
|
+
return [x, y];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveDirection(
|
|
101
|
+
dx: number,
|
|
102
|
+
dy: number,
|
|
103
|
+
live: ReadonlySet<SwipeDirection>,
|
|
104
|
+
): SwipeDirection | null {
|
|
105
|
+
const absX = Math.abs(dx);
|
|
106
|
+
const absY = Math.abs(dy);
|
|
107
|
+
if (absX < DEADZONE_PX && absY < DEADZONE_PX) return null;
|
|
108
|
+
const candidate: SwipeDirection =
|
|
109
|
+
absX > absY ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up';
|
|
110
|
+
return live.has(candidate) ? candidate : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* useSwipeStack — hand-rolled 4-directional free drag, extending
|
|
115
|
+
* useSwipeReveal's pattern (raw Pointer Events, a deadzone, transform
|
|
116
|
+
* written straight to a ref, resolved only on release) from one axis with
|
|
117
|
+
* two outcomes to four directions with a tilt. Threshold-only on purpose,
|
|
118
|
+
* matching useSwipeReveal — no velocity tracking, so a fast short flick
|
|
119
|
+
* under `threshold` still springs back.
|
|
120
|
+
*/
|
|
121
|
+
export function useSwipeStack({
|
|
122
|
+
directions,
|
|
123
|
+
threshold = 96,
|
|
124
|
+
onCommit,
|
|
125
|
+
disabled = false,
|
|
126
|
+
}: UseSwipeStackOptions): UseSwipeStackResult {
|
|
127
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
const dragState = useRef<{ startX: number; startY: number } | null>(null);
|
|
130
|
+
const live = new Set(directions);
|
|
131
|
+
|
|
132
|
+
function writeProgress(direction: SwipeDirection | null, progress: number) {
|
|
133
|
+
const wrap = wrapRef.current;
|
|
134
|
+
if (!wrap) return;
|
|
135
|
+
for (const d of ALL_DIRECTIONS) {
|
|
136
|
+
wrap.style.setProperty(`--sv-stack-progress-${d}`, d === direction ? String(progress) : '0');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeTransform(dx: number, dy: number) {
|
|
141
|
+
const card = cardRef.current;
|
|
142
|
+
if (!card) return;
|
|
143
|
+
const rotation = clamp(
|
|
144
|
+
(dx / ROTATION_RANGE_PX) * ROTATION_MAX_DEG,
|
|
145
|
+
-ROTATION_MAX_DEG,
|
|
146
|
+
ROTATION_MAX_DEG,
|
|
147
|
+
);
|
|
148
|
+
card.style.transform = `translate(${dx}px, ${dy}px) rotate(${rotation}deg)`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function springBack() {
|
|
152
|
+
const card = cardRef.current;
|
|
153
|
+
if (!card) return;
|
|
154
|
+
card.style.transition = `transform ${SWIPE_STACK_TRANSITION_MS}ms ease`;
|
|
155
|
+
card.style.transform = '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function flingOut(direction: SwipeDirection) {
|
|
159
|
+
const card = cardRef.current;
|
|
160
|
+
if (!card) return;
|
|
161
|
+
const distance = 1.4 * Math.max(window.innerWidth, window.innerHeight);
|
|
162
|
+
const [ux, uy] = UNIT_VECTOR[direction];
|
|
163
|
+
card.style.transition = `transform ${SWIPE_STACK_TRANSITION_MS}ms ease`;
|
|
164
|
+
card.style.transform = `translate(${ux * distance}px, ${uy * distance}px) rotate(${ux * ROTATION_MAX_DEG}deg)`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onPointerDown(e: ReactPointerEvent) {
|
|
168
|
+
if (disabled) return;
|
|
169
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
170
|
+
dragState.current = { startX: e.clientX, startY: e.clientY };
|
|
171
|
+
if (cardRef.current) cardRef.current.style.transition = '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function onPointerMove(e: ReactPointerEvent) {
|
|
175
|
+
const state = dragState.current;
|
|
176
|
+
if (!state) return;
|
|
177
|
+
const [dx, dy] = clampToLiveDirections(
|
|
178
|
+
e.clientX - state.startX,
|
|
179
|
+
e.clientY - state.startY,
|
|
180
|
+
live,
|
|
181
|
+
);
|
|
182
|
+
writeTransform(dx, dy);
|
|
183
|
+
const direction = resolveDirection(dx, dy, live);
|
|
184
|
+
if (direction) e.preventDefault();
|
|
185
|
+
writeProgress(
|
|
186
|
+
direction,
|
|
187
|
+
direction ? clamp(Math.max(Math.abs(dx), Math.abs(dy)) / threshold, 0, 1) : 0,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function release(e: ReactPointerEvent) {
|
|
192
|
+
const state = dragState.current;
|
|
193
|
+
dragState.current = null;
|
|
194
|
+
if (!state) return;
|
|
195
|
+
const [dx, dy] = clampToLiveDirections(
|
|
196
|
+
e.clientX - state.startX,
|
|
197
|
+
e.clientY - state.startY,
|
|
198
|
+
live,
|
|
199
|
+
);
|
|
200
|
+
const direction = resolveDirection(dx, dy, live);
|
|
201
|
+
writeProgress(null, 0);
|
|
202
|
+
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
|
203
|
+
if (direction && distance >= threshold) {
|
|
204
|
+
flingOut(direction);
|
|
205
|
+
onCommit(direction);
|
|
206
|
+
} else {
|
|
207
|
+
springBack();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function triggerCommit(direction: SwipeDirection) {
|
|
212
|
+
if (disabled || !live.has(direction)) return;
|
|
213
|
+
flingOut(direction);
|
|
214
|
+
onCommit(direction);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const hasHorizontal = live.has('left') || live.has('right');
|
|
218
|
+
const hasVertical = live.has('up') || live.has('down');
|
|
219
|
+
const touchAction =
|
|
220
|
+
hasHorizontal && hasVertical
|
|
221
|
+
? 'none'
|
|
222
|
+
: hasHorizontal
|
|
223
|
+
? 'pan-y'
|
|
224
|
+
: hasVertical
|
|
225
|
+
? 'pan-x'
|
|
226
|
+
: 'auto';
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
cardRef,
|
|
230
|
+
wrapRef,
|
|
231
|
+
handlers: { onPointerDown, onPointerMove, onPointerUp: release, onPointerCancel: release },
|
|
232
|
+
touchAction,
|
|
233
|
+
triggerCommit,
|
|
234
|
+
};
|
|
235
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { LegalLinks } from './components/LegalLinks/LegalLinks';
|
|
2
2
|
export type { LegalLinksProps } from './components/LegalLinks/LegalLinks';
|
|
3
|
+
export { ConsentPrompt } from './components/ConsentPrompt/ConsentPrompt';
|
|
4
|
+
export type { ConsentPromptProps } from './components/ConsentPrompt/ConsentPrompt';
|
|
3
5
|
export { Markdown } from './components/Markdown/Markdown';
|
|
4
6
|
export type { MarkdownProps } from './components/Markdown/Markdown';
|
|
5
7
|
export { Badge } from './components/Badge/Badge';
|
|
@@ -53,16 +55,34 @@ export type {
|
|
|
53
55
|
MenuSeparator,
|
|
54
56
|
MenuEntry,
|
|
55
57
|
} from './components/Menu/Menu';
|
|
58
|
+
export { UserMenu } from './components/UserMenu/UserMenu';
|
|
59
|
+
export type { UserMenuProps, UserMenuItem } from './components/UserMenu/UserMenu';
|
|
60
|
+
export { AppsLauncher } from './components/AppsLauncher/AppsLauncher';
|
|
61
|
+
export type { AppsLauncherProps, AppsLauncherItem } from './components/AppsLauncher/AppsLauncher';
|
|
62
|
+
export { NotificationsPanel } from './components/NotificationsPanel/NotificationsPanel';
|
|
63
|
+
export type {
|
|
64
|
+
NotificationsPanelProps,
|
|
65
|
+
NotificationsPanelItem,
|
|
66
|
+
} from './components/NotificationsPanel/NotificationsPanel';
|
|
56
67
|
export { ToastProvider, useToast } from './components/Toast/Toast';
|
|
57
68
|
export type { ToastItem, ToastContextValue } from './components/Toast/Toast';
|
|
58
69
|
export { Card } from './components/Card/Card';
|
|
59
70
|
export type { CardProps } from './components/Card/Card';
|
|
71
|
+
|
|
72
|
+
export { CardTile, CardTileGrid, NewCardTile } from './components/CardTile/CardTile';
|
|
73
|
+
export type {
|
|
74
|
+
CardTileProps,
|
|
75
|
+
CardTileGridProps,
|
|
76
|
+
NewCardTileProps,
|
|
77
|
+
} from './components/CardTile/CardTile';
|
|
60
78
|
export { FormField } from './components/FormField/FormField';
|
|
61
79
|
export type { FormFieldProps, FormFieldRenderProps } from './components/FormField/FormField';
|
|
62
80
|
export { PageHeader } from './components/PageHeader/PageHeader';
|
|
63
81
|
export type { PageHeaderProps } from './components/PageHeader/PageHeader';
|
|
64
82
|
export { PageContainer } from './components/PageContainer/PageContainer';
|
|
65
83
|
export type { PageContainerProps } from './components/PageContainer/PageContainer';
|
|
84
|
+
export { Header } from './components/Header/Header';
|
|
85
|
+
export type { HeaderProps, HeaderPlugin } from './components/Header/Header';
|
|
66
86
|
export { MobileHeader } from './components/MobileHeader/MobileHeader';
|
|
67
87
|
export type { MobileHeaderProps } from './components/MobileHeader/MobileHeader';
|
|
68
88
|
export { MobileFooter } from './components/MobileFooter/MobileFooter';
|
|
@@ -172,6 +192,10 @@ export { Combobox } from './components/Combobox/Combobox';
|
|
|
172
192
|
export type { ComboboxProps, ComboboxOption } from './components/Combobox/Combobox';
|
|
173
193
|
export { DragHandleRow } from './components/DragHandleRow/DragHandleRow';
|
|
174
194
|
export type { DragHandleRowProps } from './components/DragHandleRow/DragHandleRow';
|
|
195
|
+
export { GripIcon } from './components/GripIcon/GripIcon';
|
|
196
|
+
export type { GripIconProps } from './components/GripIcon/GripIcon';
|
|
197
|
+
export { StepStrip } from './components/StepStrip/StepStrip';
|
|
198
|
+
export type { StepStripProps, StepStripItem } from './components/StepStrip/StepStrip';
|
|
175
199
|
export { FileDropzone } from './components/FileDropzone/FileDropzone';
|
|
176
200
|
export type { FileDropzoneProps } from './components/FileDropzone/FileDropzone';
|
|
177
201
|
export { Calendar } from './components/Calendar/Calendar';
|
|
@@ -185,6 +209,8 @@ export type {
|
|
|
185
209
|
} from './components/SuggestionInput/SuggestionInput';
|
|
186
210
|
export { IconPicker } from './components/IconPicker/IconPicker';
|
|
187
211
|
export type { IconPickerProps } from './components/IconPicker/IconPicker';
|
|
212
|
+
export { ColorPicker } from './components/ColorPicker/ColorPicker';
|
|
213
|
+
export type { ColorPickerProps, ColorPickerSwatch } from './components/ColorPicker/ColorPicker';
|
|
188
214
|
export { QuantityStepper } from './components/QuantityStepper/QuantityStepper';
|
|
189
215
|
export type { QuantityStepperProps } from './components/QuantityStepper/QuantityStepper';
|
|
190
216
|
export { CheckableListRow } from './components/CheckableListRow/CheckableListRow';
|
|
@@ -215,9 +241,12 @@ export {
|
|
|
215
241
|
useSnapCarousel,
|
|
216
242
|
useResponsiveLayout,
|
|
217
243
|
useCarouselRouteSync,
|
|
244
|
+
useReorderSensors,
|
|
245
|
+
shouldHandleDndEvent,
|
|
246
|
+
useSwipeStack,
|
|
218
247
|
} from './hooks';
|
|
219
248
|
export type { OfflineTileState } from './hooks';
|
|
220
|
-
export { MOBILE_BREAKPOINT_PX } from './hooks';
|
|
249
|
+
export { MOBILE_BREAKPOINT_PX, SWIPE_STACK_TRANSITION_MS } from './hooks';
|
|
221
250
|
export type {
|
|
222
251
|
UseLongPressOptions,
|
|
223
252
|
LongPressHandlers,
|
|
@@ -231,6 +260,11 @@ export type {
|
|
|
231
260
|
UseResponsiveLayoutResult,
|
|
232
261
|
UseCarouselRouteSyncOptions,
|
|
233
262
|
UseCarouselRouteSyncResult,
|
|
263
|
+
UseReorderSensorsOptions,
|
|
264
|
+
SwipeDirection,
|
|
265
|
+
UseSwipeStackOptions,
|
|
266
|
+
UseSwipeStackHandlers,
|
|
267
|
+
UseSwipeStackResult,
|
|
234
268
|
} from './hooks';
|
|
235
269
|
export { ResponsiveSurface } from './components/ResponsiveSurface/ResponsiveSurface';
|
|
236
270
|
export type { ResponsiveSurfaceProps } from './components/ResponsiveSurface/ResponsiveSurface';
|
|
@@ -249,3 +283,29 @@ export { SwipableMobileCarouselSlideBody } from './components/SwipableMobileCaro
|
|
|
249
283
|
export type { SwipableMobileCarouselSlideBodyProps } from './components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody';
|
|
250
284
|
export { SwipableMobileCarouselSlideFooter } from './components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter';
|
|
251
285
|
export type { SwipableMobileCarouselSlideFooterProps } from './components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter';
|
|
286
|
+
export { SwipeStack } from './components/SwipeStack/SwipeStack';
|
|
287
|
+
export type { SwipeStackProps, SwipeDirectionMeta } from './components/SwipeStack/SwipeStack';
|
|
288
|
+
export { SwipeStackCard } from './components/SwipeStack/SwipeStackCard';
|
|
289
|
+
export type { SwipeStackCardProps } from './components/SwipeStack/SwipeStackCard';
|
|
290
|
+
export { ThreeColumnLayout } from './components/ThreeColumnLayout/ThreeColumnLayout';
|
|
291
|
+
export type { ThreeColumnLayoutProps } from './components/ThreeColumnLayout/ThreeColumnLayout';
|
|
292
|
+
export { HeaderFooterLayout } from './components/HeaderFooterLayout/HeaderFooterLayout';
|
|
293
|
+
export type { HeaderFooterLayoutProps } from './components/HeaderFooterLayout/HeaderFooterLayout';
|
|
294
|
+
export { RootLayout } from './components/RootLayout/RootLayout';
|
|
295
|
+
export type { RootLayoutProps, RootLayoutVariant } from './components/RootLayout/RootLayout';
|
|
296
|
+
export { PageLayout } from './components/PageLayout/PageLayout';
|
|
297
|
+
export type { PageLayoutProps, PageLayoutPadding } from './components/PageLayout/PageLayout';
|
|
298
|
+
export { NavList } from './components/NavList/NavList';
|
|
299
|
+
export type {
|
|
300
|
+
NavListProps,
|
|
301
|
+
NavListGroup,
|
|
302
|
+
NavListItem,
|
|
303
|
+
NavListLinkProps,
|
|
304
|
+
} from './components/NavList/NavList';
|
|
305
|
+
|
|
306
|
+
// Theme presets (RFC 0094) — the closed, built-in preset data. Consumed
|
|
307
|
+
// directly by runtime/src/instance-style.ts (RFC 0095); Storybook and
|
|
308
|
+
// standalone consumers instead import the generated
|
|
309
|
+
// '@sovereignfs/ui/tokens/theme-presets.css'.
|
|
310
|
+
export { THEME_PRESETS, THEME_PRESET_NAMES } from './tokens/theme-presets';
|
|
311
|
+
export type { ThemePresetName, ThemePresetTokens } from './tokens/theme-presets';
|