bits-lab 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -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 +34 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CSSProperties, type ElementType } from 'react';
|
|
2
|
+
export interface ProximityTypeProps {
|
|
3
|
+
text?: string;
|
|
4
|
+
as?: ElementType;
|
|
5
|
+
/** How far the pointer reaches, in px. */
|
|
6
|
+
radius?: number;
|
|
7
|
+
falloff?: 'smooth' | 'linear' | 'sharp';
|
|
8
|
+
minWeight?: number;
|
|
9
|
+
maxWeight?: number;
|
|
10
|
+
/** Width axis range in % (needs a font with wdth, e.g. Roboto Flex). */
|
|
11
|
+
minWidth?: number;
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
/** Raise near letters by up to this many px. */
|
|
14
|
+
lift?: number;
|
|
15
|
+
/** How far near letters shift toward the accent (0–1). */
|
|
16
|
+
tint?: number;
|
|
17
|
+
/** How quickly letters follow, 0–1 (1 is instant). */
|
|
18
|
+
ease?: number;
|
|
19
|
+
fontSize?: number | string;
|
|
20
|
+
textColor?: string;
|
|
21
|
+
accentColor?: string;
|
|
22
|
+
fontFamily?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
}
|
|
26
|
+
export default function ProximityType({ text, as: Tag, radius, falloff, minWeight, maxWeight, minWidth, maxWidth, lift, tint, ease, fontSize, textColor, accentColor, fontFamily, className, style }: ProximityTypeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useInsertionEffect, useRef } from "react";
|
|
4
|
+
const STYLE_ID = "bl-proximity-type";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.bl-prox { margin: 0; font-family: var(--bl-font); font-size: var(--bl-size); line-height: 1.1; color: var(--bl-ink); touch-action: pan-y; }
|
|
7
|
+
.bl-prox__word { display: inline-block; white-space: nowrap; }
|
|
8
|
+
.bl-prox__ch { --k: 0; display: inline-block; white-space: pre; font-weight: calc(var(--bl-wmin) + var(--k) * (var(--bl-wmax) - var(--bl-wmin))); font-stretch: calc(var(--bl-dmin) + var(--k) * (var(--bl-dmax) - var(--bl-dmin))); color: color-mix(in srgb, var(--bl-accent) calc(var(--k) * var(--bl-tint)), var(--bl-ink)); transform: translateY(calc(var(--k) * var(--bl-lift) * -1)); transform-origin: 50% 100%; }
|
|
9
|
+
`;
|
|
10
|
+
function useStyles() {
|
|
11
|
+
useInsertionEffect(() => {
|
|
12
|
+
if (typeof document === "undefined") return;
|
|
13
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
14
|
+
style.textContent = CSS;
|
|
15
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
16
|
+
}, []);
|
|
17
|
+
}
|
|
18
|
+
function ProximityType({
|
|
19
|
+
text = "",
|
|
20
|
+
as: Tag = "p",
|
|
21
|
+
radius = 160,
|
|
22
|
+
falloff = "smooth",
|
|
23
|
+
minWeight = 300,
|
|
24
|
+
maxWeight = 900,
|
|
25
|
+
minWidth = 100,
|
|
26
|
+
maxWidth = 100,
|
|
27
|
+
lift = 0,
|
|
28
|
+
tint = 0,
|
|
29
|
+
ease = 0.2,
|
|
30
|
+
fontSize = 56,
|
|
31
|
+
textColor = "#f4f4f4",
|
|
32
|
+
accentColor = "#73eff7",
|
|
33
|
+
fontFamily = '"Roboto Flex", ui-sans-serif, system-ui, sans-serif',
|
|
34
|
+
className = "",
|
|
35
|
+
style
|
|
36
|
+
}) {
|
|
37
|
+
useStyles();
|
|
38
|
+
const rootRef = useRef(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const root = rootRef.current;
|
|
41
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return void 0;
|
|
42
|
+
const letters = [...root.querySelectorAll(".bl-prox__ch")];
|
|
43
|
+
const level = letters.map(() => 0);
|
|
44
|
+
let centres = [];
|
|
45
|
+
const pointer = { x: -1e4, y: -1e4 };
|
|
46
|
+
let raf = 0;
|
|
47
|
+
const measure = () => {
|
|
48
|
+
centres = letters.map((node) => {
|
|
49
|
+
const r = node.getBoundingClientRect();
|
|
50
|
+
const k = Number(node.style.getPropertyValue("--k")) || 0;
|
|
51
|
+
return { x: r.left + r.width / 2 + window.scrollX, y: r.top + r.height / 2 + window.scrollY + k * lift };
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const shape = (t) => falloff === "linear" ? t : falloff === "sharp" ? t * t * t : t * t * (3 - 2 * t);
|
|
55
|
+
const tick = () => {
|
|
56
|
+
let moving = false;
|
|
57
|
+
letters.forEach((node, i) => {
|
|
58
|
+
const c = centres[i];
|
|
59
|
+
const d = Math.hypot(c.x - pointer.x, c.y - pointer.y);
|
|
60
|
+
const goal = d < radius ? shape(1 - d / radius) : 0;
|
|
61
|
+
level[i] += (goal - level[i]) * ease;
|
|
62
|
+
if (Math.abs(goal - level[i]) > 2e-3) moving = true;
|
|
63
|
+
else level[i] = goal;
|
|
64
|
+
node.style.setProperty("--k", level[i].toFixed(3));
|
|
65
|
+
});
|
|
66
|
+
raf = moving ? requestAnimationFrame(tick) : 0;
|
|
67
|
+
};
|
|
68
|
+
const wake = () => {
|
|
69
|
+
if (!raf) raf = requestAnimationFrame(tick);
|
|
70
|
+
};
|
|
71
|
+
const onMove = (event) => {
|
|
72
|
+
if (!centres.length) measure();
|
|
73
|
+
pointer.x = event.clientX + window.scrollX;
|
|
74
|
+
pointer.y = event.clientY + window.scrollY;
|
|
75
|
+
wake();
|
|
76
|
+
};
|
|
77
|
+
const onLeave = () => {
|
|
78
|
+
pointer.x = -1e4;
|
|
79
|
+
pointer.y = -1e4;
|
|
80
|
+
wake();
|
|
81
|
+
};
|
|
82
|
+
const onLayout = () => {
|
|
83
|
+
centres = [];
|
|
84
|
+
};
|
|
85
|
+
let listening = false;
|
|
86
|
+
const listen = (on) => {
|
|
87
|
+
if (on === listening) return;
|
|
88
|
+
listening = on;
|
|
89
|
+
if (on) {
|
|
90
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
91
|
+
document.documentElement.addEventListener("pointerleave", onLeave);
|
|
92
|
+
} else {
|
|
93
|
+
window.removeEventListener("pointermove", onMove);
|
|
94
|
+
document.documentElement.removeEventListener("pointerleave", onLeave);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
98
|
+
listen(entry.isIntersecting);
|
|
99
|
+
if (!entry.isIntersecting) onLeave();
|
|
100
|
+
});
|
|
101
|
+
observer.observe(root);
|
|
102
|
+
const resize = new ResizeObserver(onLayout);
|
|
103
|
+
resize.observe(root);
|
|
104
|
+
window.addEventListener("scroll", onLayout, { passive: true, capture: true });
|
|
105
|
+
document.fonts?.ready.then(onLayout);
|
|
106
|
+
return () => {
|
|
107
|
+
cancelAnimationFrame(raf);
|
|
108
|
+
listen(false);
|
|
109
|
+
observer.disconnect();
|
|
110
|
+
resize.disconnect();
|
|
111
|
+
window.removeEventListener("scroll", onLayout, { capture: true });
|
|
112
|
+
letters.forEach((node) => node.style.removeProperty("--k"));
|
|
113
|
+
};
|
|
114
|
+
}, [text, radius, falloff, lift, ease]);
|
|
115
|
+
let index = 0;
|
|
116
|
+
return /* @__PURE__ */ jsx(
|
|
117
|
+
Tag,
|
|
118
|
+
{
|
|
119
|
+
ref: rootRef,
|
|
120
|
+
className: `bl-prox ${className}`,
|
|
121
|
+
style: { "--bl-size": typeof fontSize === "number" ? `${fontSize}px` : fontSize, "--bl-wmin": minWeight, "--bl-wmax": maxWeight, "--bl-dmin": `${minWidth}%`, "--bl-dmax": `${maxWidth}%`, "--bl-lift": `${lift}px`, "--bl-tint": `${tint * 100}%`, "--bl-ink": textColor, "--bl-accent": accentColor, "--bl-font": fontFamily, ...style },
|
|
122
|
+
children: text.split(/(\s+)/).map((part, w) => {
|
|
123
|
+
if (!part) return null;
|
|
124
|
+
if (/^\s+$/.test(part)) return " ";
|
|
125
|
+
return (
|
|
126
|
+
// Letters of a word stay together so lines only break between words.
|
|
127
|
+
/* @__PURE__ */ jsx("span", { className: "bl-prox__word", children: [...part].map((ch) => /* @__PURE__ */ jsx("span", { className: "bl-prox__ch", children: ch }, index++)) }, w)
|
|
128
|
+
);
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
ProximityType as default
|
|
135
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
export interface DialogueChoice {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Passed to onChoice; defaults to the label. */
|
|
5
|
+
value?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DialogueLine {
|
|
8
|
+
/** Name shown in the tab above the box. */
|
|
9
|
+
speaker?: string;
|
|
10
|
+
text: string;
|
|
11
|
+
/** Image or sprite shown at the left of the box. */
|
|
12
|
+
portrait?: ReactNode;
|
|
13
|
+
/** Choices shown once the line finishes typing. */
|
|
14
|
+
choices?: DialogueChoice[];
|
|
15
|
+
}
|
|
16
|
+
export interface QuestDialogProps {
|
|
17
|
+
/** The conversation, in order. */
|
|
18
|
+
lines?: DialogueLine[];
|
|
19
|
+
/** Characters per second. */
|
|
20
|
+
speed?: number;
|
|
21
|
+
/** Max width, in px. */
|
|
22
|
+
width?: number;
|
|
23
|
+
fontSize?: number;
|
|
24
|
+
/** Minimum text lines the box reserves. */
|
|
25
|
+
minLines?: number;
|
|
26
|
+
portraitSize?: number;
|
|
27
|
+
boxColor?: string;
|
|
28
|
+
borderColor?: string;
|
|
29
|
+
textColor?: string;
|
|
30
|
+
nameColor?: string;
|
|
31
|
+
nameBackground?: string;
|
|
32
|
+
accentColor?: string;
|
|
33
|
+
/** Font for the name tab and choices. */
|
|
34
|
+
fontFamily?: string;
|
|
35
|
+
/** Font for the dialogue text. */
|
|
36
|
+
bodyFont?: string;
|
|
37
|
+
/** Start over after the last line. */
|
|
38
|
+
loop?: boolean;
|
|
39
|
+
/** Called for each typed (non-space) character, e.g. to play a blip. */
|
|
40
|
+
onChar?: (char: string) => void;
|
|
41
|
+
/** Called with the new line index after advancing. */
|
|
42
|
+
onAdvance?: (index: number) => void;
|
|
43
|
+
/** Called with the chosen value and its index. */
|
|
44
|
+
onChoice?: (value: string, index: number) => void;
|
|
45
|
+
/** Called after the last line. */
|
|
46
|
+
onComplete?: () => void;
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: CSSProperties;
|
|
49
|
+
}
|
|
50
|
+
export default function QuestDialog({ lines, speed, width, fontSize, minLines, portraitSize, boxColor, borderColor, textColor, nameColor, nameBackground, accentColor, fontFamily, bodyFont, loop, onChar, onAdvance, onChoice, onComplete, className, style }: QuestDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useInsertionEffect, useRef, useState } from "react";
|
|
4
|
+
const STYLE_ID = "bl-quest-dialog";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.bl-quest { position: relative; width: min(var(--bl-width), 100%); margin: 4px; padding: 22px 22px 20px; background: var(--bl-box); color: var(--bl-text); cursor: pointer; user-select: none;
|
|
7
|
+
box-shadow: 0 -4px 0 0 var(--bl-border), 0 4px 0 0 var(--bl-border), -4px 0 0 0 var(--bl-border), 4px 0 0 0 var(--bl-border), inset 0 0 0 4px #0b0b14, 8px 8px 0 4px rgba(11,11,20,0.45); }
|
|
8
|
+
.bl-quest:focus-visible { outline: 4px solid #41a6f6; outline-offset: 8px; }
|
|
9
|
+
.bl-quest__name { position: absolute; top: -16px; left: 18px; padding: 6px 10px; background: var(--bl-name-bg); color: var(--bl-name); font: 10px/1 var(--bl-font, "Press Start 2P", ui-monospace, monospace); text-transform: uppercase;
|
|
10
|
+
box-shadow: 0 -3px 0 0 #0b0b14, 0 3px 0 0 #0b0b14, -3px 0 0 0 #0b0b14, 3px 0 0 0 #0b0b14; }
|
|
11
|
+
.bl-quest__body { display: flex; gap: 18px; align-items: flex-start; }
|
|
12
|
+
.bl-quest__portrait { flex: none; display: grid; place-items: center; width: var(--bl-portrait); height: var(--bl-portrait); background: #0b0b14; box-shadow: inset 0 0 0 3px var(--bl-border); overflow: hidden; }
|
|
13
|
+
.bl-quest__text { flex: 1; min-height: calc(var(--bl-lines) * 1.6em); margin: 4px 0 0; font: var(--bl-size)/1.6 var(--bl-body, var(--bl-font, "Press Start 2P", ui-monospace, monospace)); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
14
|
+
.bl-quest__ghost { visibility: hidden; }
|
|
15
|
+
.bl-quest__next { position: absolute; right: 16px; bottom: 8px; color: var(--bl-accent); font: 12px/1 monospace; animation: bl-quest-bob 0.7s steps(2) infinite; }
|
|
16
|
+
.bl-quest__choices { display: grid; gap: 8px; margin: 14px 0 0; padding: 0 0 0 calc(var(--bl-portrait-gap)); list-style: none; }
|
|
17
|
+
.bl-quest__choice { display: flex; gap: 10px; width: 100%; padding: 6px 8px; border: 0; background: none; color: var(--bl-text); font: var(--bl-size)/1.3 var(--bl-body, var(--bl-font, "Press Start 2P", ui-monospace, monospace)); text-align: left; cursor: pointer; }
|
|
18
|
+
.bl-quest__choice::before { content: "\u25BA"; visibility: hidden; color: var(--bl-accent); }
|
|
19
|
+
.bl-quest__choice[data-active]::before { visibility: visible; animation: bl-quest-blink 0.8s steps(1) infinite; }
|
|
20
|
+
.bl-quest__choice[data-active] { color: var(--bl-accent); }
|
|
21
|
+
.bl-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
22
|
+
@keyframes bl-quest-bob { 50% { transform: translateY(4px); } }
|
|
23
|
+
@keyframes bl-quest-blink { 50% { opacity: 0; } }
|
|
24
|
+
@media (prefers-reduced-motion: reduce) { .bl-quest__next, .bl-quest__choice[data-active]::before { animation: none; } }
|
|
25
|
+
`;
|
|
26
|
+
function useStyles() {
|
|
27
|
+
useInsertionEffect(() => {
|
|
28
|
+
if (typeof document === "undefined") return;
|
|
29
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
30
|
+
style.textContent = CSS;
|
|
31
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
32
|
+
}, []);
|
|
33
|
+
}
|
|
34
|
+
function useReducedMotion() {
|
|
35
|
+
const [reduced, setReduced] = useState(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const query = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
38
|
+
if (!query) return void 0;
|
|
39
|
+
setReduced(query.matches);
|
|
40
|
+
const onChange = (event) => setReduced(event.matches);
|
|
41
|
+
query.addEventListener("change", onChange);
|
|
42
|
+
return () => query.removeEventListener("change", onChange);
|
|
43
|
+
}, []);
|
|
44
|
+
return reduced;
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_LINES = [
|
|
47
|
+
{ speaker: "Old Miner", text: "Ah, a new face! These tunnels run deeper than anyone remembers." },
|
|
48
|
+
{ speaker: "Old Miner", text: "Legend says a diamond the size of your fist waits at the bottom... Will you go?", choices: [{ label: "I'm in. Hand me a pickaxe.", value: "yes" }, { label: "Maybe after lunch.", value: "no" }] }
|
|
49
|
+
];
|
|
50
|
+
const PAUSES = { ".": 8, "!": 8, "?": 8, ",": 4, ";": 4, ":": 4, "\u2014": 4 };
|
|
51
|
+
function QuestDialog({
|
|
52
|
+
lines = DEFAULT_LINES,
|
|
53
|
+
speed = 40,
|
|
54
|
+
width = 620,
|
|
55
|
+
fontSize = 13,
|
|
56
|
+
minLines = 3,
|
|
57
|
+
portraitSize = 72,
|
|
58
|
+
boxColor = "#1a1c2c",
|
|
59
|
+
borderColor = "#f4f4f4",
|
|
60
|
+
textColor = "#f4f4f4",
|
|
61
|
+
nameColor = "#0b0b14",
|
|
62
|
+
nameBackground = "#ffcd75",
|
|
63
|
+
accentColor = "#ffcd75",
|
|
64
|
+
fontFamily,
|
|
65
|
+
bodyFont,
|
|
66
|
+
loop = false,
|
|
67
|
+
onChar,
|
|
68
|
+
onAdvance,
|
|
69
|
+
onChoice,
|
|
70
|
+
onComplete,
|
|
71
|
+
className = "",
|
|
72
|
+
style
|
|
73
|
+
}) {
|
|
74
|
+
useStyles();
|
|
75
|
+
const reduced = useReducedMotion();
|
|
76
|
+
const uid = useId();
|
|
77
|
+
const [index, setIndex] = useState(0);
|
|
78
|
+
const [shown, setShown] = useState(0);
|
|
79
|
+
const [active, setActive] = useState(0);
|
|
80
|
+
const [finished, setFinished] = useState(false);
|
|
81
|
+
const callbacks = useRef({ onChar });
|
|
82
|
+
callbacks.current = { onChar };
|
|
83
|
+
const line = lines[Math.min(index, lines.length - 1)] ?? { text: "" };
|
|
84
|
+
const text = line.text ?? "";
|
|
85
|
+
const typing = shown < text.length;
|
|
86
|
+
const last = index >= lines.length - 1;
|
|
87
|
+
const choices = !typing && line.choices?.length ? line.choices : null;
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
setShown(reduced ? text.length : 0);
|
|
90
|
+
setActive(0);
|
|
91
|
+
}, [index, text, reduced]);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!typing || reduced) return void 0;
|
|
94
|
+
const char = text[shown];
|
|
95
|
+
const delay = 1e3 / Math.max(1, speed) * (PAUSES[text[shown - 1]] ?? 1);
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
setShown((count) => count + 1);
|
|
98
|
+
if (char && char.trim()) callbacks.current.onChar?.(char);
|
|
99
|
+
}, delay);
|
|
100
|
+
return () => clearTimeout(timer);
|
|
101
|
+
}, [typing, shown, text, speed, reduced]);
|
|
102
|
+
const advance = useCallback(() => {
|
|
103
|
+
if (finished) return;
|
|
104
|
+
if (typing) {
|
|
105
|
+
setShown(text.length);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (choices) return;
|
|
109
|
+
if (last) {
|
|
110
|
+
onComplete?.();
|
|
111
|
+
if (loop) setIndex(0);
|
|
112
|
+
else setFinished(true);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setIndex((current) => current + 1);
|
|
116
|
+
onAdvance?.(index + 1);
|
|
117
|
+
}, [finished, typing, text.length, choices, last, loop, onComplete, onAdvance, index]);
|
|
118
|
+
const choose = useCallback(
|
|
119
|
+
(choiceIndex) => {
|
|
120
|
+
const choice = choices?.[choiceIndex];
|
|
121
|
+
if (!choice) return;
|
|
122
|
+
onChoice?.(choice.value ?? choice.label, choiceIndex);
|
|
123
|
+
if (last) {
|
|
124
|
+
onComplete?.();
|
|
125
|
+
if (loop) setIndex(0);
|
|
126
|
+
else setFinished(true);
|
|
127
|
+
} else setIndex((current) => current + 1);
|
|
128
|
+
},
|
|
129
|
+
[choices, onChoice, last, loop, onComplete]
|
|
130
|
+
);
|
|
131
|
+
const onKeyDown = (event) => {
|
|
132
|
+
if (choices && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
setActive((current) => (current + (event.key === "ArrowDown" ? 1 : -1) + choices.length) % choices.length);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
if (choices) choose(active);
|
|
140
|
+
else advance();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const portrait = line.portrait;
|
|
144
|
+
return /* @__PURE__ */ jsxs(
|
|
145
|
+
"div",
|
|
146
|
+
{
|
|
147
|
+
className: `bl-quest ${className}`,
|
|
148
|
+
role: "group",
|
|
149
|
+
"aria-roledescription": "dialogue",
|
|
150
|
+
"aria-label": line.speaker ? `${line.speaker} says` : "Dialogue",
|
|
151
|
+
tabIndex: 0,
|
|
152
|
+
onClick: () => !choices && advance(),
|
|
153
|
+
onKeyDown,
|
|
154
|
+
style: {
|
|
155
|
+
"--bl-width": `${width}px`,
|
|
156
|
+
"--bl-box": boxColor,
|
|
157
|
+
"--bl-border": borderColor,
|
|
158
|
+
"--bl-text": textColor,
|
|
159
|
+
"--bl-name": nameColor,
|
|
160
|
+
"--bl-name-bg": nameBackground,
|
|
161
|
+
"--bl-accent": accentColor,
|
|
162
|
+
"--bl-size": `${fontSize}px`,
|
|
163
|
+
"--bl-lines": minLines,
|
|
164
|
+
"--bl-portrait": `${portraitSize}px`,
|
|
165
|
+
"--bl-portrait-gap": portrait ? `${portraitSize + 18}px` : "0px",
|
|
166
|
+
...fontFamily ? { "--bl-font": fontFamily } : null,
|
|
167
|
+
...bodyFont ? { "--bl-body": bodyFont } : null,
|
|
168
|
+
...style
|
|
169
|
+
},
|
|
170
|
+
children: [
|
|
171
|
+
line.speaker && /* @__PURE__ */ jsx("span", { className: "bl-quest__name", children: line.speaker }),
|
|
172
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-quest__body", children: [
|
|
173
|
+
portrait && /* @__PURE__ */ jsx("div", { className: "bl-quest__portrait", "aria-hidden": "true", children: portrait }),
|
|
174
|
+
/* @__PURE__ */ jsxs("p", { className: "bl-quest__text", "aria-hidden": "true", children: [
|
|
175
|
+
text.slice(0, shown),
|
|
176
|
+
/* @__PURE__ */ jsx("span", { className: "bl-quest__ghost", children: text.slice(shown) })
|
|
177
|
+
] }),
|
|
178
|
+
/* @__PURE__ */ jsx("span", { className: "bl-sr", "aria-live": "polite", children: text })
|
|
179
|
+
] }),
|
|
180
|
+
choices && /* @__PURE__ */ jsx("ul", { className: "bl-quest__choices", role: "listbox", "aria-label": "Choices", "aria-activedescendant": `${uid}-choice-${active}`, children: choices.map((choice, choiceIndex) => /* @__PURE__ */ jsx("li", { role: "presentation", children: /* @__PURE__ */ jsx(
|
|
181
|
+
"button",
|
|
182
|
+
{
|
|
183
|
+
id: `${uid}-choice-${choiceIndex}`,
|
|
184
|
+
type: "button",
|
|
185
|
+
role: "option",
|
|
186
|
+
"aria-selected": choiceIndex === active,
|
|
187
|
+
className: "bl-quest__choice",
|
|
188
|
+
"data-active": choiceIndex === active || void 0,
|
|
189
|
+
onMouseEnter: () => setActive(choiceIndex),
|
|
190
|
+
onClick: (event) => {
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
choose(choiceIndex);
|
|
193
|
+
},
|
|
194
|
+
children: choice.label
|
|
195
|
+
}
|
|
196
|
+
) }, choice.label)) }),
|
|
197
|
+
!typing && !choices && !finished && /* @__PURE__ */ jsx("span", { className: "bl-quest__next", "aria-hidden": "true", children: last ? "\u25A0" : "\u25BC" })
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
QuestDialog as default
|
|
204
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
export interface Quest {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
/** XP paid into the bar when done. */
|
|
7
|
+
xp?: number;
|
|
8
|
+
/** Quest ids that must be done first; locked until then. */
|
|
9
|
+
requires?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface QuestLogProps {
|
|
12
|
+
quests?: Quest[];
|
|
13
|
+
/** Controlled list of done quest ids. */
|
|
14
|
+
completed?: string[];
|
|
15
|
+
defaultCompleted?: string[];
|
|
16
|
+
onCompletedChange?: (completed: string[]) => void;
|
|
17
|
+
/** Fires when a quest is ticked (hook up a sound or analytics). */
|
|
18
|
+
onQuestComplete?: (quest: Quest) => void;
|
|
19
|
+
/** Unticking also unticks quests that depend on it. */
|
|
20
|
+
allowUndo?: boolean;
|
|
21
|
+
title?: string;
|
|
22
|
+
finishedMessage?: string;
|
|
23
|
+
/** Max width in px. */
|
|
24
|
+
width?: number;
|
|
25
|
+
backgroundColor?: string;
|
|
26
|
+
rowColor?: string;
|
|
27
|
+
hoverColor?: string;
|
|
28
|
+
textColor?: string;
|
|
29
|
+
mutedColor?: string;
|
|
30
|
+
lineColor?: string;
|
|
31
|
+
trackColor?: string;
|
|
32
|
+
accentColor?: string;
|
|
33
|
+
doneColor?: string;
|
|
34
|
+
fontFamily?: string;
|
|
35
|
+
titleFontFamily?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
style?: CSSProperties;
|
|
38
|
+
}
|
|
39
|
+
export default function QuestLog({ quests, completed, defaultCompleted, onCompletedChange, onQuestComplete, allowUndo, title, finishedMessage, width, backgroundColor, rowColor, hoverColor, textColor, mutedColor, lineColor, trackColor, accentColor, doneColor, fontFamily, titleFontFamily, className, style }: QuestLogProps): import("react").JSX.Element;
|
package/dist/QuestLog.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useInsertionEffect, useRef, useState } from "react";
|
|
4
|
+
const STYLE_ID = "bl-quest-log";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.bl-ql { position: relative; width: 100%; max-width: var(--bl-w); padding: 18px; box-sizing: border-box; background: var(--bl-bg); color: var(--bl-ink); font-family: var(--bl-font); box-shadow: 0 0 0 3px var(--bl-line), 6px 6px 0 3px rgba(0,0,0,0.45); }
|
|
7
|
+
.bl-ql__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 10px; }
|
|
8
|
+
.bl-ql__title { margin: 0; font-family: var(--bl-title-font); font-size: 13px; font-weight: 400; color: var(--bl-accent); letter-spacing: 0.04em; }
|
|
9
|
+
.bl-ql__xp { color: var(--bl-muted); font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
10
|
+
.bl-ql__bar { position: relative; height: 12px; margin-bottom: 16px; background: var(--bl-track); box-shadow: inset 0 0 0 2px rgba(0,0,0,0.35); }
|
|
11
|
+
.bl-ql__fill { position: absolute; inset: 0 auto 0 0; width: calc(var(--bl-p) * 100%); background: repeating-linear-gradient(90deg, var(--bl-accent) 0 8px, color-mix(in srgb, var(--bl-accent) 75%, #000) 8px 10px); transition: width 600ms steps(12); }
|
|
12
|
+
.bl-ql__list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
|
13
|
+
.bl-ql__quest { position: relative; display: grid; grid-template-columns: 22px 1fr auto; align-items: start; gap: 12px; padding: 10px 12px; background: var(--bl-row); cursor: pointer; }
|
|
14
|
+
.bl-ql__quest:hover { background: var(--bl-hover); }
|
|
15
|
+
.bl-ql__quest[data-state="locked"] { cursor: not-allowed; opacity: 0.55; }
|
|
16
|
+
.bl-ql__quest[data-state="done"] .bl-ql__name { color: var(--bl-muted); text-decoration: line-through; text-decoration-thickness: 2px; }
|
|
17
|
+
.bl-ql__quest[data-flash] { animation: bl-ql-flash 600ms steps(4); }
|
|
18
|
+
.bl-ql__input { position: absolute; width: 1px; height: 1px; margin: 0; overflow: hidden; clip: rect(0 0 0 0); opacity: 0; }
|
|
19
|
+
.bl-ql__box { position: relative; width: 22px; height: 22px; background: var(--bl-track); box-shadow: inset 0 0 0 3px var(--bl-line); color: var(--bl-muted); }
|
|
20
|
+
.bl-ql__input:focus-visible ~ .bl-ql__box { outline: 3px solid var(--bl-accent); outline-offset: 2px; }
|
|
21
|
+
.bl-ql__quest[data-state="done"] .bl-ql__box { color: var(--bl-bg); background: var(--bl-done); box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--bl-done) 60%, #000); }
|
|
22
|
+
.bl-ql__box svg { position: absolute; inset: 3px; width: 16px; height: 16px; }
|
|
23
|
+
.bl-ql__mark { position: absolute; inset: 0; display: grid; place-items: center; color: var(--bl-accent); font-family: var(--bl-title-font); font-size: 11px; animation: bl-ql-bob 1.2s steps(2) infinite; }
|
|
24
|
+
.bl-ql__body { display: grid; gap: 3px; min-width: 0; }
|
|
25
|
+
.bl-ql__name { font-size: 15px; font-weight: 600; line-height: 1.3; }
|
|
26
|
+
.bl-ql__desc { color: var(--bl-muted); font-size: 13px; line-height: 1.4; }
|
|
27
|
+
.bl-ql__reward { position: relative; padding: 3px 7px; background: var(--bl-track); color: var(--bl-accent); font-family: var(--bl-title-font); font-size: 9px; line-height: 1.4; white-space: nowrap; }
|
|
28
|
+
.bl-ql__quest[data-state="done"] .bl-ql__reward { background: var(--bl-accent); color: var(--bl-bg); }
|
|
29
|
+
.bl-ql__pop { position: absolute; right: 0; bottom: 100%; color: var(--bl-accent); font-family: var(--bl-title-font); font-size: 10px; white-space: nowrap; text-shadow: 2px 2px 0 rgba(0,0,0,0.6); pointer-events: none; animation: bl-ql-pop 1000ms steps(10) forwards; }
|
|
30
|
+
.bl-ql__banner { position: relative; margin-top: 14px; padding: 12px; background: var(--bl-accent); color: var(--bl-bg); font-family: var(--bl-title-font); font-size: 11px; line-height: 1.5; text-align: center; animation: bl-ql-banner 500ms cubic-bezier(.3,1.5,.5,1) both; }
|
|
31
|
+
.bl-ql__spark { position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; background: var(--c); animation: bl-ql-spark 900ms cubic-bezier(.2,.8,.3,1) forwards; }
|
|
32
|
+
.bl-ql__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
33
|
+
@keyframes bl-ql-flash { 25%, 75% { background: color-mix(in srgb, var(--bl-done) 35%, var(--bl-row)); } }
|
|
34
|
+
@keyframes bl-ql-bob { 50% { transform: translateY(-3px); } }
|
|
35
|
+
@keyframes bl-ql-pop { from { transform: translateY(6px); opacity: 0; } 15% { opacity: 1; } to { transform: translateY(-26px); opacity: 0; } }
|
|
36
|
+
@keyframes bl-ql-banner { from { transform: scale(0.6); opacity: 0; } }
|
|
37
|
+
@keyframes bl-ql-spark { from { transform: translate(-50%, -50%); opacity: 1; } to { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))); opacity: 0; } }
|
|
38
|
+
@media (prefers-reduced-motion: reduce) {
|
|
39
|
+
.bl-ql__fill { transition: none; }
|
|
40
|
+
.bl-ql__quest[data-flash], .bl-ql__mark, .bl-ql__banner, .bl-ql__spark { animation: none; }
|
|
41
|
+
.bl-ql__spark { display: none; }
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
function useStyles() {
|
|
45
|
+
useInsertionEffect(() => {
|
|
46
|
+
if (typeof document === "undefined") return;
|
|
47
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
48
|
+
style.textContent = CSS;
|
|
49
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
50
|
+
}, []);
|
|
51
|
+
}
|
|
52
|
+
const SPARK_COLORS = ["#ffcd75", "#ef7d57", "#a7f070", "#73eff7", "#f4f4f4", "#b13e53"];
|
|
53
|
+
const SPARKS = Array.from({ length: 14 }, (_, i) => {
|
|
54
|
+
const angle = i / 14 * Math.PI * 2;
|
|
55
|
+
const dist = 60 + i % 3 * 24;
|
|
56
|
+
return { dx: `${Math.round(Math.cos(angle) * dist * 1.6)}px`, dy: `${Math.round(Math.sin(angle) * dist * 0.7)}px`, c: SPARK_COLORS[i % SPARK_COLORS.length] };
|
|
57
|
+
});
|
|
58
|
+
function QuestLog({
|
|
59
|
+
quests = [],
|
|
60
|
+
completed,
|
|
61
|
+
defaultCompleted = [],
|
|
62
|
+
onCompletedChange,
|
|
63
|
+
onQuestComplete,
|
|
64
|
+
allowUndo = true,
|
|
65
|
+
title = "Quest log",
|
|
66
|
+
finishedMessage = "All quests complete!",
|
|
67
|
+
width = 520,
|
|
68
|
+
backgroundColor = "#1a1c2c",
|
|
69
|
+
rowColor = "#262b44",
|
|
70
|
+
hoverColor = "#2e3552",
|
|
71
|
+
textColor = "#f4f4f4",
|
|
72
|
+
mutedColor = "#94b0c2",
|
|
73
|
+
lineColor = "#333c57",
|
|
74
|
+
trackColor = "#0b0b14",
|
|
75
|
+
accentColor = "#ffcd75",
|
|
76
|
+
doneColor = "#38b764",
|
|
77
|
+
fontFamily = '"Pixelify Sans", ui-sans-serif, system-ui, sans-serif',
|
|
78
|
+
titleFontFamily = '"Press Start 2P", ui-monospace, monospace',
|
|
79
|
+
className = "",
|
|
80
|
+
style
|
|
81
|
+
}) {
|
|
82
|
+
useStyles();
|
|
83
|
+
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
84
|
+
const [inner, setInner] = useState(defaultCompleted);
|
|
85
|
+
const done = completed ?? inner;
|
|
86
|
+
const [flash, setFlash] = useState(null);
|
|
87
|
+
const [pops, setPops] = useState([]);
|
|
88
|
+
const [announce, setAnnounce] = useState("");
|
|
89
|
+
const popId = useRef(0);
|
|
90
|
+
const byId = Object.fromEntries(quests.map((q) => [q.id, q]));
|
|
91
|
+
const isLocked = (quest) => (quest.requires ?? []).some((id) => !done.includes(id));
|
|
92
|
+
const earned = quests.reduce((sum, q) => sum + (done.includes(q.id) ? q.xp ?? 0 : 0), 0);
|
|
93
|
+
const totalXp = quests.reduce((sum, q) => sum + (q.xp ?? 0), 0);
|
|
94
|
+
const allDone = quests.length > 0 && quests.every((q) => done.includes(q.id));
|
|
95
|
+
const commit = (next) => {
|
|
96
|
+
if (completed === void 0) setInner(next);
|
|
97
|
+
onCompletedChange?.(next);
|
|
98
|
+
};
|
|
99
|
+
const toggle = (quest) => {
|
|
100
|
+
if (isLocked(quest)) return;
|
|
101
|
+
if (done.includes(quest.id)) {
|
|
102
|
+
if (!allowUndo) return;
|
|
103
|
+
const removed = /* @__PURE__ */ new Set([quest.id]);
|
|
104
|
+
let grew = true;
|
|
105
|
+
while (grew) {
|
|
106
|
+
grew = false;
|
|
107
|
+
for (const q of quests) {
|
|
108
|
+
if (!removed.has(q.id) && (q.requires ?? []).some((id) => removed.has(id))) {
|
|
109
|
+
removed.add(q.id);
|
|
110
|
+
grew = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
commit(done.filter((id) => !removed.has(id)));
|
|
115
|
+
setAnnounce(`${quest.title} marked not done.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const next = [...done, quest.id];
|
|
119
|
+
commit(next);
|
|
120
|
+
onQuestComplete?.(quest);
|
|
121
|
+
setFlash(quest.id);
|
|
122
|
+
popId.current += 1;
|
|
123
|
+
setPops((list) => [...list, { id: popId.current, quest: quest.id, xp: quest.xp ?? 0 }]);
|
|
124
|
+
const finishing = quests.every((q) => next.includes(q.id));
|
|
125
|
+
setAnnounce(`Quest complete: ${quest.title}.${quest.xp ? ` Plus ${quest.xp} XP.` : ""}${finishing ? ` ${finishedMessage}` : ""}`);
|
|
126
|
+
};
|
|
127
|
+
return /* @__PURE__ */ jsxs(
|
|
128
|
+
"section",
|
|
129
|
+
{
|
|
130
|
+
className: `bl-ql ${className}`,
|
|
131
|
+
"aria-labelledby": `${uid}-title`,
|
|
132
|
+
style: {
|
|
133
|
+
"--bl-w": `${width}px`,
|
|
134
|
+
"--bl-bg": backgroundColor,
|
|
135
|
+
"--bl-row": rowColor,
|
|
136
|
+
"--bl-hover": hoverColor,
|
|
137
|
+
"--bl-ink": textColor,
|
|
138
|
+
"--bl-muted": mutedColor,
|
|
139
|
+
"--bl-line": lineColor,
|
|
140
|
+
"--bl-track": trackColor,
|
|
141
|
+
"--bl-accent": accentColor,
|
|
142
|
+
"--bl-done": doneColor,
|
|
143
|
+
"--bl-p": totalXp ? earned / totalXp : 0,
|
|
144
|
+
"--bl-font": fontFamily,
|
|
145
|
+
"--bl-title-font": titleFontFamily,
|
|
146
|
+
...style
|
|
147
|
+
},
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-ql__head", children: [
|
|
150
|
+
/* @__PURE__ */ jsx("h3", { id: `${uid}-title`, className: "bl-ql__title", children: title }),
|
|
151
|
+
/* @__PURE__ */ jsxs("span", { className: "bl-ql__xp", children: [
|
|
152
|
+
earned,
|
|
153
|
+
" / ",
|
|
154
|
+
totalXp,
|
|
155
|
+
" XP"
|
|
156
|
+
] })
|
|
157
|
+
] }),
|
|
158
|
+
/* @__PURE__ */ jsx("div", { className: "bl-ql__bar", role: "progressbar", "aria-label": "Experience", "aria-valuemin": 0, "aria-valuemax": totalXp, "aria-valuenow": earned, children: /* @__PURE__ */ jsx("div", { className: "bl-ql__fill" }) }),
|
|
159
|
+
/* @__PURE__ */ jsx("ul", { className: "bl-ql__list", children: quests.map((quest) => {
|
|
160
|
+
const locked = isLocked(quest);
|
|
161
|
+
const complete = done.includes(quest.id);
|
|
162
|
+
const state = complete ? "done" : locked ? "locked" : "active";
|
|
163
|
+
const needs = (quest.requires ?? []).filter((id) => !done.includes(id)).map((id) => byId[id]?.title ?? id);
|
|
164
|
+
const descId = `${uid}-${quest.id}-desc`;
|
|
165
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", { className: "bl-ql__quest", "data-state": state, "data-flash": flash === quest.id || void 0, onAnimationEnd: (event) => event.target === event.currentTarget && setFlash(null), children: [
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
"input",
|
|
168
|
+
{
|
|
169
|
+
type: "checkbox",
|
|
170
|
+
className: "bl-ql__input",
|
|
171
|
+
checked: complete,
|
|
172
|
+
disabled: locked || complete && !allowUndo,
|
|
173
|
+
"aria-describedby": descId,
|
|
174
|
+
onChange: () => toggle(quest)
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
/* @__PURE__ */ jsxs("span", { className: "bl-ql__box", "aria-hidden": "true", children: [
|
|
178
|
+
complete && /* @__PURE__ */ jsx("svg", { viewBox: "0 0 8 8", shapeRendering: "crispEdges", children: /* @__PURE__ */ jsx("path", { d: "M1 4 3 6 7 2", fill: "none", stroke: "currentColor", strokeWidth: "1.6" }) }),
|
|
179
|
+
state === "active" && /* @__PURE__ */ jsx("span", { className: "bl-ql__mark", children: "!" }),
|
|
180
|
+
locked && /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 8 8", shapeRendering: "crispEdges", fill: "currentColor", children: [
|
|
181
|
+
/* @__PURE__ */ jsx("rect", { x: 2, y: 1, width: 4, height: 1 }),
|
|
182
|
+
/* @__PURE__ */ jsx("rect", { x: 1, y: 2, width: 1, height: 2 }),
|
|
183
|
+
/* @__PURE__ */ jsx("rect", { x: 6, y: 2, width: 1, height: 2 }),
|
|
184
|
+
/* @__PURE__ */ jsx("rect", { x: 1, y: 4, width: 6, height: 4 })
|
|
185
|
+
] })
|
|
186
|
+
] }),
|
|
187
|
+
/* @__PURE__ */ jsxs("span", { className: "bl-ql__body", children: [
|
|
188
|
+
/* @__PURE__ */ jsx("span", { className: "bl-ql__name", children: quest.title }),
|
|
189
|
+
/* @__PURE__ */ jsx("span", { id: descId, className: "bl-ql__desc", children: locked ? `Locked. Requires: ${needs.join(", ")}.` : quest.description })
|
|
190
|
+
] }),
|
|
191
|
+
quest.xp ? /* @__PURE__ */ jsxs("span", { className: "bl-ql__reward", "aria-label": `${quest.xp} XP`, children: [
|
|
192
|
+
"+",
|
|
193
|
+
quest.xp,
|
|
194
|
+
" XP",
|
|
195
|
+
pops.filter((pop) => pop.quest === quest.id).map((pop) => /* @__PURE__ */ jsxs("span", { className: "bl-ql__pop", "aria-hidden": "true", onAnimationEnd: () => setPops((list) => list.filter((p) => p.id !== pop.id)), children: [
|
|
196
|
+
"+",
|
|
197
|
+
pop.xp,
|
|
198
|
+
" XP"
|
|
199
|
+
] }, pop.id))
|
|
200
|
+
] }) : /* @__PURE__ */ jsx("span", {})
|
|
201
|
+
] }) }, quest.id);
|
|
202
|
+
}) }),
|
|
203
|
+
allDone && /* @__PURE__ */ jsxs("div", { className: "bl-ql__banner", "aria-hidden": "true", children: [
|
|
204
|
+
finishedMessage,
|
|
205
|
+
SPARKS.map((spark, i) => /* @__PURE__ */ jsx("i", { className: "bl-ql__spark", style: { "--dx": spark.dx, "--dy": spark.dy, "--c": spark.c } }, i))
|
|
206
|
+
] }),
|
|
207
|
+
/* @__PURE__ */ jsx("p", { className: "bl-ql__sr", "aria-live": "polite", children: announce })
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
QuestLog as default
|
|
214
|
+
};
|