bits-lab 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +57 -2
- package/dist/Accordion.d.ts +52 -0
- package/dist/Accordion.js +192 -0
- package/dist/AchievementToast.d.ts +36 -0
- package/dist/AchievementToast.js +162 -0
- package/dist/AgentCard.d.ts +41 -0
- package/dist/AgentCard.js +168 -0
- package/dist/ArcadeButtons.d.ts +23 -0
- package/dist/ArcadeButtons.js +169 -0
- package/dist/AuroraBands.d.ts +19 -0
- package/dist/AuroraBands.js +126 -0
- package/dist/AvatarTooltips.d.ts +32 -0
- package/dist/AvatarTooltips.js +121 -0
- package/dist/BarRace.d.ts +39 -0
- package/dist/BarRace.js +227 -0
- package/dist/BentoGrid.d.ts +50 -0
- package/dist/BentoGrid.js +145 -0
- package/dist/BootSequence.d.ts +48 -0
- package/dist/BootSequence.js +234 -0
- package/dist/BreadcrumbTrail.d.ts +31 -0
- package/dist/BreadcrumbTrail.js +181 -0
- package/dist/CRTPower.d.ts +30 -0
- package/dist/CRTPower.js +142 -0
- package/dist/CardDeck.d.ts +29 -0
- package/dist/CardDeck.js +154 -0
- package/dist/ChangelogTimeline.d.ts +35 -0
- package/dist/ChangelogTimeline.js +137 -0
- package/dist/ChapterProgress.d.ts +23 -0
- package/dist/ChapterProgress.js +211 -0
- package/dist/ChatBubbles.d.ts +42 -0
- package/dist/ChatBubbles.js +198 -0
- package/dist/ChiptuneKit.d.ts +158 -0
- package/dist/ChiptuneKit.js +207 -0
- package/dist/CipherCard.d.ts +22 -0
- package/dist/CipherCard.js +91 -0
- package/dist/CloudSky.d.ts +28 -0
- package/dist/CloudSky.js +222 -0
- package/dist/CodeRain.d.ts +24 -0
- package/dist/CodeRain.js +158 -0
- package/dist/CodeShowcase.d.ts +50 -0
- package/dist/CodeShowcase.js +307 -0
- package/dist/ColorDial.d.ts +42 -0
- package/dist/ColorDial.js +288 -0
- package/dist/CommandPalette.d.ts +59 -0
- package/dist/CommandPalette.js +349 -0
- package/dist/CompareSlider.d.ts +33 -0
- package/dist/CompareSlider.js +152 -0
- package/dist/Confetti.d.ts +61 -0
- package/dist/Confetti.js +176 -0
- package/dist/ConnectBeams.d.ts +39 -0
- package/dist/ConnectBeams.js +132 -0
- package/dist/ContextOrb.d.ts +34 -0
- package/dist/ContextOrb.js +269 -0
- package/dist/ContourLines.d.ts +20 -0
- package/dist/ContourLines.js +159 -0
- package/dist/Coverflow.d.ts +27 -0
- package/dist/Coverflow.js +167 -0
- package/dist/CursorAttractor.d.ts +21 -0
- package/dist/CursorAttractor.js +136 -0
- package/dist/CutReveal.d.ts +21 -0
- package/dist/CutReveal.js +116 -0
- package/dist/DateRange.d.ts +45 -0
- package/dist/DateRange.js +327 -0
- package/dist/DeviceFrame.d.ts +30 -0
- package/dist/DeviceFrame.js +145 -0
- package/dist/DiffReveal.d.ts +36 -0
- package/dist/DiffReveal.js +247 -0
- package/dist/DirectionHover.d.ts +15 -0
- package/dist/DirectionHover.js +90 -0
- package/dist/Discard.d.ts +18 -0
- package/dist/Discard.js +61 -0
- package/dist/DitherField.d.ts +15 -0
- package/dist/DitherField.js +157 -0
- package/dist/DotGlobe.d.ts +48 -0
- package/dist/DotGlobe.js +312 -0
- package/dist/DotMap.d.ts +47 -0
- package/dist/DotMap.js +172 -0
- package/dist/DropZone.d.ts +57 -0
- package/dist/DropZone.js +304 -0
- package/dist/DynamicIsland.d.ts +32 -0
- package/dist/DynamicIsland.js +92 -0
- package/dist/EdgeFX.d.ts +27 -0
- package/dist/EdgeFX.js +98 -0
- package/dist/ElasticString.d.ts +22 -0
- package/dist/ElasticString.js +127 -0
- package/dist/ExpandCard.d.ts +44 -0
- package/dist/ExpandCard.js +182 -0
- package/dist/FeatureTabs.d.ts +34 -0
- package/dist/FeatureTabs.js +169 -0
- package/dist/FeedList.d.ts +37 -0
- package/dist/FeedList.js +163 -0
- package/dist/FlickerGrid.d.ts +29 -0
- package/dist/FlickerGrid.js +156 -0
- package/dist/FocusGrid.d.ts +20 -0
- package/dist/FocusGrid.js +48 -0
- package/dist/FocusPull.d.ts +26 -0
- package/dist/FocusPull.js +106 -0
- package/dist/Footprints.d.ts +24 -0
- package/dist/Footprints.js +126 -0
- package/dist/FunnelFlow.d.ts +34 -0
- package/dist/FunnelFlow.js +267 -0
- package/dist/GameOver.d.ts +46 -0
- package/dist/GameOver.js +191 -0
- package/dist/GaugeMeter.d.ts +38 -0
- package/dist/GaugeMeter.js +195 -0
- package/dist/GlassPanel.d.ts +27 -0
- package/dist/GlassPanel.js +80 -0
- package/dist/GrainFX.d.ts +34 -0
- package/dist/GrainFX.js +122 -0
- package/dist/GravityBox.d.ts +30 -0
- package/dist/GravityBox.js +220 -0
- package/dist/HealthBar.d.ts +33 -0
- package/dist/HealthBar.js +140 -0
- package/dist/Heatmap.d.ts +42 -0
- package/dist/Heatmap.js +232 -0
- package/dist/HexField.d.ts +29 -0
- package/dist/HexField.js +188 -0
- package/dist/HoldToConfirm.d.ts +29 -0
- package/dist/HoldToConfirm.js +163 -0
- package/dist/HorizontalTrack.d.ts +28 -0
- package/dist/HorizontalTrack.js +195 -0
- package/dist/HotbarDock.d.ts +40 -0
- package/dist/HotbarDock.js +148 -0
- package/dist/InkText.d.ts +27 -0
- package/dist/InkText.js +65 -0
- package/dist/InlineEdit.d.ts +30 -0
- package/dist/InlineEdit.js +198 -0
- package/dist/InteractiveGrid.d.ts +25 -0
- package/dist/InteractiveGrid.js +194 -0
- package/dist/InventoryGrid.d.ts +45 -0
- package/dist/InventoryGrid.js +272 -0
- package/dist/JellyBlob.d.ts +30 -0
- package/dist/JellyBlob.js +209 -0
- package/dist/KineticHeadline.d.ts +30 -0
- package/dist/KineticHeadline.js +133 -0
- package/dist/LavaLamp.d.ts +21 -0
- package/dist/LavaLamp.js +133 -0
- package/dist/LetterSwap.d.ts +20 -0
- package/dist/LetterSwap.js +78 -0
- package/dist/LevelMap.d.ts +35 -0
- package/dist/LevelMap.js +189 -0
- package/dist/LightShafts.d.ts +22 -0
- package/dist/LightShafts.js +170 -0
- package/dist/LightboxGallery.d.ts +26 -0
- package/dist/LightboxGallery.js +327 -0
- package/dist/LinkPeek.d.ts +36 -0
- package/dist/LinkPeek.js +194 -0
- package/dist/LogoOrbit.d.ts +37 -0
- package/dist/LogoOrbit.js +117 -0
- package/dist/Marquee.d.ts +33 -0
- package/dist/Marquee.js +148 -0
- package/dist/MarqueeLine.d.ts +33 -0
- package/dist/MarqueeLine.js +167 -0
- package/dist/MasonryWall.d.ts +13 -0
- package/dist/MasonryWall.js +90 -0
- package/dist/MegaMenu.d.ts +32 -0
- package/dist/MegaMenu.js +280 -0
- package/dist/MeteorShower.d.ts +24 -0
- package/dist/MeteorShower.js +155 -0
- package/dist/Minimap.d.ts +21 -0
- package/dist/Minimap.js +236 -0
- package/dist/MobileTabBar.d.ts +38 -0
- package/dist/MobileTabBar.js +148 -0
- package/dist/MorphDialog.d.ts +34 -0
- package/dist/MorphDialog.js +199 -0
- package/dist/MorphPopover.d.ts +33 -0
- package/dist/MorphPopover.js +168 -0
- package/dist/MorphWords.d.ts +18 -0
- package/dist/MorphWords.js +136 -0
- package/dist/OTPInput.d.ts +43 -0
- package/dist/OTPInput.js +174 -0
- package/dist/OverlayKit.d.ts +97 -0
- package/dist/OverlayKit.js +466 -0
- package/dist/OverlayMenu.d.ts +32 -0
- package/dist/OverlayMenu.js +154 -0
- package/dist/ParallaxCollage.d.ts +41 -0
- package/dist/ParallaxCollage.js +143 -0
- package/dist/PasswordMeter.d.ts +44 -0
- package/dist/PasswordMeter.js +165 -0
- package/dist/PegBoard.d.ts +26 -0
- package/dist/PegBoard.js +184 -0
- package/dist/PinCard.d.ts +21 -0
- package/dist/PinCard.js +84 -0
- package/dist/PixelAvatar.d.ts +33 -0
- package/dist/PixelAvatar.js +144 -0
- package/dist/PixelButton.d.ts +20 -0
- package/dist/PixelButton.js +128 -0
- package/dist/PixelExplode.d.ts +31 -0
- package/dist/PixelExplode.js +164 -0
- package/dist/PixelLoader.d.ts +32 -0
- package/dist/PixelLoader.js +142 -0
- package/dist/PixelParallax.d.ts +41 -0
- package/dist/PixelParallax.js +151 -0
- package/dist/PixelSpinners.d.ts +21 -0
- package/dist/PixelSpinners.js +110 -0
- package/dist/PixelType.d.ts +26 -0
- package/dist/PixelType.js +220 -0
- package/dist/PointerKit.d.ts +23 -0
- package/dist/PointerKit.js +222 -0
- package/dist/Polaroids.d.ts +41 -0
- package/dist/Polaroids.js +293 -0
- package/dist/PressStart.d.ts +28 -0
- package/dist/PressStart.js +99 -0
- package/dist/PricingFlip.d.ts +52 -0
- package/dist/PricingFlip.js +190 -0
- package/dist/ProximityType.d.ts +26 -0
- package/dist/ProximityType.js +135 -0
- package/dist/QuestDialog.d.ts +50 -0
- package/dist/QuestDialog.js +204 -0
- package/dist/QuestLog.d.ts +39 -0
- package/dist/QuestLog.js +214 -0
- package/dist/RangeDual.d.ts +34 -0
- package/dist/RangeDual.js +238 -0
- package/dist/RatingPips.d.ts +25 -0
- package/dist/RatingPips.js +129 -0
- package/dist/RetroHorizon.d.ts +27 -0
- package/dist/RetroHorizon.js +169 -0
- package/dist/Reveal.d.ts +30 -0
- package/dist/Reveal.js +92 -0
- package/dist/RingProgress.d.ts +45 -0
- package/dist/RingProgress.js +180 -0
- package/dist/RollingNumber.d.ts +28 -0
- package/dist/RollingNumber.js +108 -0
- package/dist/ScanScope.d.ts +24 -0
- package/dist/ScanScope.js +164 -0
- package/dist/ScoreTicker.d.ts +32 -0
- package/dist/ScoreTicker.js +176 -0
- package/dist/ScreenScroller.d.ts +24 -0
- package/dist/ScreenScroller.js +138 -0
- package/dist/Screensaver.d.ts +31 -0
- package/dist/Screensaver.js +221 -0
- package/dist/ScrollTilt.d.ts +27 -0
- package/dist/ScrollTilt.js +100 -0
- package/dist/ScrollTypography.d.ts +26 -0
- package/dist/ScrollTypography.js +124 -0
- package/dist/ScrollZoom.d.ts +31 -0
- package/dist/ScrollZoom.js +123 -0
- package/dist/ScrubNumber.d.ts +29 -0
- package/dist/ScrubNumber.js +152 -0
- package/dist/ShadowStack.d.ts +28 -0
- package/dist/ShadowStack.js +116 -0
- package/dist/SideRail.d.ts +43 -0
- package/dist/SideRail.js +253 -0
- package/dist/SignalText.d.ts +25 -0
- package/dist/SignalText.js +178 -0
- package/dist/SkeletonShimmer.d.ts +18 -0
- package/dist/SkeletonShimmer.js +111 -0
- package/dist/SketchHighlight.d.ts +27 -0
- package/dist/SketchHighlight.js +180 -0
- package/dist/SlideTabs.d.ts +40 -0
- package/dist/SlideTabs.js +190 -0
- package/dist/SmartNavbar.d.ts +48 -0
- package/dist/SmartNavbar.js +188 -0
- package/dist/SparkChart.d.ts +41 -0
- package/dist/SparkChart.js +188 -0
- package/dist/Spoiler.d.ts +22 -0
- package/dist/Spoiler.js +97 -0
- package/dist/SpotlightTour.d.ts +38 -0
- package/dist/SpotlightTour.js +231 -0
- package/dist/SpringDrawer.d.ts +39 -0
- package/dist/SpringDrawer.js +360 -0
- package/dist/SpriteAnimator.d.ts +41 -0
- package/dist/SpriteAnimator.js +124 -0
- package/dist/StatCard.d.ts +43 -0
- package/dist/StatCard.js +168 -0
- package/dist/StepLoader.d.ts +41 -0
- package/dist/StepLoader.js +191 -0
- package/dist/StepperForm.d.ts +41 -0
- package/dist/StepperForm.js +206 -0
- package/dist/StickerBoard.d.ts +39 -0
- package/dist/StickerBoard.js +209 -0
- package/dist/StickyReveal.d.ts +42 -0
- package/dist/StickyReveal.js +181 -0
- package/dist/StoryViewer.d.ts +36 -0
- package/dist/StoryViewer.js +175 -0
- package/dist/StreamingText.d.ts +29 -0
- package/dist/StreamingText.js +271 -0
- package/dist/SuggestionChips.d.ts +32 -0
- package/dist/SuggestionChips.js +96 -0
- package/dist/SwipeActions.d.ts +30 -0
- package/dist/SwipeActions.js +141 -0
- package/dist/TagInput.d.ts +30 -0
- package/dist/TagInput.js +284 -0
- package/dist/TallyUp.d.ts +38 -0
- package/dist/TallyUp.js +157 -0
- package/dist/TerminalDemo.d.ts +63 -0
- package/dist/TerminalDemo.js +214 -0
- package/dist/TestimonialWall.d.ts +35 -0
- package/dist/TestimonialWall.js +131 -0
- package/dist/TextOnPath.d.ts +36 -0
- package/dist/TextOnPath.js +130 -0
- package/dist/ThinkingShimmer.d.ts +36 -0
- package/dist/ThinkingShimmer.js +132 -0
- package/dist/ToastStack.d.ts +62 -0
- package/dist/ToastStack.js +289 -0
- package/dist/ToggleGroup.d.ts +39 -0
- package/dist/ToggleGroup.js +190 -0
- package/dist/ToggleKit.d.ts +70 -0
- package/dist/ToggleKit.js +229 -0
- package/dist/TracingLine.d.ts +24 -0
- package/dist/TracingLine.js +139 -0
- package/dist/TradingCard.d.ts +30 -0
- package/dist/TradingCard.js +147 -0
- package/dist/VanishInput.d.ts +34 -0
- package/dist/VanishInput.js +190 -0
- package/dist/VideoHero.d.ts +32 -0
- package/dist/VideoHero.js +207 -0
- package/dist/VoiceOrb.d.ts +23 -0
- package/dist/VoiceOrb.js +182 -0
- package/dist/VoxelTiles.d.ts +38 -0
- package/dist/VoxelTiles.js +201 -0
- package/dist/WarpField.d.ts +18 -0
- package/dist/WarpField.js +160 -0
- package/dist/WebcamPixels.d.ts +38 -0
- package/dist/WebcamPixels.js +292 -0
- package/dist/WheelPicker.d.ts +29 -0
- package/dist/WheelPicker.js +199 -0
- package/dist/WobbleCard.d.ts +23 -0
- package/dist/WobbleCard.js +112 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.js +159 -0
- package/package.json +58 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useInsertionEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
const STYLE_ID = "bl-spotlight-tour";
|
|
6
|
+
const CSS = `
|
|
7
|
+
.bl-tour { position: fixed; inset: 0; z-index: 1950; font-family: var(--bl-font); }
|
|
8
|
+
.bl-tour__shade { position: fixed; inset: 0; }
|
|
9
|
+
.bl-tour__hole { position: fixed; top: 0; left: 0; border-radius: var(--bl-radius); box-shadow: 0 0 0 2px var(--bl-accent), 0 0 0 200vmax var(--bl-dim); pointer-events: none; transition: transform 420ms cubic-bezier(.3,1.2,.5,1), width 420ms cubic-bezier(.3,1.2,.5,1), height 420ms cubic-bezier(.3,1.2,.5,1); animation: bl-tour-in 260ms ease both; }
|
|
10
|
+
.bl-tour__hole[data-none] { width: 0 !important; height: 0 !important; box-shadow: 0 0 0 200vmax var(--bl-dim); }
|
|
11
|
+
.bl-tour__bubble { position: fixed; top: 0; left: 0; width: min(var(--bl-w), calc(100vw - 24px)); box-sizing: border-box; padding: 18px; border-radius: 14px; background: var(--bl-bg); color: var(--bl-ink); box-shadow: 0 0 0 1px var(--bl-line), 0 24px 60px -24px rgba(0,0,0,0.9); outline: none; transition: transform 420ms cubic-bezier(.3,1.2,.5,1); }
|
|
12
|
+
.bl-tour__card { animation: bl-tour-card 360ms cubic-bezier(.3,1.4,.5,1) both; }
|
|
13
|
+
.bl-tour__step { color: var(--bl-accent); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
14
|
+
.bl-tour__title { margin: 6px 0 6px; font-size: 17px; font-weight: 700; line-height: 1.3; }
|
|
15
|
+
.bl-tour__body { margin: 0; color: var(--bl-muted); font-size: 14px; line-height: 1.55; }
|
|
16
|
+
.bl-tour__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
|
|
17
|
+
.bl-tour__dots { display: flex; gap: 6px; }
|
|
18
|
+
.bl-tour__dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--bl-line); transition: width 240ms, background-color 240ms; }
|
|
19
|
+
.bl-tour__dots i[data-on] { width: 18px; border-radius: 4px; background: var(--bl-accent); }
|
|
20
|
+
.bl-tour__btns { display: flex; gap: 8px; }
|
|
21
|
+
.bl-tour__btn { padding: 8px 14px; border: 0; border-radius: 8px; background: transparent; color: var(--bl-muted); font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
22
|
+
.bl-tour__btn:hover { color: var(--bl-ink); }
|
|
23
|
+
.bl-tour__btn[data-primary] { background: var(--bl-accent); color: var(--bl-on-accent); }
|
|
24
|
+
.bl-tour__btn[data-primary]:hover { filter: brightness(1.12); }
|
|
25
|
+
.bl-tour__btn:focus-visible { outline: 2px solid var(--bl-accent); outline-offset: 2px; }
|
|
26
|
+
.bl-tour__close { position: absolute; top: 10px; right: 10px; display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--bl-muted); cursor: pointer; }
|
|
27
|
+
.bl-tour__close:hover { color: var(--bl-ink); background: var(--bl-line); }
|
|
28
|
+
.bl-tour__close:focus-visible { outline: 2px solid var(--bl-accent); }
|
|
29
|
+
@keyframes bl-tour-in { from { opacity: 0; } }
|
|
30
|
+
@keyframes bl-tour-card { from { opacity: 0; transform: translateY(8px) scale(0.96); } }
|
|
31
|
+
@media (prefers-reduced-motion: reduce) { .bl-tour__hole, .bl-tour__bubble { transition: none; } .bl-tour__card { animation: none; } }
|
|
32
|
+
`;
|
|
33
|
+
function useStyles() {
|
|
34
|
+
useInsertionEffect(() => {
|
|
35
|
+
if (typeof document === "undefined") return;
|
|
36
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
37
|
+
style.textContent = CSS;
|
|
38
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
39
|
+
}, []);
|
|
40
|
+
}
|
|
41
|
+
const resolve = (target) => typeof target === "string" ? document.querySelector(target) : target?.current ?? null;
|
|
42
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
43
|
+
function SpotlightTour({
|
|
44
|
+
steps = [],
|
|
45
|
+
open = false,
|
|
46
|
+
onClose,
|
|
47
|
+
onStepChange,
|
|
48
|
+
startAt = 0,
|
|
49
|
+
padding = 8,
|
|
50
|
+
radius = 12,
|
|
51
|
+
gap = 14,
|
|
52
|
+
width = 320,
|
|
53
|
+
closeOnBackdrop = false,
|
|
54
|
+
backLabel = "Back",
|
|
55
|
+
nextLabel = "Next",
|
|
56
|
+
doneLabel = "Done",
|
|
57
|
+
skipLabel = "Skip tour",
|
|
58
|
+
dimColor = "rgba(8,8,16,0.68)",
|
|
59
|
+
backgroundColor = "#1a1c2c",
|
|
60
|
+
textColor = "#f4f4f4",
|
|
61
|
+
mutedColor = "#94b0c2",
|
|
62
|
+
lineColor = "#333c57",
|
|
63
|
+
accentColor = "#73eff7",
|
|
64
|
+
onAccentColor = "#0b0b14",
|
|
65
|
+
fontFamily = "ui-sans-serif, system-ui, sans-serif"
|
|
66
|
+
}) {
|
|
67
|
+
useStyles();
|
|
68
|
+
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
69
|
+
const [index, setIndex] = useState(startAt);
|
|
70
|
+
const [hole, setHole] = useState(null);
|
|
71
|
+
const [bubblePos, setBubblePos] = useState({ x: 0, y: 0 });
|
|
72
|
+
const bubbleRef = useRef(null);
|
|
73
|
+
const returnFocus = useRef(null);
|
|
74
|
+
const step = steps[index];
|
|
75
|
+
const last = index === steps.length - 1;
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (open) {
|
|
78
|
+
returnFocus.current = document.activeElement;
|
|
79
|
+
setIndex(startAt);
|
|
80
|
+
}
|
|
81
|
+
}, [open, startAt]);
|
|
82
|
+
const finish = (completed) => {
|
|
83
|
+
onClose?.(completed);
|
|
84
|
+
const back = returnFocus.current;
|
|
85
|
+
if (back instanceof HTMLElement) setTimeout(() => back.focus({ preventScroll: true }), 0);
|
|
86
|
+
};
|
|
87
|
+
const go = (next) => {
|
|
88
|
+
if (next < 0 || next >= steps.length) return;
|
|
89
|
+
setIndex(next);
|
|
90
|
+
onStepChange?.(next);
|
|
91
|
+
};
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
if (!open || !step) return void 0;
|
|
94
|
+
const el = resolve(step.target);
|
|
95
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
96
|
+
el?.scrollIntoView?.({ block: "center", inline: "nearest", behavior: reduced ? "auto" : "smooth" });
|
|
97
|
+
let frame = 0;
|
|
98
|
+
const place = () => {
|
|
99
|
+
frame = 0;
|
|
100
|
+
const bubble = bubbleRef.current;
|
|
101
|
+
const bw = bubble?.offsetWidth ?? width;
|
|
102
|
+
const bh = bubble?.offsetHeight ?? 160;
|
|
103
|
+
const vw = window.innerWidth;
|
|
104
|
+
const vh = window.innerHeight;
|
|
105
|
+
if (!el) {
|
|
106
|
+
setHole(null);
|
|
107
|
+
setBubblePos({ x: (vw - bw) / 2, y: (vh - bh) / 2 });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const r = el.getBoundingClientRect();
|
|
111
|
+
const box = { x: r.left - padding, y: r.top - padding, w: r.width + padding * 2, h: r.height + padding * 2 };
|
|
112
|
+
setHole(box);
|
|
113
|
+
const room = { bottom: vh - (box.y + box.h), top: box.y, right: vw - (box.x + box.w), left: box.x };
|
|
114
|
+
const fits = (side2) => side2 === "top" || side2 === "bottom" ? room[side2] >= bh + gap + 8 : room[side2] >= bw + gap + 8;
|
|
115
|
+
const side = step.side && fits(step.side) ? step.side : ["bottom", "top", "right", "left"].find(fits) ?? "bottom";
|
|
116
|
+
let x = side === "right" ? box.x + box.w + gap : side === "left" ? box.x - bw - gap : box.x + box.w / 2 - bw / 2;
|
|
117
|
+
let y = side === "bottom" ? box.y + box.h + gap : side === "top" ? box.y - bh - gap : box.y + box.h / 2 - bh / 2;
|
|
118
|
+
x = Math.max(12, Math.min(vw - bw - 12, x));
|
|
119
|
+
y = Math.max(12, Math.min(vh - bh - 12, y));
|
|
120
|
+
setBubblePos({ x, y });
|
|
121
|
+
};
|
|
122
|
+
const schedule = () => {
|
|
123
|
+
if (!frame) frame = requestAnimationFrame(place);
|
|
124
|
+
};
|
|
125
|
+
place();
|
|
126
|
+
window.addEventListener("scroll", schedule, true);
|
|
127
|
+
window.addEventListener("resize", schedule);
|
|
128
|
+
const resize = new ResizeObserver(schedule);
|
|
129
|
+
if (el) resize.observe(el);
|
|
130
|
+
if (bubbleRef.current) resize.observe(bubbleRef.current);
|
|
131
|
+
return () => {
|
|
132
|
+
cancelAnimationFrame(frame);
|
|
133
|
+
window.removeEventListener("scroll", schedule, true);
|
|
134
|
+
window.removeEventListener("resize", schedule);
|
|
135
|
+
resize.disconnect();
|
|
136
|
+
};
|
|
137
|
+
}, [open, step, padding, gap, width]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!open) return void 0;
|
|
140
|
+
bubbleRef.current?.focus({ preventScroll: true });
|
|
141
|
+
const onKey = (event) => {
|
|
142
|
+
if (event.key === "Escape") {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
finish(false);
|
|
145
|
+
} else if (event.key === "ArrowRight") {
|
|
146
|
+
if (last) finish(true);
|
|
147
|
+
else go(index + 1);
|
|
148
|
+
} else if (event.key === "ArrowLeft") {
|
|
149
|
+
go(index - 1);
|
|
150
|
+
} else if (event.key === "Tab") {
|
|
151
|
+
const items = [...bubbleRef.current?.querySelectorAll(FOCUSABLE) ?? []];
|
|
152
|
+
if (!items.length) return;
|
|
153
|
+
const first = items[0];
|
|
154
|
+
const end = items[items.length - 1];
|
|
155
|
+
if (event.shiftKey && (document.activeElement === first || document.activeElement === bubbleRef.current)) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
end.focus();
|
|
158
|
+
} else if (!event.shiftKey && document.activeElement === end) {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
first.focus();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
document.addEventListener("keydown", onKey);
|
|
165
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
166
|
+
}, [open, index]);
|
|
167
|
+
if (!open || !step) return null;
|
|
168
|
+
const vars = {
|
|
169
|
+
"--bl-dim": dimColor,
|
|
170
|
+
"--bl-radius": `${radius}px`,
|
|
171
|
+
"--bl-w": `${width}px`,
|
|
172
|
+
"--bl-bg": backgroundColor,
|
|
173
|
+
"--bl-ink": textColor,
|
|
174
|
+
"--bl-muted": mutedColor,
|
|
175
|
+
"--bl-line": lineColor,
|
|
176
|
+
"--bl-accent": accentColor,
|
|
177
|
+
"--bl-on-accent": onAccentColor,
|
|
178
|
+
"--bl-font": fontFamily
|
|
179
|
+
};
|
|
180
|
+
return createPortal(
|
|
181
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-tour", style: vars, children: [
|
|
182
|
+
/* @__PURE__ */ jsx("div", { className: "bl-tour__shade", onClick: () => closeOnBackdrop && finish(false) }),
|
|
183
|
+
/* @__PURE__ */ jsx(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
className: "bl-tour__hole",
|
|
187
|
+
"data-none": !hole || void 0,
|
|
188
|
+
style: hole ? { width: hole.w, height: hole.h, transform: `translate(${hole.x}px, ${hole.y}px)` } : { transform: `translate(50vw, 50vh)` },
|
|
189
|
+
"aria-hidden": "true"
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
/* @__PURE__ */ jsxs(
|
|
193
|
+
"div",
|
|
194
|
+
{
|
|
195
|
+
ref: bubbleRef,
|
|
196
|
+
className: "bl-tour__bubble",
|
|
197
|
+
role: "dialog",
|
|
198
|
+
"aria-modal": "true",
|
|
199
|
+
"aria-labelledby": `${uid}-title`,
|
|
200
|
+
"aria-describedby": `${uid}-body`,
|
|
201
|
+
tabIndex: -1,
|
|
202
|
+
style: { transform: `translate(${bubblePos.x}px, ${bubblePos.y}px)` },
|
|
203
|
+
children: [
|
|
204
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-tour__card", children: [
|
|
205
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-tour__step", children: [
|
|
206
|
+
"Step ",
|
|
207
|
+
index + 1,
|
|
208
|
+
" of ",
|
|
209
|
+
steps.length
|
|
210
|
+
] }),
|
|
211
|
+
/* @__PURE__ */ jsx("h2", { id: `${uid}-title`, className: "bl-tour__title", children: step.title }),
|
|
212
|
+
/* @__PURE__ */ jsx("div", { id: `${uid}-body`, className: "bl-tour__body", children: step.body }),
|
|
213
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-tour__foot", children: [
|
|
214
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tour__dots", "aria-hidden": "true", children: steps.map((_, i) => /* @__PURE__ */ jsx("i", { "data-on": i === index || void 0 }, i)) }),
|
|
215
|
+
/* @__PURE__ */ jsxs("span", { className: "bl-tour__btns", children: [
|
|
216
|
+
index > 0 && /* @__PURE__ */ jsx("button", { type: "button", className: "bl-tour__btn", onClick: () => go(index - 1), children: backLabel }),
|
|
217
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "bl-tour__btn", "data-primary": "", onClick: () => last ? finish(true) : go(index + 1), children: last ? doneLabel : nextLabel })
|
|
218
|
+
] })
|
|
219
|
+
] })
|
|
220
|
+
] }, index),
|
|
221
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "bl-tour__close", "aria-label": skipLabel, onClick: () => finish(false), children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 2l8 8M10 2 2 10" }) }) })
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
] }),
|
|
226
|
+
document.body
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
export {
|
|
230
|
+
SpotlightTour as default
|
|
231
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface SpringDrawerProps {
|
|
3
|
+
/** Controlled open state. */
|
|
4
|
+
open: boolean;
|
|
5
|
+
/** Drag-to-close, Esc and the backdrop call this with false. */
|
|
6
|
+
onOpenChange?: (open: boolean) => void;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
/** Heading; also the dialog's accessible name. */
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
/** Wired up as the dialog's accessible description. */
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
side?: 'bottom' | 'right' | 'left';
|
|
13
|
+
/** Bottom only: open heights as fractions of the viewport (or container). */
|
|
14
|
+
snapPoints?: number[];
|
|
15
|
+
/** Index into the sorted snap points to open at. */
|
|
16
|
+
defaultSnap?: number;
|
|
17
|
+
/** Side drawers: width, in px. */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Bottom sheet max width, in px. */
|
|
20
|
+
maxWidth?: number;
|
|
21
|
+
/** Allow closing by drag, Esc and backdrop. */
|
|
22
|
+
dismissible?: boolean;
|
|
23
|
+
/** Render inside the parent element instead of the window. Nested drawers inherit it. */
|
|
24
|
+
contained?: boolean;
|
|
25
|
+
/** Spring stiffness. */
|
|
26
|
+
stiffness?: number;
|
|
27
|
+
/** Damping ratio: below 1 bounces, 1 is critically damped. */
|
|
28
|
+
damping?: number;
|
|
29
|
+
radius?: number;
|
|
30
|
+
backgroundColor?: string;
|
|
31
|
+
textColor?: string;
|
|
32
|
+
mutedColor?: string;
|
|
33
|
+
lineColor?: string;
|
|
34
|
+
accentColor?: string;
|
|
35
|
+
backdropColor?: string;
|
|
36
|
+
fontFamily?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
export default function SpringDrawer({ open, onOpenChange, children, title, description, side, snapPoints, defaultSnap, size, maxWidth, dismissible, contained, stiffness, damping, radius, backgroundColor, textColor, mutedColor, lineColor, accentColor, backdropColor, fontFamily, className }: SpringDrawerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
const STYLE_ID = "bl-spring-drawer";
|
|
6
|
+
const CSS = `
|
|
7
|
+
.bl-drawer__backdrop { position: fixed; inset: 0; background: var(--bl-backdrop); opacity: 0; }
|
|
8
|
+
.bl-drawer__shell { position: fixed; display: flex; flex-direction: column; background: var(--bl-bg); color: var(--bl-ink); font-family: var(--bl-font); box-shadow: 0 0 0 1px var(--bl-line), 0 -20px 60px -20px rgba(0,0,0,0.6); outline: none; touch-action: none; will-change: transform; transition: filter 300ms; }
|
|
9
|
+
.bl-drawer[data-contained] .bl-drawer__backdrop, .bl-drawer[data-contained] .bl-drawer__shell { position: absolute; }
|
|
10
|
+
.bl-drawer[data-side="bottom"] .bl-drawer__shell { left: 0; right: 0; bottom: 0; max-width: var(--bl-max-w); margin: 0 auto; height: var(--bl-size); border-radius: var(--bl-radius) var(--bl-radius) 0 0; }
|
|
11
|
+
.bl-drawer[data-side="right"] .bl-drawer__shell { top: 0; bottom: 0; right: 0; width: var(--bl-size); max-width: 92%; border-radius: var(--bl-radius) 0 0 var(--bl-radius); }
|
|
12
|
+
.bl-drawer[data-side="left"] .bl-drawer__shell { top: 0; bottom: 0; left: 0; width: var(--bl-size); max-width: 92%; border-radius: 0 var(--bl-radius) var(--bl-radius) 0; }
|
|
13
|
+
.bl-drawer__shell[data-nested] { filter: brightness(0.7); }
|
|
14
|
+
.bl-drawer__handle { flex: 0 0 auto; display: grid; place-items: center; height: 28px; margin: 0; padding: 0; border: 0; background: transparent; cursor: grab; }
|
|
15
|
+
.bl-drawer__handle::before { content: ""; width: 44px; height: 5px; border-radius: var(--bl-pill); background: var(--bl-muted); opacity: 0.5; }
|
|
16
|
+
.bl-drawer__handle:focus-visible { outline: 2px solid var(--bl-accent); outline-offset: -4px; }
|
|
17
|
+
.bl-drawer[data-side="right"] .bl-drawer__handle, .bl-drawer[data-side="left"] .bl-drawer__handle { position: absolute; top: 50%; width: 28px; height: 60px; transform: translateY(-50%); }
|
|
18
|
+
.bl-drawer[data-side="right"] .bl-drawer__handle { left: 0; }
|
|
19
|
+
.bl-drawer[data-side="left"] .bl-drawer__handle { right: 0; }
|
|
20
|
+
.bl-drawer[data-side="right"] .bl-drawer__handle::before, .bl-drawer[data-side="left"] .bl-drawer__handle::before { width: 5px; height: 44px; }
|
|
21
|
+
.bl-drawer__header { flex: 0 0 auto; padding: 4px 22px 10px; }
|
|
22
|
+
.bl-drawer[data-side="right"] .bl-drawer__header, .bl-drawer[data-side="left"] .bl-drawer__header { padding-top: 22px; }
|
|
23
|
+
.bl-drawer__title { margin: 0; font-size: 1.15em; font-weight: 700; }
|
|
24
|
+
.bl-drawer__desc { margin: 4px 0 0; color: var(--bl-muted); font-size: 0.92em; }
|
|
25
|
+
.bl-drawer__body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 8px 22px 22px; }
|
|
26
|
+
`;
|
|
27
|
+
function useStyles() {
|
|
28
|
+
useInsertionEffect(() => {
|
|
29
|
+
if (typeof document === "undefined") return;
|
|
30
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
31
|
+
style.textContent = CSS;
|
|
32
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
33
|
+
}, []);
|
|
34
|
+
}
|
|
35
|
+
const DrawerContext = createContext(null);
|
|
36
|
+
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
37
|
+
function SpringDrawer({
|
|
38
|
+
open,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
children,
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
side = "bottom",
|
|
44
|
+
snapPoints = [0.5, 0.92],
|
|
45
|
+
defaultSnap = 0,
|
|
46
|
+
size = 380,
|
|
47
|
+
maxWidth = 720,
|
|
48
|
+
dismissible = true,
|
|
49
|
+
contained = false,
|
|
50
|
+
stiffness = 520,
|
|
51
|
+
damping = 0.82,
|
|
52
|
+
radius = 20,
|
|
53
|
+
backgroundColor = "#1a1c2c",
|
|
54
|
+
textColor = "#f4f4f4",
|
|
55
|
+
mutedColor = "#94b0c2",
|
|
56
|
+
lineColor = "rgba(255,255,255,0.1)",
|
|
57
|
+
accentColor = "#73eff7",
|
|
58
|
+
backdropColor = "rgba(8,8,16,0.6)",
|
|
59
|
+
fontFamily = "ui-sans-serif, system-ui, sans-serif",
|
|
60
|
+
className = ""
|
|
61
|
+
}) {
|
|
62
|
+
useStyles();
|
|
63
|
+
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
64
|
+
const parent = useContext(DrawerContext);
|
|
65
|
+
const anchorRef = useRef(null);
|
|
66
|
+
const shellRef = useRef(null);
|
|
67
|
+
const backdropRef = useRef(null);
|
|
68
|
+
const bodyRef = useRef(null);
|
|
69
|
+
const [mounted, setMounted] = useState(false);
|
|
70
|
+
const [host, setHost] = useState(null);
|
|
71
|
+
const [nested, setNested] = useState(false);
|
|
72
|
+
const physics = useRef({ x: 0, v: 0, target: 0, frame: 0, onRest: null });
|
|
73
|
+
const geometry = useRef({ snaps: [0], closed: 1, full: 1 });
|
|
74
|
+
const [snapIndex, setSnapIndex] = useState(defaultSnap);
|
|
75
|
+
const opener = useRef(null);
|
|
76
|
+
const isContained = parent ? parent.contained : contained;
|
|
77
|
+
const bottom = side === "bottom";
|
|
78
|
+
const sign = side === "left" ? -1 : 1;
|
|
79
|
+
const request = (next) => onOpenChange?.(next);
|
|
80
|
+
const childContext = useMemo(() => ({ host, contained: isContained, setNested }), [host, isContained]);
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
setHost(parent?.host ?? (contained ? anchorRef.current?.parentElement : document.body));
|
|
83
|
+
}, [parent, contained]);
|
|
84
|
+
const measure = () => {
|
|
85
|
+
const box = isContained ? host : null;
|
|
86
|
+
const viewH = box ? box.clientHeight : window.innerHeight;
|
|
87
|
+
const viewW = box ? box.clientWidth : window.innerWidth;
|
|
88
|
+
if (bottom) {
|
|
89
|
+
const points = [...snapPoints].sort((a, b) => a - b);
|
|
90
|
+
const full = Math.max(...points) * viewH;
|
|
91
|
+
geometry.current = { full, closed: full, snaps: points.map((p) => full - p * viewH) };
|
|
92
|
+
} else {
|
|
93
|
+
const full = Math.min(size, viewW * 0.92);
|
|
94
|
+
geometry.current = { full, closed: full, snaps: [0] };
|
|
95
|
+
}
|
|
96
|
+
shellRef.current?.style.setProperty("--bl-size", `${geometry.current.full}px`);
|
|
97
|
+
};
|
|
98
|
+
const apply = () => {
|
|
99
|
+
const { x } = physics.current;
|
|
100
|
+
const { closed, snaps } = geometry.current;
|
|
101
|
+
const shell = shellRef.current;
|
|
102
|
+
if (!shell) return;
|
|
103
|
+
const pushed = nestedRef.current ? bottom ? " scale(0.94) translateY(-14px)" : " scale(0.96)" : "";
|
|
104
|
+
shell.style.transform = bottom ? `translate3d(0, ${x}px, 0)${pushed}` : `translate3d(${sign * x}px, 0, 0)${pushed}`;
|
|
105
|
+
const openAmount = 1 - Math.min(1, Math.max(0, (x - Math.min(...snaps)) / Math.max(1, closed - Math.min(...snaps))));
|
|
106
|
+
if (backdropRef.current) backdropRef.current.style.opacity = String(openAmount);
|
|
107
|
+
};
|
|
108
|
+
const nestedRef = useRef(false);
|
|
109
|
+
nestedRef.current = nested;
|
|
110
|
+
const springTo = (target, velocity = physics.current.v, onRest = null) => {
|
|
111
|
+
const p = physics.current;
|
|
112
|
+
p.target = target;
|
|
113
|
+
p.v = velocity;
|
|
114
|
+
p.onRest = onRest;
|
|
115
|
+
cancelAnimationFrame(p.frame);
|
|
116
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
117
|
+
p.x = target;
|
|
118
|
+
p.v = 0;
|
|
119
|
+
apply();
|
|
120
|
+
onRest?.();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const critical = 2 * Math.sqrt(stiffness);
|
|
124
|
+
let last = performance.now();
|
|
125
|
+
const step = (now) => {
|
|
126
|
+
const dt = Math.min(0.25, (now - last) / 1e3);
|
|
127
|
+
last = now;
|
|
128
|
+
const steps = Math.max(1, Math.ceil(dt / 4e-3));
|
|
129
|
+
for (let i = 0; i < steps; i++) {
|
|
130
|
+
const h = dt / steps;
|
|
131
|
+
const force = -stiffness * (p.x - p.target) - critical * damping * p.v;
|
|
132
|
+
p.v += force * h;
|
|
133
|
+
p.x += p.v * h;
|
|
134
|
+
}
|
|
135
|
+
apply();
|
|
136
|
+
if (Math.abs(p.x - p.target) < 0.4 && Math.abs(p.v) < 8) {
|
|
137
|
+
p.x = p.target;
|
|
138
|
+
p.v = 0;
|
|
139
|
+
apply();
|
|
140
|
+
p.frame = 0;
|
|
141
|
+
const done = p.onRest;
|
|
142
|
+
p.onRest = null;
|
|
143
|
+
done?.();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
p.frame = requestAnimationFrame(step);
|
|
147
|
+
};
|
|
148
|
+
p.frame = requestAnimationFrame(step);
|
|
149
|
+
};
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (open) {
|
|
152
|
+
opener.current = document.activeElement;
|
|
153
|
+
setSnapIndex(Math.min(defaultSnap, (bottom ? snapPoints.length : 1) - 1));
|
|
154
|
+
setMounted(true);
|
|
155
|
+
} else if (mounted) {
|
|
156
|
+
springTo(geometry.current.closed, physics.current.v, () => {
|
|
157
|
+
setMounted(false);
|
|
158
|
+
opener.current?.focus?.({ preventScroll: true });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
parent?.setNested(open);
|
|
162
|
+
}, [open]);
|
|
163
|
+
useEffect(() => () => parent?.setNested(false), [parent]);
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
if (!mounted || !host) return;
|
|
166
|
+
measure();
|
|
167
|
+
physics.current.x = geometry.current.closed;
|
|
168
|
+
apply();
|
|
169
|
+
springTo(geometry.current.snaps[Math.min(snapIndex, geometry.current.snaps.length - 1)], 0);
|
|
170
|
+
shellRef.current.focus({ preventScroll: true });
|
|
171
|
+
let unlock = () => {
|
|
172
|
+
};
|
|
173
|
+
if (!isContained && !parent) {
|
|
174
|
+
const previous = document.body.style.overflow;
|
|
175
|
+
document.body.style.overflow = "hidden";
|
|
176
|
+
unlock = () => document.body.style.overflow = previous;
|
|
177
|
+
}
|
|
178
|
+
const onResize = () => {
|
|
179
|
+
measure();
|
|
180
|
+
springTo(geometry.current.snaps[Math.min(snapIndexRef.current, geometry.current.snaps.length - 1)]);
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("resize", onResize);
|
|
183
|
+
return () => {
|
|
184
|
+
unlock();
|
|
185
|
+
window.removeEventListener("resize", onResize);
|
|
186
|
+
cancelAnimationFrame(physics.current.frame);
|
|
187
|
+
};
|
|
188
|
+
}, [mounted, host]);
|
|
189
|
+
const snapIndexRef = useRef(snapIndex);
|
|
190
|
+
snapIndexRef.current = snapIndex;
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (mounted) apply();
|
|
193
|
+
}, [nested]);
|
|
194
|
+
const goToSnap = (index) => {
|
|
195
|
+
const { snaps } = geometry.current;
|
|
196
|
+
if (index < 0) {
|
|
197
|
+
if (dismissible) request(false);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const clamped = Math.min(snaps.length - 1, index);
|
|
201
|
+
setSnapIndex(clamped);
|
|
202
|
+
springTo(snaps[clamped]);
|
|
203
|
+
};
|
|
204
|
+
const drag = useRef(null);
|
|
205
|
+
const onPointerDown = (event) => {
|
|
206
|
+
if (event.button > 0 || event.target.closest("input, textarea, select, a, [data-no-drag]")) return;
|
|
207
|
+
const inBody = bodyRef.current?.contains(event.target);
|
|
208
|
+
if (inBody && bodyRef.current.scrollTop > 0) return;
|
|
209
|
+
if (event.target.closest("button") && !event.target.closest(".bl-drawer__handle")) return;
|
|
210
|
+
cancelAnimationFrame(physics.current.frame);
|
|
211
|
+
drag.current = { id: event.pointerId, start: bottom ? event.clientY : event.clientX, origin: physics.current.x, samples: [], moved: false, inBody };
|
|
212
|
+
};
|
|
213
|
+
const onPointerMove = (event) => {
|
|
214
|
+
const d = drag.current;
|
|
215
|
+
if (!d || event.pointerId !== d.id) return;
|
|
216
|
+
const pos = bottom ? event.clientY : event.clientX;
|
|
217
|
+
let delta = (pos - d.start) * (bottom ? 1 : sign);
|
|
218
|
+
if (!d.moved) {
|
|
219
|
+
if (Math.abs(delta) < 4) return;
|
|
220
|
+
if (d.inBody && delta < 0 && bodyRef.current.scrollHeight > bodyRef.current.clientHeight) {
|
|
221
|
+
drag.current = null;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
d.moved = true;
|
|
225
|
+
try {
|
|
226
|
+
shellRef.current.setPointerCapture(event.pointerId);
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let x = d.origin + delta;
|
|
231
|
+
const min = Math.min(...geometry.current.snaps);
|
|
232
|
+
if (x < min) x = min - Math.pow(min - x, 0.7);
|
|
233
|
+
if (!dismissible && x > geometry.current.snaps[0]) x = geometry.current.snaps[0] + Math.pow(x - geometry.current.snaps[0], 0.7);
|
|
234
|
+
physics.current.x = x;
|
|
235
|
+
d.samples.push([performance.now(), x]);
|
|
236
|
+
if (d.samples.length > 5) d.samples.shift();
|
|
237
|
+
apply();
|
|
238
|
+
};
|
|
239
|
+
const onPointerUp = (event) => {
|
|
240
|
+
const d = drag.current;
|
|
241
|
+
drag.current = null;
|
|
242
|
+
if (!d || !d.moved) return;
|
|
243
|
+
if (shellRef.current.hasPointerCapture?.(event.pointerId)) shellRef.current.releasePointerCapture(event.pointerId);
|
|
244
|
+
const [first, last] = [d.samples[0], d.samples[d.samples.length - 1]];
|
|
245
|
+
const velocity = first && last && last[0] > first[0] ? (last[1] - first[1]) / (last[0] - first[0]) * 1e3 : 0;
|
|
246
|
+
const projected = physics.current.x + velocity * 0.2;
|
|
247
|
+
const { snaps, closed } = geometry.current;
|
|
248
|
+
const candidates = snaps.map((x, i) => ({ x, i }));
|
|
249
|
+
if (dismissible) candidates.push({ x: closed, i: -1 });
|
|
250
|
+
const best = candidates.reduce((a, b) => Math.abs(b.x - projected) < Math.abs(a.x - projected) ? b : a);
|
|
251
|
+
physics.current.v = velocity;
|
|
252
|
+
if (best.i === -1) request(false);
|
|
253
|
+
else {
|
|
254
|
+
setSnapIndex(best.i);
|
|
255
|
+
springTo(best.x, velocity);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const onKeyDown = (event) => {
|
|
259
|
+
if (event.key === "Escape" && dismissible) {
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
request(false);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (event.key !== "Tab") return;
|
|
265
|
+
const items = [...shellRef.current.querySelectorAll(FOCUSABLE)].filter((el) => !el.closest("[inert]"));
|
|
266
|
+
if (!items.length) return event.preventDefault();
|
|
267
|
+
const first = items[0];
|
|
268
|
+
const last = items[items.length - 1];
|
|
269
|
+
if (event.shiftKey && (document.activeElement === first || document.activeElement === shellRef.current)) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
last.focus();
|
|
272
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
first.focus();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const onHandleKey = (event) => {
|
|
278
|
+
if (!bottom) return;
|
|
279
|
+
if (event.key === "ArrowUp") {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
goToSnap(snapIndex + 1);
|
|
282
|
+
} else if (event.key === "ArrowDown") {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
goToSnap(snapIndex - 1);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const snapCount = bottom ? snapPoints.length : 1;
|
|
288
|
+
const content = mounted && host && /* @__PURE__ */ jsxs(
|
|
289
|
+
"div",
|
|
290
|
+
{
|
|
291
|
+
className: `bl-drawer ${className}`,
|
|
292
|
+
"data-side": side,
|
|
293
|
+
"data-contained": isContained || void 0,
|
|
294
|
+
style: {
|
|
295
|
+
position: isContained ? "absolute" : "fixed",
|
|
296
|
+
inset: 0,
|
|
297
|
+
zIndex: 1200,
|
|
298
|
+
pointerEvents: "none",
|
|
299
|
+
"--bl-radius": `${radius}px`,
|
|
300
|
+
"--bl-pill": radius ? "999px" : "0",
|
|
301
|
+
"--bl-bg": backgroundColor,
|
|
302
|
+
"--bl-ink": textColor,
|
|
303
|
+
"--bl-muted": mutedColor,
|
|
304
|
+
"--bl-line": lineColor,
|
|
305
|
+
"--bl-accent": accentColor,
|
|
306
|
+
"--bl-backdrop": backdropColor,
|
|
307
|
+
"--bl-font": fontFamily,
|
|
308
|
+
"--bl-max-w": `${maxWidth}px`,
|
|
309
|
+
overflow: "hidden"
|
|
310
|
+
},
|
|
311
|
+
children: [
|
|
312
|
+
/* @__PURE__ */ jsx("div", { ref: backdropRef, className: "bl-drawer__backdrop", style: { pointerEvents: "auto" }, onClick: () => dismissible && request(false), "aria-hidden": "true" }),
|
|
313
|
+
/* @__PURE__ */ jsxs(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
ref: shellRef,
|
|
317
|
+
className: "bl-drawer__shell",
|
|
318
|
+
role: "dialog",
|
|
319
|
+
"aria-modal": "true",
|
|
320
|
+
"aria-labelledby": title ? `${uid}-title` : void 0,
|
|
321
|
+
"aria-describedby": description ? `${uid}-desc` : void 0,
|
|
322
|
+
tabIndex: -1,
|
|
323
|
+
"data-nested": nested || void 0,
|
|
324
|
+
inert: nested ? true : void 0,
|
|
325
|
+
style: { pointerEvents: "auto" },
|
|
326
|
+
onPointerDown,
|
|
327
|
+
onPointerMove,
|
|
328
|
+
onPointerUp,
|
|
329
|
+
onPointerCancel: onPointerUp,
|
|
330
|
+
onKeyDown,
|
|
331
|
+
children: [
|
|
332
|
+
/* @__PURE__ */ jsx(
|
|
333
|
+
"button",
|
|
334
|
+
{
|
|
335
|
+
type: "button",
|
|
336
|
+
className: "bl-drawer__handle",
|
|
337
|
+
"aria-label": bottom && snapCount > 1 ? `Resize drawer (position ${snapIndex + 1} of ${snapCount}). Use up and down arrows.` : "Drag to close",
|
|
338
|
+
onKeyDown: onHandleKey,
|
|
339
|
+
onClick: () => bottom && snapCount > 1 && goToSnap((snapIndex + 1) % snapCount)
|
|
340
|
+
}
|
|
341
|
+
),
|
|
342
|
+
(title || description) && /* @__PURE__ */ jsxs("div", { className: "bl-drawer__header", children: [
|
|
343
|
+
title && /* @__PURE__ */ jsx("h2", { id: `${uid}-title`, className: "bl-drawer__title", children: title }),
|
|
344
|
+
description && /* @__PURE__ */ jsx("p", { id: `${uid}-desc`, className: "bl-drawer__desc", children: description })
|
|
345
|
+
] }),
|
|
346
|
+
/* @__PURE__ */ jsx("div", { ref: bodyRef, className: "bl-drawer__body", children: /* @__PURE__ */ jsx(DrawerContext.Provider, { value: childContext, children }) })
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsx("span", { ref: anchorRef, hidden: true }),
|
|
355
|
+
content && createPortal(content, host)
|
|
356
|
+
] });
|
|
357
|
+
}
|
|
358
|
+
export {
|
|
359
|
+
SpringDrawer as default
|
|
360
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
export interface SpriteAnimation {
|
|
3
|
+
/** Row of the sheet this animation lives on (0 is the top). */
|
|
4
|
+
row?: number;
|
|
5
|
+
/** Number of frames in the row. */
|
|
6
|
+
frames?: number;
|
|
7
|
+
/** Frames per second. */
|
|
8
|
+
fps?: number;
|
|
9
|
+
loop?: boolean;
|
|
10
|
+
/** Play forward then backward instead of jumping back to frame 0. */
|
|
11
|
+
pingPong?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SpriteAnimatorProps {
|
|
14
|
+
/** Sprite sheet image URL. */
|
|
15
|
+
src?: string;
|
|
16
|
+
/** Size of one frame on the sheet, in px. */
|
|
17
|
+
frameWidth?: number;
|
|
18
|
+
frameHeight?: number;
|
|
19
|
+
/** Named animations; pick one with `animation`. */
|
|
20
|
+
animations?: Record<string, SpriteAnimation>;
|
|
21
|
+
/** Key of the animation to play. Without it, `row`, `frames` and `fps` are used. */
|
|
22
|
+
animation?: string;
|
|
23
|
+
row?: number;
|
|
24
|
+
frames?: number;
|
|
25
|
+
fps?: number;
|
|
26
|
+
/** Integer upscaling factor (pixels stay crisp). */
|
|
27
|
+
scale?: number;
|
|
28
|
+
loop?: boolean;
|
|
29
|
+
pingPong?: boolean;
|
|
30
|
+
/** Pause on the current frame when false. */
|
|
31
|
+
playing?: boolean;
|
|
32
|
+
/** Mirror horizontally. */
|
|
33
|
+
flipX?: boolean;
|
|
34
|
+
/** Accessible description of the sprite. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Called when a non-looping animation reaches its last frame. */
|
|
37
|
+
onEnd?: () => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
}
|
|
41
|
+
export default function SpriteAnimator({ src, frameWidth, frameHeight, animations, animation, row, frames, fps, scale, loop, pingPong, playing, flipX, label, onEnd, className, style }: SpriteAnimatorProps): import("react").JSX.Element;
|