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,30 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
export interface TradingCardProps {
|
|
3
|
+
name?: string;
|
|
4
|
+
image?: string;
|
|
5
|
+
imageAlt?: string;
|
|
6
|
+
/** Frame colour and how strong the holo foil is (common has none). */
|
|
7
|
+
rarity?: 'common' | 'rare' | 'epic' | 'legendary';
|
|
8
|
+
stats?: {
|
|
9
|
+
label: string;
|
|
10
|
+
value: ReactNode;
|
|
11
|
+
}[];
|
|
12
|
+
/** With a back, the card is a button that flips. */
|
|
13
|
+
back?: ReactNode;
|
|
14
|
+
flipped?: boolean;
|
|
15
|
+
onFlip?: (flipped: boolean) => void;
|
|
16
|
+
/** Maximum tilt toward the pointer, in degrees. */
|
|
17
|
+
tilt?: number;
|
|
18
|
+
width?: number;
|
|
19
|
+
radius?: number;
|
|
20
|
+
cardColor?: string;
|
|
21
|
+
backColor?: string;
|
|
22
|
+
textColor?: string;
|
|
23
|
+
mutedColor?: string;
|
|
24
|
+
accentColor?: string;
|
|
25
|
+
frameColor?: string;
|
|
26
|
+
fontFamily?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
style?: CSSProperties;
|
|
29
|
+
}
|
|
30
|
+
export default function TradingCard({ name, image, imageAlt, rarity, stats, back, flipped: controlled, onFlip, tilt, width, radius, cardColor, backColor, textColor, mutedColor, accentColor, frameColor, fontFamily, className, style }: TradingCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useInsertionEffect, useRef, useState } from "react";
|
|
4
|
+
const STYLE_ID = "bl-trading-card";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.bl-tcard { --rx: 0deg; --ry: 0deg; --gx: 50%; --gy: 50%; --shift: 0.5; display: inline-block; width: var(--bl-w); perspective: 900px; font-family: var(--bl-font); color: var(--bl-ink); }
|
|
7
|
+
.bl-tcard__flip { position: relative; display: block; width: 100%; aspect-ratio: 5 / 7; margin: 0; padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; transform: rotateX(var(--rx)) rotateY(calc(var(--ry) + var(--turn))); transform-style: preserve-3d; transition: transform 500ms cubic-bezier(.3,1.2,.4,1); cursor: default; }
|
|
8
|
+
.bl-tcard[data-flippable] .bl-tcard__flip { cursor: pointer; }
|
|
9
|
+
.bl-tcard[data-tilting] .bl-tcard__flip { transition: transform 80ms linear; }
|
|
10
|
+
.bl-tcard__flip:focus-visible { outline: 2px solid var(--bl-accent); outline-offset: 6px; border-radius: var(--bl-radius); }
|
|
11
|
+
.bl-tcard__face { position: absolute; inset: 0; display: grid; grid-template-rows: auto 1fr auto; gap: 8px; overflow: hidden; padding: 10px; border-radius: var(--bl-radius); background: var(--bl-card); box-shadow: inset 0 0 0 4px var(--bl-frame), inset 0 0 0 6px rgba(0,0,0,0.35), 0 18px 34px -16px rgba(0,0,0,0.9); backface-visibility: hidden; }
|
|
12
|
+
.bl-tcard__back { transform: rotateY(180deg); place-items: center; grid-template-rows: 1fr; background: var(--bl-back); }
|
|
13
|
+
.bl-tcard__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 4px 6px 0; }
|
|
14
|
+
.bl-tcard__name { font-size: 15px; font-weight: 700; line-height: 1.2; }
|
|
15
|
+
.bl-tcard__rarity { color: var(--bl-frame); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
16
|
+
.bl-tcard__art { position: relative; overflow: hidden; border-radius: 6px; background: #0b0b14; box-shadow: inset 0 0 0 2px rgba(0,0,0,0.5); }
|
|
17
|
+
.bl-tcard__art > img { display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
|
|
18
|
+
.bl-tcard__stats { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px; padding: 0 2px 2px; }
|
|
19
|
+
.bl-tcard__stat { display: grid; justify-items: center; padding: 4px 2px; border-radius: 4px; background: rgba(0,0,0,0.3); }
|
|
20
|
+
.bl-tcard__stat-label { color: var(--bl-muted); font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
21
|
+
.bl-tcard__stat-value { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
22
|
+
.bl-tcard__foil { position: absolute; inset: 0; border-radius: inherit; background: repeating-linear-gradient(115deg, #ef7d57 0 8%, #ffcd75 8% 16%, #a7f070 16% 24%, #73eff7 24% 32%, #41a6f6 32% 40%, #b13e53 40% 48%); background-size: 260% 260%; background-position: calc(var(--shift) * 100%) calc(var(--shift) * 60%); mix-blend-mode: color-dodge; opacity: var(--bl-foil); pointer-events: none; }
|
|
23
|
+
.bl-tcard__glare { position: absolute; inset: 0; border-radius: inherit; background: radial-gradient(circle at var(--gx) var(--gy), rgba(255,255,255,0.45), transparent 45%); mix-blend-mode: overlay; opacity: 0; transition: opacity 200ms; pointer-events: none; }
|
|
24
|
+
.bl-tcard[data-tilting] .bl-tcard__glare { opacity: 1; }
|
|
25
|
+
@media (prefers-reduced-motion: reduce) { .bl-tcard__flip { transition: none; transform: rotateY(var(--turn)); } .bl-tcard__glare { display: none; } }
|
|
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 RARITY = {
|
|
36
|
+
common: { frame: "#94b0c2", foil: 0 },
|
|
37
|
+
rare: { frame: "#41a6f6", foil: 0.25 },
|
|
38
|
+
epic: { frame: "#b13e53", foil: 0.4 },
|
|
39
|
+
legendary: { frame: "#ffcd75", foil: 0.55 }
|
|
40
|
+
};
|
|
41
|
+
function TradingCard({
|
|
42
|
+
name = "",
|
|
43
|
+
image,
|
|
44
|
+
imageAlt = "",
|
|
45
|
+
rarity = "rare",
|
|
46
|
+
stats = [],
|
|
47
|
+
back,
|
|
48
|
+
flipped: controlled,
|
|
49
|
+
onFlip,
|
|
50
|
+
tilt = 14,
|
|
51
|
+
width = 240,
|
|
52
|
+
radius = 14,
|
|
53
|
+
cardColor = "#262b44",
|
|
54
|
+
backColor = "#29366f",
|
|
55
|
+
textColor = "#f4f4f4",
|
|
56
|
+
mutedColor = "#94b0c2",
|
|
57
|
+
accentColor = "#73eff7",
|
|
58
|
+
frameColor,
|
|
59
|
+
fontFamily = "inherit",
|
|
60
|
+
className = "",
|
|
61
|
+
style
|
|
62
|
+
}) {
|
|
63
|
+
useStyles();
|
|
64
|
+
const rootRef = useRef(null);
|
|
65
|
+
const [inner, setInner] = useState(false);
|
|
66
|
+
const flipped = controlled ?? inner;
|
|
67
|
+
const tier = RARITY[rarity] ?? RARITY.rare;
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const root = rootRef.current;
|
|
70
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return void 0;
|
|
71
|
+
let raf = 0;
|
|
72
|
+
const onMove = (event) => {
|
|
73
|
+
if (raf) return;
|
|
74
|
+
raf = requestAnimationFrame(() => {
|
|
75
|
+
raf = 0;
|
|
76
|
+
const r = root.getBoundingClientRect();
|
|
77
|
+
const x = (event.clientX - r.left) / r.width;
|
|
78
|
+
const y = (event.clientY - r.top) / r.height;
|
|
79
|
+
root.style.setProperty("--ry", `${((x - 0.5) * tilt * 2).toFixed(2)}deg`);
|
|
80
|
+
root.style.setProperty("--rx", `${((0.5 - y) * tilt * 2).toFixed(2)}deg`);
|
|
81
|
+
root.style.setProperty("--gx", `${(x * 100).toFixed(1)}%`);
|
|
82
|
+
root.style.setProperty("--gy", `${(y * 100).toFixed(1)}%`);
|
|
83
|
+
root.style.setProperty("--shift", ((x + y) / 2).toFixed(3));
|
|
84
|
+
root.dataset.tilting = "";
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
const onLeave = () => {
|
|
88
|
+
cancelAnimationFrame(raf);
|
|
89
|
+
raf = 0;
|
|
90
|
+
["--rx", "--ry", "--gx", "--gy", "--shift"].forEach((p) => root.style.removeProperty(p));
|
|
91
|
+
delete root.dataset.tilting;
|
|
92
|
+
};
|
|
93
|
+
root.addEventListener("pointermove", onMove);
|
|
94
|
+
root.addEventListener("pointerleave", onLeave);
|
|
95
|
+
return () => {
|
|
96
|
+
cancelAnimationFrame(raf);
|
|
97
|
+
root.removeEventListener("pointermove", onMove);
|
|
98
|
+
root.removeEventListener("pointerleave", onLeave);
|
|
99
|
+
};
|
|
100
|
+
}, [tilt]);
|
|
101
|
+
const flip = () => {
|
|
102
|
+
if (!back) return;
|
|
103
|
+
if (controlled === void 0) setInner((f) => !f);
|
|
104
|
+
onFlip?.(!flipped);
|
|
105
|
+
};
|
|
106
|
+
const face = /* @__PURE__ */ jsxs("span", { className: "bl-tcard__face", children: [
|
|
107
|
+
/* @__PURE__ */ jsxs("span", { className: "bl-tcard__head", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__name", children: name }),
|
|
109
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__rarity", children: rarity })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__art", children: image && /* @__PURE__ */ jsx("img", { src: image, alt: imageAlt, draggable: false }) }),
|
|
112
|
+
stats.length > 0 && // Spans, not a <dl>: the card can be a button, which only takes inline content.
|
|
113
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__stats", role: "list", children: stats.map((s) => /* @__PURE__ */ jsxs("span", { className: "bl-tcard__stat", role: "listitem", children: [
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__stat-label", children: s.label }),
|
|
115
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__stat-value", children: s.value })
|
|
116
|
+
] }, s.label)) }),
|
|
117
|
+
tier.foil > 0 && /* @__PURE__ */ jsx("span", { className: "bl-tcard__foil", "aria-hidden": "true" }),
|
|
118
|
+
/* @__PURE__ */ jsx("span", { className: "bl-tcard__glare", "aria-hidden": "true" })
|
|
119
|
+
] });
|
|
120
|
+
const Tag = back ? "button" : "div";
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
"div",
|
|
123
|
+
{
|
|
124
|
+
ref: rootRef,
|
|
125
|
+
className: `bl-tcard ${className}`,
|
|
126
|
+
"data-flippable": back ? "" : void 0,
|
|
127
|
+
style: { "--bl-w": `${width}px`, "--bl-radius": `${radius}px`, "--bl-card": cardColor, "--bl-back": backColor, "--bl-ink": textColor, "--bl-muted": mutedColor, "--bl-accent": accentColor, "--bl-frame": frameColor ?? tier.frame, "--bl-foil": tier.foil, "--turn": flipped ? "180deg" : "0deg", "--bl-font": fontFamily, ...style },
|
|
128
|
+
children: /* @__PURE__ */ jsxs(
|
|
129
|
+
Tag,
|
|
130
|
+
{
|
|
131
|
+
className: "bl-tcard__flip",
|
|
132
|
+
type: back ? "button" : void 0,
|
|
133
|
+
"aria-pressed": back ? flipped : void 0,
|
|
134
|
+
"aria-label": back ? `${name}, ${rarity} card. ${flipped ? "Showing the back" : "Showing the front"}; press to flip.` : void 0,
|
|
135
|
+
onClick: back ? flip : void 0,
|
|
136
|
+
children: [
|
|
137
|
+
face,
|
|
138
|
+
back && /* @__PURE__ */ jsx("span", { className: "bl-tcard__face bl-tcard__back", "aria-hidden": !flipped || void 0, children: back })
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
TradingCard as default
|
|
147
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
export interface VanishInputProps {
|
|
3
|
+
/** Suggestions that rotate while the field is empty. */
|
|
4
|
+
placeholders?: string[];
|
|
5
|
+
/** Time per placeholder, in ms. */
|
|
6
|
+
interval?: number;
|
|
7
|
+
/** Called with the trimmed text; the field then clears. */
|
|
8
|
+
onSubmit?: (text: string) => void;
|
|
9
|
+
onChange?: (text: string) => void;
|
|
10
|
+
/** Accessible name of the field. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Accessible name of the submit button. */
|
|
13
|
+
submitLabel?: string;
|
|
14
|
+
/** Particle size, in px. */
|
|
15
|
+
particleSize?: number;
|
|
16
|
+
/** Dissolve length, in ms. */
|
|
17
|
+
duration?: number;
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
radius?: number;
|
|
21
|
+
fontSize?: number;
|
|
22
|
+
/** Left text padding, in px. */
|
|
23
|
+
padding?: number;
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
textColor?: string;
|
|
26
|
+
mutedColor?: string;
|
|
27
|
+
lineColor?: string;
|
|
28
|
+
accentColor?: string;
|
|
29
|
+
accentTextColor?: string;
|
|
30
|
+
fontFamily?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
}
|
|
34
|
+
export default function VanishInput({ placeholders, interval, onSubmit, onChange, label, submitLabel, particleSize, duration, width, height, radius, fontSize, padding, backgroundColor, textColor, mutedColor, lineColor, accentColor, accentTextColor, fontFamily, className, style }: VanishInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useInsertionEffect, useRef, useState } from "react";
|
|
4
|
+
const STYLE_ID = "bl-vanish";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.bl-vanish { position: relative; display: flex; align-items: center; width: 100%; max-width: var(--bl-w); height: var(--bl-h); border-radius: var(--bl-radius); background: var(--bl-bg); box-shadow: inset 0 0 0 1px var(--bl-line), 0 10px 30px -18px rgba(0,0,0,0.6); font-family: var(--bl-font); transition: box-shadow 160ms; }
|
|
7
|
+
.bl-vanish:focus-within { box-shadow: inset 0 0 0 1.5px var(--bl-accent), 0 0 0 4px var(--bl-ring); }
|
|
8
|
+
.bl-vanish__field { position: relative; flex: 1; min-width: 0; height: 100%; overflow: hidden; }
|
|
9
|
+
.bl-vanish__input { width: 100%; height: 100%; padding: 0 0 0 var(--bl-pad); border: 0; background: transparent; color: var(--bl-ink); font: inherit; font-size: var(--bl-size); outline: none; }
|
|
10
|
+
.bl-vanish[data-vanishing] .bl-vanish__input { color: transparent; }
|
|
11
|
+
.bl-vanish__placeholder { position: absolute; left: var(--bl-pad); right: 8px; top: 50%; color: var(--bl-muted); font-size: var(--bl-size); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; transform: translateY(-50%); animation: bl-vanish-in 420ms cubic-bezier(.2,.9,.25,1); }
|
|
12
|
+
@keyframes bl-vanish-in { from { opacity: 0; transform: translateY(calc(-50% + 12px)); } }
|
|
13
|
+
.bl-vanish__canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
14
|
+
.bl-vanish__submit { flex: 0 0 auto; display: grid; place-items: center; width: calc(var(--bl-h) - 12px); height: calc(var(--bl-h) - 12px); margin-right: 6px; border: 0; border-radius: calc(var(--bl-radius) - 4px); background: var(--bl-accent); color: var(--bl-accent-ink); cursor: pointer; transition: opacity 160ms, transform 160ms; }
|
|
15
|
+
.bl-vanish__submit:disabled { opacity: 0.35; cursor: default; }
|
|
16
|
+
.bl-vanish__submit:not(:disabled):hover { transform: scale(1.05); }
|
|
17
|
+
.bl-vanish__submit:focus-visible { outline: 2px solid var(--bl-accent); outline-offset: 2px; }
|
|
18
|
+
@media (prefers-reduced-motion: reduce) { .bl-vanish__placeholder { animation: none; } .bl-vanish, .bl-vanish__submit { transition: none; } }
|
|
19
|
+
`;
|
|
20
|
+
function useStyles() {
|
|
21
|
+
useInsertionEffect(() => {
|
|
22
|
+
if (typeof document === "undefined") return;
|
|
23
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
24
|
+
style.textContent = CSS;
|
|
25
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
26
|
+
}, []);
|
|
27
|
+
}
|
|
28
|
+
const DEFAULT_PLACEHOLDERS = ["Search for a component\u2026", 'Try "pixel button"', "Find a background", "What are you building today?"];
|
|
29
|
+
function VanishInput({
|
|
30
|
+
placeholders = DEFAULT_PLACEHOLDERS,
|
|
31
|
+
interval = 3e3,
|
|
32
|
+
onSubmit,
|
|
33
|
+
onChange,
|
|
34
|
+
label = "Search",
|
|
35
|
+
submitLabel = "Submit",
|
|
36
|
+
particleSize = 2,
|
|
37
|
+
duration = 900,
|
|
38
|
+
width = 560,
|
|
39
|
+
height = 52,
|
|
40
|
+
radius = 26,
|
|
41
|
+
fontSize = 16,
|
|
42
|
+
padding = 20,
|
|
43
|
+
backgroundColor = "#1a1c2c",
|
|
44
|
+
textColor = "#f4f4f4",
|
|
45
|
+
mutedColor = "#94b0c2",
|
|
46
|
+
lineColor = "rgba(255,255,255,0.12)",
|
|
47
|
+
accentColor = "#73eff7",
|
|
48
|
+
accentTextColor = "#0b0b14",
|
|
49
|
+
fontFamily = "ui-sans-serif, system-ui, sans-serif",
|
|
50
|
+
className = "",
|
|
51
|
+
style
|
|
52
|
+
}) {
|
|
53
|
+
useStyles();
|
|
54
|
+
const [value, setValue] = useState("");
|
|
55
|
+
const [index, setIndex] = useState(0);
|
|
56
|
+
const [vanishing, setVanishing] = useState(false);
|
|
57
|
+
const inputRef = useRef(null);
|
|
58
|
+
const canvasRef = useRef(null);
|
|
59
|
+
const frame = useRef(0);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (placeholders.length < 2 || value) return void 0;
|
|
62
|
+
const timer = setInterval(() => setIndex((i) => (i + 1) % placeholders.length), interval);
|
|
63
|
+
return () => clearInterval(timer);
|
|
64
|
+
}, [placeholders.length, interval, value]);
|
|
65
|
+
useEffect(() => () => cancelAnimationFrame(frame.current), []);
|
|
66
|
+
const vanish = (text) => {
|
|
67
|
+
const input = inputRef.current;
|
|
68
|
+
const canvas = canvasRef.current;
|
|
69
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
70
|
+
if (reduced || !canvas) return;
|
|
71
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
72
|
+
const w = input.clientWidth;
|
|
73
|
+
const h = input.clientHeight;
|
|
74
|
+
canvas.width = Math.round(w * dpr);
|
|
75
|
+
canvas.height = Math.round(h * dpr);
|
|
76
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
77
|
+
const computed = getComputedStyle(input);
|
|
78
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
79
|
+
ctx.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
|
|
80
|
+
ctx.fillStyle = textColor;
|
|
81
|
+
ctx.textBaseline = "middle";
|
|
82
|
+
ctx.fillText(text, parseFloat(computed.paddingLeft) - input.scrollLeft, h / 2);
|
|
83
|
+
const step = Math.max(1, Math.round(particleSize * dpr));
|
|
84
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
85
|
+
const particles = [];
|
|
86
|
+
let maxX = 0;
|
|
87
|
+
for (let y = 0; y < canvas.height; y += step) {
|
|
88
|
+
for (let x = 0; x < canvas.width; x += step) {
|
|
89
|
+
const i = (y * canvas.width + x) * 4;
|
|
90
|
+
if (data[i + 3] < 100) continue;
|
|
91
|
+
particles.push({ x: x / dpr, y: y / dpr, vx: 0, vy: 0, alpha: 1, color: `rgb(${data[i]},${data[i + 1]},${data[i + 2]})` });
|
|
92
|
+
maxX = Math.max(maxX, x / dpr);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const size = step / dpr;
|
|
96
|
+
const start = performance.now();
|
|
97
|
+
const sweep = duration * 0.55;
|
|
98
|
+
const tick = (now) => {
|
|
99
|
+
const t = now - start;
|
|
100
|
+
ctx.clearRect(0, 0, w, h);
|
|
101
|
+
let alive = 0;
|
|
102
|
+
for (const p of particles) {
|
|
103
|
+
if (p.alpha <= 0) continue;
|
|
104
|
+
alive++;
|
|
105
|
+
const released = t > (maxX - p.x) / Math.max(1, maxX) * sweep;
|
|
106
|
+
if (released) {
|
|
107
|
+
if (!p.vx) {
|
|
108
|
+
p.vx = 0.6 + Math.random() * 1.6;
|
|
109
|
+
p.vy = (Math.random() - 0.5) * 1.6;
|
|
110
|
+
}
|
|
111
|
+
p.x += p.vx;
|
|
112
|
+
p.y += p.vy;
|
|
113
|
+
p.alpha -= 0.02 + Math.random() * 0.03;
|
|
114
|
+
}
|
|
115
|
+
ctx.globalAlpha = Math.max(0, p.alpha);
|
|
116
|
+
ctx.fillStyle = p.color;
|
|
117
|
+
ctx.fillRect(p.x, p.y, size, size);
|
|
118
|
+
}
|
|
119
|
+
ctx.globalAlpha = 1;
|
|
120
|
+
if (alive && t < duration * 2) frame.current = requestAnimationFrame(tick);
|
|
121
|
+
else {
|
|
122
|
+
ctx.clearRect(0, 0, w, h);
|
|
123
|
+
setVanishing(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
setVanishing(true);
|
|
127
|
+
cancelAnimationFrame(frame.current);
|
|
128
|
+
frame.current = requestAnimationFrame(tick);
|
|
129
|
+
};
|
|
130
|
+
const submit = (event) => {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
const text = value.trim();
|
|
133
|
+
if (!text || vanishing) return;
|
|
134
|
+
vanish(value);
|
|
135
|
+
onSubmit?.(text);
|
|
136
|
+
setValue("");
|
|
137
|
+
onChange?.("");
|
|
138
|
+
};
|
|
139
|
+
return /* @__PURE__ */ jsxs(
|
|
140
|
+
"form",
|
|
141
|
+
{
|
|
142
|
+
className: `bl-vanish ${className}`,
|
|
143
|
+
role: "search",
|
|
144
|
+
"data-vanishing": vanishing || void 0,
|
|
145
|
+
onSubmit: submit,
|
|
146
|
+
style: {
|
|
147
|
+
"--bl-w": `${width}px`,
|
|
148
|
+
"--bl-h": `${height}px`,
|
|
149
|
+
"--bl-radius": `${radius}px`,
|
|
150
|
+
"--bl-size": `${fontSize}px`,
|
|
151
|
+
"--bl-pad": `${padding}px`,
|
|
152
|
+
"--bl-bg": backgroundColor,
|
|
153
|
+
"--bl-ink": textColor,
|
|
154
|
+
"--bl-muted": mutedColor,
|
|
155
|
+
"--bl-line": lineColor,
|
|
156
|
+
"--bl-accent": accentColor,
|
|
157
|
+
"--bl-accent-ink": accentTextColor,
|
|
158
|
+
"--bl-ring": `color-mix(in srgb, ${accentColor} 22%, transparent)`,
|
|
159
|
+
"--bl-font": fontFamily,
|
|
160
|
+
...style
|
|
161
|
+
},
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsxs("div", { className: "bl-vanish__field", children: [
|
|
164
|
+
/* @__PURE__ */ jsx(
|
|
165
|
+
"input",
|
|
166
|
+
{
|
|
167
|
+
ref: inputRef,
|
|
168
|
+
className: "bl-vanish__input",
|
|
169
|
+
type: "text",
|
|
170
|
+
value,
|
|
171
|
+
onChange: (event) => {
|
|
172
|
+
setValue(event.target.value);
|
|
173
|
+
onChange?.(event.target.value);
|
|
174
|
+
},
|
|
175
|
+
"aria-label": label,
|
|
176
|
+
autoComplete: "off",
|
|
177
|
+
enterKeyHint: "send"
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
!value && !vanishing && placeholders.length > 0 && /* @__PURE__ */ jsx("span", { className: "bl-vanish__placeholder", "aria-hidden": "true", children: placeholders[index % placeholders.length] }, index),
|
|
181
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "bl-vanish__canvas", "aria-hidden": "true" })
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsx("button", { type: "submit", className: "bl-vanish__submit", disabled: !value.trim() || vanishing, "aria-label": submitLabel, children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 8h10M9 4l4 4-4 4" }) }) })
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
VanishInput as default
|
|
190
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
export interface VideoHeroProps {
|
|
3
|
+
/** A video file to play in the lightbox. */
|
|
4
|
+
src?: string;
|
|
5
|
+
/** An embed player URL (rendered in an iframe) instead of a file. */
|
|
6
|
+
embedUrl?: string;
|
|
7
|
+
/** Thumbnail image (also the video poster). */
|
|
8
|
+
poster?: string;
|
|
9
|
+
/** Custom thumbnail content instead of `poster`. */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
title?: ReactNode;
|
|
12
|
+
meta?: ReactNode;
|
|
13
|
+
/** Button name; defaults to "Play video: title". */
|
|
14
|
+
label?: string;
|
|
15
|
+
aspectRatio?: string;
|
|
16
|
+
/** Max thumbnail width, in px. */
|
|
17
|
+
width?: number;
|
|
18
|
+
radius?: number;
|
|
19
|
+
buttonSize?: number;
|
|
20
|
+
/** Play button leans toward the cursor. */
|
|
21
|
+
magnetic?: boolean;
|
|
22
|
+
/** Lightbox grow length, in ms. */
|
|
23
|
+
duration?: number;
|
|
24
|
+
accentColor?: string;
|
|
25
|
+
accentTextColor?: string;
|
|
26
|
+
backdropColor?: string;
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
className?: string;
|
|
30
|
+
style?: CSSProperties;
|
|
31
|
+
}
|
|
32
|
+
export default function VideoHero({ src, embedUrl, poster, children, title, meta, label, aspectRatio, width, radius, buttonSize, magnetic, duration, accentColor, accentTextColor, backdropColor, fontFamily, onOpenChange, className, style }: VideoHeroProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useInsertionEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
const STYLE_ID = "bl-video-hero";
|
|
6
|
+
const CSS = `
|
|
7
|
+
.bl-vhero { position: relative; display: block; width: 100%; max-width: var(--bl-w); aspect-ratio: var(--bl-ratio); padding: 0; overflow: hidden; border: 0; border-radius: var(--bl-radius); background: #000; color: #fff; font-family: var(--bl-font); cursor: pointer; box-shadow: 0 30px 60px -30px rgba(0,0,0,0.7); }
|
|
8
|
+
.bl-vhero:focus-visible { outline: 3px solid var(--bl-accent); outline-offset: 3px; }
|
|
9
|
+
.bl-vhero__poster { position: absolute; inset: 0; transition: transform 600ms cubic-bezier(.2,.9,.25,1), filter 300ms; }
|
|
10
|
+
.bl-vhero__poster > img, .bl-vhero__poster > video { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
11
|
+
.bl-vhero:hover .bl-vhero__poster { transform: scale(1.04); filter: brightness(0.8); }
|
|
12
|
+
.bl-vhero__shade { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.55), transparent 55%); }
|
|
13
|
+
.bl-vhero__play { position: absolute; left: 50%; top: 50%; display: grid; place-items: center; width: var(--bl-btn); height: var(--bl-btn); border-radius: 50%; background: var(--bl-accent); color: var(--bl-accent-ink); transform: translate(calc(-50% + var(--bl-px, 0px)), calc(-50% + var(--bl-py, 0px))) scale(var(--bl-ps, 1)); transition: transform 300ms cubic-bezier(.2,.9,.25,1.3); box-shadow: 0 10px 30px -8px rgba(0,0,0,0.6); }
|
|
14
|
+
.bl-vhero:hover .bl-vhero__play { --bl-ps: 1.1; }
|
|
15
|
+
.bl-vhero__play::before, .bl-vhero__play::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--bl-accent); animation: bl-vhero-ring 2.4s cubic-bezier(.2,.6,.3,1) infinite; }
|
|
16
|
+
.bl-vhero__play::after { animation-delay: 1.2s; }
|
|
17
|
+
.bl-vhero__caption { position: absolute; left: 18px; right: 18px; bottom: 16px; text-align: left; }
|
|
18
|
+
.bl-vhero__title { display: block; font-size: 18px; font-weight: 700; }
|
|
19
|
+
.bl-vhero__meta { display: block; margin-top: 3px; color: rgba(255,255,255,0.75); font-size: 13px; }
|
|
20
|
+
.bl-vhero-box__backdrop { position: fixed; inset: 0; z-index: 1400; background: var(--bl-backdrop); }
|
|
21
|
+
.bl-vhero-box { position: fixed; z-index: 1401; overflow: hidden; border-radius: var(--bl-radius); background: #000; box-shadow: 0 40px 100px -30px rgba(0,0,0,0.9); outline: none; }
|
|
22
|
+
.bl-vhero-box > video, .bl-vhero-box > iframe { display: block; width: 100%; height: 100%; border: 0; }
|
|
23
|
+
.bl-vhero-box__close { position: fixed; z-index: 1402; top: 16px; right: 16px; display: grid; place-items: center; width: 42px; height: 42px; border: 0; border-radius: 50%; background: rgba(255,255,255,0.14); color: #fff; cursor: pointer; }
|
|
24
|
+
.bl-vhero-box__close:focus-visible { outline: 2px solid var(--bl-accent); outline-offset: 2px; }
|
|
25
|
+
@keyframes bl-vhero-ring { from { transform: scale(1); opacity: 0.8; } to { transform: scale(1.9); opacity: 0; } }
|
|
26
|
+
@media (prefers-reduced-motion: reduce) { .bl-vhero__poster, .bl-vhero__play { transition: none; } .bl-vhero__play::before, .bl-vhero__play::after { animation: none; opacity: 0; } }
|
|
27
|
+
`;
|
|
28
|
+
function useStyles() {
|
|
29
|
+
useInsertionEffect(() => {
|
|
30
|
+
if (typeof document === "undefined") return;
|
|
31
|
+
const style = document.getElementById(STYLE_ID) ?? Object.assign(document.createElement("style"), { id: STYLE_ID });
|
|
32
|
+
style.textContent = CSS;
|
|
33
|
+
if (!style.isConnected) document.head.appendChild(style);
|
|
34
|
+
}, []);
|
|
35
|
+
}
|
|
36
|
+
const reduced = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
37
|
+
function VideoHero({
|
|
38
|
+
src,
|
|
39
|
+
embedUrl,
|
|
40
|
+
poster,
|
|
41
|
+
children,
|
|
42
|
+
title,
|
|
43
|
+
meta,
|
|
44
|
+
label,
|
|
45
|
+
aspectRatio = "16 / 9",
|
|
46
|
+
width = 720,
|
|
47
|
+
radius = 16,
|
|
48
|
+
buttonSize = 72,
|
|
49
|
+
magnetic = true,
|
|
50
|
+
duration = 520,
|
|
51
|
+
accentColor = "#f4f4f4",
|
|
52
|
+
accentTextColor = "#1a1c2c",
|
|
53
|
+
backdropColor = "rgba(6,6,12,0.82)",
|
|
54
|
+
fontFamily = "ui-sans-serif, system-ui, sans-serif",
|
|
55
|
+
onOpenChange,
|
|
56
|
+
className = "",
|
|
57
|
+
style
|
|
58
|
+
}) {
|
|
59
|
+
useStyles();
|
|
60
|
+
const triggerRef = useRef(null);
|
|
61
|
+
const boxRef = useRef(null);
|
|
62
|
+
const backdropRef = useRef(null);
|
|
63
|
+
const closeRef = useRef(null);
|
|
64
|
+
const [open, setOpen] = useState(false);
|
|
65
|
+
const [mounted, setMounted] = useState(false);
|
|
66
|
+
const closing = useRef(false);
|
|
67
|
+
const change = (next) => {
|
|
68
|
+
setOpen(next);
|
|
69
|
+
onOpenChange?.(next);
|
|
70
|
+
};
|
|
71
|
+
const target = () => {
|
|
72
|
+
const [w, h] = aspectRatio.split("/").map(Number);
|
|
73
|
+
const ratio = w / h || 16 / 9;
|
|
74
|
+
const maxW = Math.min(window.innerWidth - 32, 1100);
|
|
75
|
+
const maxH = window.innerHeight - 96;
|
|
76
|
+
const width2 = Math.min(maxW, maxH * ratio);
|
|
77
|
+
const height = width2 / ratio;
|
|
78
|
+
return { left: (window.innerWidth - width2) / 2, top: (window.innerHeight - height) / 2, width: width2, height };
|
|
79
|
+
};
|
|
80
|
+
const box = (r) => ({ left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (open) setMounted(true);
|
|
83
|
+
}, [open]);
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
if (!mounted || !open) return void 0;
|
|
86
|
+
const node = boxRef.current;
|
|
87
|
+
const from = triggerRef.current.getBoundingClientRect();
|
|
88
|
+
const to = target();
|
|
89
|
+
Object.assign(node.style, box(to));
|
|
90
|
+
if (!reduced()) node.animate([box(from), box(to)], { duration, easing: "cubic-bezier(.2,.9,.25,1)" });
|
|
91
|
+
backdropRef.current.animate({ opacity: [0, 1] }, { duration: duration * 0.6, fill: "both" });
|
|
92
|
+
const video = node.querySelector("video");
|
|
93
|
+
video?.play().catch(() => {
|
|
94
|
+
video.muted = true;
|
|
95
|
+
video.play().catch(() => {
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
closeRef.current?.focus({ preventScroll: true });
|
|
99
|
+
const previous = document.body.style.overflow;
|
|
100
|
+
document.body.style.overflow = "hidden";
|
|
101
|
+
const onResize = () => Object.assign(node.style, box(target()));
|
|
102
|
+
window.addEventListener("resize", onResize);
|
|
103
|
+
return () => {
|
|
104
|
+
document.body.style.overflow = previous;
|
|
105
|
+
window.removeEventListener("resize", onResize);
|
|
106
|
+
};
|
|
107
|
+
}, [mounted, open]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (open || !mounted || closing.current) return;
|
|
110
|
+
closing.current = true;
|
|
111
|
+
const node = boxRef.current;
|
|
112
|
+
const finish = () => {
|
|
113
|
+
closing.current = false;
|
|
114
|
+
setMounted(false);
|
|
115
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
116
|
+
};
|
|
117
|
+
node.querySelector("video")?.pause();
|
|
118
|
+
backdropRef.current.animate({ opacity: [1, 0] }, { duration: duration * 0.6, fill: "forwards" });
|
|
119
|
+
if (reduced()) return finish();
|
|
120
|
+
const from = node.getBoundingClientRect();
|
|
121
|
+
node.animate([box(from), box(triggerRef.current.getBoundingClientRect())], { duration: duration * 0.85, easing: "cubic-bezier(.4,0,.2,1)", fill: "forwards" }).onfinish = finish;
|
|
122
|
+
}, [open, mounted]);
|
|
123
|
+
const onPointerMove = (event) => {
|
|
124
|
+
if (!magnetic || reduced()) return;
|
|
125
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
126
|
+
const x = (event.clientX - rect.left) / rect.width - 0.5;
|
|
127
|
+
const y = (event.clientY - rect.top) / rect.height - 0.5;
|
|
128
|
+
event.currentTarget.style.setProperty("--bl-px", `${x * 40}px`);
|
|
129
|
+
event.currentTarget.style.setProperty("--bl-py", `${y * 30}px`);
|
|
130
|
+
};
|
|
131
|
+
const onPointerLeave = (event) => {
|
|
132
|
+
event.currentTarget.style.removeProperty("--bl-px");
|
|
133
|
+
event.currentTarget.style.removeProperty("--bl-py");
|
|
134
|
+
};
|
|
135
|
+
const vars = {
|
|
136
|
+
"--bl-w": `${width}px`,
|
|
137
|
+
"--bl-ratio": aspectRatio,
|
|
138
|
+
"--bl-radius": `${radius}px`,
|
|
139
|
+
"--bl-btn": `${buttonSize}px`,
|
|
140
|
+
"--bl-accent": accentColor,
|
|
141
|
+
"--bl-accent-ink": accentTextColor,
|
|
142
|
+
"--bl-backdrop": backdropColor,
|
|
143
|
+
"--bl-font": fontFamily
|
|
144
|
+
};
|
|
145
|
+
const name = label ?? (typeof title === "string" ? `Play video: ${title}` : "Play video");
|
|
146
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
147
|
+
/* @__PURE__ */ jsxs(
|
|
148
|
+
"button",
|
|
149
|
+
{
|
|
150
|
+
ref: triggerRef,
|
|
151
|
+
type: "button",
|
|
152
|
+
className: `bl-vhero ${className}`,
|
|
153
|
+
"aria-label": name,
|
|
154
|
+
"aria-haspopup": "dialog",
|
|
155
|
+
onClick: () => change(true),
|
|
156
|
+
onPointerMove,
|
|
157
|
+
onPointerLeave,
|
|
158
|
+
style: { ...vars, ...style },
|
|
159
|
+
children: [
|
|
160
|
+
/* @__PURE__ */ jsx("span", { className: "bl-vhero__poster", "aria-hidden": "true", children: children ?? (poster ? /* @__PURE__ */ jsx("img", { src: poster, alt: "" }) : null) }),
|
|
161
|
+
(title || meta) && /* @__PURE__ */ jsx("span", { className: "bl-vhero__shade", "aria-hidden": "true" }),
|
|
162
|
+
/* @__PURE__ */ jsx("span", { className: "bl-vhero__play", "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { width: buttonSize * 0.34, height: buttonSize * 0.34, viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M7 4.5v15l13-7.5z" }) }) }),
|
|
163
|
+
(title || meta) && /* @__PURE__ */ jsxs("span", { className: "bl-vhero__caption", "aria-hidden": "true", children: [
|
|
164
|
+
title && /* @__PURE__ */ jsx("span", { className: "bl-vhero__title", children: title }),
|
|
165
|
+
meta && /* @__PURE__ */ jsx("span", { className: "bl-vhero__meta", children: meta })
|
|
166
|
+
] })
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
),
|
|
170
|
+
mounted && createPortal(
|
|
171
|
+
/* @__PURE__ */ jsxs("div", { style: vars, children: [
|
|
172
|
+
/* @__PURE__ */ jsx("div", { ref: backdropRef, className: "bl-vhero-box__backdrop", onClick: () => change(false) }),
|
|
173
|
+
/* @__PURE__ */ jsxs(
|
|
174
|
+
"div",
|
|
175
|
+
{
|
|
176
|
+
ref: boxRef,
|
|
177
|
+
className: "bl-vhero-box",
|
|
178
|
+
role: "dialog",
|
|
179
|
+
"aria-modal": "true",
|
|
180
|
+
"aria-label": typeof title === "string" ? title : "Video",
|
|
181
|
+
tabIndex: -1,
|
|
182
|
+
onKeyDown: (event) => {
|
|
183
|
+
if (event.key === "Escape") {
|
|
184
|
+
event.stopPropagation();
|
|
185
|
+
change(false);
|
|
186
|
+
}
|
|
187
|
+
if (event.key === "Tab") {
|
|
188
|
+
const items = [closeRef.current, boxRef.current.querySelector("video, iframe")].filter(Boolean);
|
|
189
|
+
const index = items.indexOf(document.activeElement);
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
items[(index + (event.shiftKey ? -1 : 1) + items.length) % items.length]?.focus();
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
children: [
|
|
195
|
+
/* @__PURE__ */ jsx("button", { ref: closeRef, type: "button", className: "bl-vhero-box__close", "aria-label": "Close video", onClick: () => change(false), children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 3l10 10M13 3 3 13" }) }) }),
|
|
196
|
+
embedUrl ? /* @__PURE__ */ jsx("iframe", { src: embedUrl, title: typeof title === "string" ? title : "Video", allow: "autoplay; fullscreen; picture-in-picture; encrypted-media", allowFullScreen: true }) : /* @__PURE__ */ jsx("video", { src, poster, controls: true, autoPlay: true, playsInline: true })
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
] }),
|
|
201
|
+
document.body
|
|
202
|
+
)
|
|
203
|
+
] });
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
VideoHero as default
|
|
207
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
export type OrbState = 'idle' | 'listening' | 'thinking' | 'speaking';
|
|
3
|
+
export interface VoiceOrbProps {
|
|
4
|
+
state?: OrbState;
|
|
5
|
+
/** With "mic", the microphone opens only while listening and is released right after. */
|
|
6
|
+
source?: 'simulated' | 'mic';
|
|
7
|
+
/** Drive the orb yourself (0–1), e.g. from your TTS audio. Overrides the source. */
|
|
8
|
+
level?: number;
|
|
9
|
+
/** Diameter, in px. */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** Gradient stops; they rotate. */
|
|
12
|
+
colors?: string[];
|
|
13
|
+
glowColor?: string;
|
|
14
|
+
/** Level gain. */
|
|
15
|
+
sensitivity?: number;
|
|
16
|
+
/** Accessible name, combined with the state. */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Called if the microphone is denied or missing (falls back to a simulated voice). */
|
|
19
|
+
onMicError?: (error: unknown) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
style?: CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
export default function VoiceOrb({ state, source, level, size, colors, glowColor, sensitivity, label, onMicError, className, style }: VoiceOrbProps): import("react").JSX.Element;
|