@reactberry/system 2.0.0-beta
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/README.md +48 -0
- package/package.json +74 -0
- package/src/blocks/Accordion/index.tsx +158 -0
- package/src/blocks/AnimatedCarousel/index.tsx +188 -0
- package/src/blocks/AppleGlow/index.tsx +144 -0
- package/src/blocks/Avatar/index.tsx +167 -0
- package/src/blocks/Await/index.tsx +45 -0
- package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
- package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
- package/src/blocks/Cards/InfoCard/index.tsx +206 -0
- package/src/blocks/Cards/TickerCard/index.tsx +125 -0
- package/src/blocks/Carousel/index.tsx +216 -0
- package/src/blocks/Checkbox/index.tsx +101 -0
- package/src/blocks/Collection/index.tsx +59 -0
- package/src/blocks/Container/index.tsx +55 -0
- package/src/blocks/Controls/Control.tsx +67 -0
- package/src/blocks/Controls/index.tsx +11 -0
- package/src/blocks/CyclingNumber/index.tsx +78 -0
- package/src/blocks/DisplaySet/index.tsx +42 -0
- package/src/blocks/Divider/index.tsx +14 -0
- package/src/blocks/Draggable/index.tsx +266 -0
- package/src/blocks/Drawer/index.tsx +136 -0
- package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
- package/src/blocks/DynamicIsland/index.tsx +2 -0
- package/src/blocks/Fader/index.tsx +145 -0
- package/src/blocks/FamilyDrawer/README.md +116 -0
- package/src/blocks/FamilyDrawer/example.tsx +108 -0
- package/src/blocks/FamilyDrawer/index.tsx +119 -0
- package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
- package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
- package/src/blocks/FieldSet/index.tsx +173 -0
- package/src/blocks/Filesystem/index.tsx +198 -0
- package/src/blocks/Gallery/Carousel/index.tsx +257 -0
- package/src/blocks/Gallery/Modal/index.tsx +83 -0
- package/src/blocks/Gallery/index.tsx +57 -0
- package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
- package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
- package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
- package/src/blocks/Gallery/utils/range.ts +11 -0
- package/src/blocks/GradientMesh/index.tsx +106 -0
- package/src/blocks/Group/index.tsx +152 -0
- package/src/blocks/Heading/index.tsx +111 -0
- package/src/blocks/HorizontalScroller/index.tsx +135 -0
- package/src/blocks/Icon/index.tsx +45 -0
- package/src/blocks/Indicator/index.tsx +27 -0
- package/src/blocks/InlineEditor/index.tsx +216 -0
- package/src/blocks/List/index.tsx +657 -0
- package/src/blocks/Main/index.tsx +17 -0
- package/src/blocks/Marquee/index.tsx +116 -0
- package/src/blocks/MaskedField/index.tsx +199 -0
- package/src/blocks/Menu/MenuContent.tsx +246 -0
- package/src/blocks/Menu/MenuContext.tsx +34 -0
- package/src/blocks/Menu/MenuItem.tsx +104 -0
- package/src/blocks/Menu/index.tsx +60 -0
- package/src/blocks/Modal/index.tsx +268 -0
- package/src/blocks/MorphingPopover/index.tsx +294 -0
- package/src/blocks/Overlay/Backdrop.tsx +48 -0
- package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
- package/src/blocks/Overlay/index.ts +2 -0
- package/src/blocks/Parallax/index.tsx +117 -0
- package/src/blocks/ParallaxSection/index.tsx +61 -0
- package/src/blocks/Placeholder/index.tsx +48 -0
- package/src/blocks/Popover/index.tsx +402 -0
- package/src/blocks/Progress/getProgressColor.ts +61 -0
- package/src/blocks/Progress/index.tsx +179 -0
- package/src/blocks/ProgressiveBlur/index.tsx +75 -0
- package/src/blocks/README.md +15 -0
- package/src/blocks/RenderAsset/index.tsx +18 -0
- package/src/blocks/ScrollContainer/index.tsx +93 -0
- package/src/blocks/ShinyText/index.tsx +72 -0
- package/src/blocks/Skeleton/index.tsx +71 -0
- package/src/blocks/Slider/SliderControls.tsx +119 -0
- package/src/blocks/Slider/index.tsx +140 -0
- package/src/blocks/Slider/useSlider.ts +126 -0
- package/src/blocks/Slideshow/index.tsx +177 -0
- package/src/blocks/Spotlight/index.tsx +144 -0
- package/src/blocks/Steps/StepIndicator.tsx +149 -0
- package/src/blocks/Steps/StepProgress.tsx +164 -0
- package/src/blocks/Steps/Steps.tsx +197 -0
- package/src/blocks/Steps/StepsNav.tsx +30 -0
- package/src/blocks/Steps/StepsTracker.tsx +80 -0
- package/src/blocks/Steps/hooks.ts +71 -0
- package/src/blocks/Steps/index.tsx +16 -0
- package/src/blocks/Steps/types.ts +71 -0
- package/src/blocks/StickySectionStack/index.tsx +136 -0
- package/src/blocks/Switch/index.tsx +85 -0
- package/src/blocks/SystemNotice/index.tsx +81 -0
- package/src/blocks/Table/README.md +251 -0
- package/src/blocks/Table/Table.tsx +207 -0
- package/src/blocks/Table/TablePagination.tsx +189 -0
- package/src/blocks/Table/index.ts +33 -0
- package/src/blocks/Table/useTableControls.ts +331 -0
- package/src/blocks/Tag/index.tsx +27 -0
- package/src/blocks/TextBreak/index.tsx +96 -0
- package/src/blocks/TextReveal/index.tsx +104 -0
- package/src/blocks/Thumbnail/index.tsx +26 -0
- package/src/blocks/Ticker/index.tsx +112 -0
- package/src/blocks/Toast/index.tsx +77 -0
- package/src/blocks/Tooltip/index.tsx +174 -0
- package/src/blocks/Underlay/index.tsx +104 -0
- package/src/blocks/Upload/Dropzone.tsx +92 -0
- package/src/blocks/Upload/UploadBtn.tsx +38 -0
- package/src/blocks/Upload/index.tsx +61 -0
- package/src/blocks/Upload/types.ts +37 -0
- package/src/blocks/VideoMarquee/index.tsx +511 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/pagination/Pagination.tsx +148 -0
- package/src/blocks/pagination/PaginationList.tsx +41 -0
- package/src/blocks/pagination/index.ts +2 -0
- package/src/charts/BarChart.tsx +63 -0
- package/src/charts/PieChart.tsx +39 -0
- package/src/charts/index.ts +3 -0
- package/src/charts/utils.ts +103 -0
- package/src/docs/README.md +373 -0
- package/src/docs/reference/README.md +299 -0
- package/src/elements/box.ts +163 -0
- package/src/elements/button.ts +49 -0
- package/src/elements/field.ts +129 -0
- package/src/elements/index.ts +8 -0
- package/src/elements/text.ts +47 -0
- package/src/elements/utils.js +97 -0
- package/src/hooks/use-copy-to-clipboard.tsx +33 -0
- package/src/hooks/use-enter-submit.tsx +23 -0
- package/src/hooks/use-local-storage.ts +42 -0
- package/src/hooks/use-sidebar.tsx +109 -0
- package/src/hooks/useAnimatedText.ts +32 -0
- package/src/hooks/useAutosizeTextArea.ts +45 -0
- package/src/hooks/useBreakpoint.tsx +123 -0
- package/src/hooks/useClickOutside.tsx +38 -0
- package/src/hooks/useHover.tsx +33 -0
- package/src/hooks/useHoverList.tsx +17 -0
- package/src/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/hooks/useKeypress.ts +27 -0
- package/src/hooks/useOverlay.ts +32 -0
- package/src/hooks/useReducedMotion.ts +25 -0
- package/src/hooks/useStandaloneMode.ts +35 -0
- package/src/hooks/useTouchDevice.ts +34 -0
- package/src/icons/index.tsx +129 -0
- package/src/index.ts +12 -0
- package/src/providers/DesignSystemProvider.tsx +35 -0
- package/src/providers/StyledComponentsRegistry.tsx +30 -0
- package/src/providers/index.ts +2 -0
- package/src/themes/README.md +30 -0
- package/src/themes/default/assets/badge-avatar.tsx +45 -0
- package/src/themes/default/assets/logo.tsx +42 -0
- package/src/themes/default/global.ts +138 -0
- package/src/themes/default/modes/dark/config.js +49 -0
- package/src/themes/default/modes/dark/skins.js +631 -0
- package/src/themes/default/modes/dark/theme.js +87 -0
- package/src/themes/default/modes/light/config.js +48 -0
- package/src/themes/default/modes/light/skins.js +1026 -0
- package/src/themes/default/modes/light/theme.js +74 -0
- package/src/themes/default/tokens/controls.js +53 -0
- package/src/themes/default/tokens/shadows.js +63 -0
- package/src/themes/default/tokens/shapes.js +37 -0
- package/src/themes/default/tokens/space.js +143 -0
- package/src/themes/default/tokens/spectre.js +16 -0
- package/src/themes/default/utils.js +523 -0
- package/src/themes/index.ts +11 -0
- package/src/types.ts +394 -0
- package/src/utils/overlayTheme.ts +61 -0
- package/src/utils/pickColor.ts +15 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box } from "@/design-system/elements";
|
|
3
|
+
import { motion, useInView, useScroll } from "motion/react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
|
+
|
|
7
|
+
interface ImageData {
|
|
8
|
+
id: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CardProps {
|
|
13
|
+
imgUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ParallaxProps {
|
|
17
|
+
images: ImageData[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Card = ({ imgUrl }: CardProps) => {
|
|
21
|
+
// Definition for sticky position of the card
|
|
22
|
+
const vertMargin = 10;
|
|
23
|
+
|
|
24
|
+
// Ref for container
|
|
25
|
+
const container = useRef(null);
|
|
26
|
+
|
|
27
|
+
// State vars
|
|
28
|
+
const [maxScrollY, setMaxScrollY] = useState(Infinity);
|
|
29
|
+
const [dynamicStyles, setDynamicStyles] = useState({
|
|
30
|
+
scale: 1,
|
|
31
|
+
filter: 0,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Framer Motion helpers
|
|
35
|
+
const { scrollY } = useScroll({
|
|
36
|
+
target: container,
|
|
37
|
+
});
|
|
38
|
+
const isInView = useInView(container, {
|
|
39
|
+
// Fix: Use proper MarginType format
|
|
40
|
+
margin: `0px 0px -${100 - vertMargin}% 0px` as any,
|
|
41
|
+
once: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Scroll tracking
|
|
45
|
+
scrollY.on("change", (scrollY) => {
|
|
46
|
+
// animationValue indicates progress after container hits sticky point, going from 1 to 0
|
|
47
|
+
let animationValue = 1;
|
|
48
|
+
if (scrollY > maxScrollY) {
|
|
49
|
+
animationValue = Math.max(0, 1 - (scrollY - maxScrollY) / 10000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setDynamicStyles({
|
|
53
|
+
scale: animationValue,
|
|
54
|
+
filter: (1 - animationValue) * 100,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isInView) {
|
|
60
|
+
setMaxScrollY(scrollY.get());
|
|
61
|
+
}
|
|
62
|
+
}, [isInView, scrollY]); // Fix: Add scrollY to dependency array
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Box
|
|
66
|
+
as={motion.div}
|
|
67
|
+
ref={container}
|
|
68
|
+
position={"sticky"}
|
|
69
|
+
width={"100%"}
|
|
70
|
+
bg="neutral"
|
|
71
|
+
overflow={"hidden"}
|
|
72
|
+
shape="roundedLarge"
|
|
73
|
+
$shadow="medium"
|
|
74
|
+
style={{
|
|
75
|
+
scale: dynamicStyles.scale,
|
|
76
|
+
filter: `blur(${dynamicStyles.filter}px)`,
|
|
77
|
+
height: `${100 - 2 * vertMargin}vh`,
|
|
78
|
+
top: `${vertMargin}vh`,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<Box position="relative" zIndex={22} p="medium">
|
|
82
|
+
<h1>Card</h1>
|
|
83
|
+
</Box>
|
|
84
|
+
<Image
|
|
85
|
+
src={imgUrl}
|
|
86
|
+
alt={imgUrl}
|
|
87
|
+
fill
|
|
88
|
+
sizes="100vw"
|
|
89
|
+
style={{ objectFit: "cover" }}
|
|
90
|
+
/>
|
|
91
|
+
</Box>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default function Parallax({ images }: ParallaxProps) {
|
|
96
|
+
return (
|
|
97
|
+
<Box
|
|
98
|
+
display="flex"
|
|
99
|
+
flexDirection="column"
|
|
100
|
+
alignItems={"center"}
|
|
101
|
+
minHeight={"100vh"}
|
|
102
|
+
>
|
|
103
|
+
<Box
|
|
104
|
+
position={"relative"}
|
|
105
|
+
display="flex"
|
|
106
|
+
flexDirection="column"
|
|
107
|
+
gap="10vh"
|
|
108
|
+
py="10vh"
|
|
109
|
+
width={"100%"}
|
|
110
|
+
>
|
|
111
|
+
{images.map((img) => (
|
|
112
|
+
<Card key={img.id} imgUrl={img.url} />
|
|
113
|
+
))}
|
|
114
|
+
</Box>
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { motion, useScroll, useTransform } from "motion/react";
|
|
3
|
+
import React, { useRef } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { Box } from "@/design-system/elements";
|
|
6
|
+
|
|
7
|
+
interface ParallaxSectionProps {
|
|
8
|
+
image: string;
|
|
9
|
+
content?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ParallaxSection: React.FC<ParallaxSectionProps> = ({
|
|
13
|
+
image,
|
|
14
|
+
content,
|
|
15
|
+
}) => {
|
|
16
|
+
const sectionRef = useRef(null);
|
|
17
|
+
const { scrollYProgress } = useScroll({
|
|
18
|
+
target: sectionRef,
|
|
19
|
+
offset: ["start end", "end start"],
|
|
20
|
+
});
|
|
21
|
+
const y = useTransform(scrollYProgress, [0, 1], ["-20%", "20%"]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Box
|
|
25
|
+
ref={sectionRef}
|
|
26
|
+
as="section"
|
|
27
|
+
position="relative"
|
|
28
|
+
height="100vh"
|
|
29
|
+
overflow="hidden"
|
|
30
|
+
>
|
|
31
|
+
{content}
|
|
32
|
+
<Box
|
|
33
|
+
as={motion.div}
|
|
34
|
+
position="absolute"
|
|
35
|
+
width="100%"
|
|
36
|
+
height="120%"
|
|
37
|
+
zIndex={0}
|
|
38
|
+
style={{ top: y }}
|
|
39
|
+
>
|
|
40
|
+
<Box
|
|
41
|
+
position="absolute"
|
|
42
|
+
width={"100%"}
|
|
43
|
+
height={"100%"}
|
|
44
|
+
top="0"
|
|
45
|
+
left={"0"}
|
|
46
|
+
zIndex={10}
|
|
47
|
+
bg="rgba(0, 0, 0, 0.3)"
|
|
48
|
+
/>
|
|
49
|
+
<Image
|
|
50
|
+
src={image}
|
|
51
|
+
alt="Overview"
|
|
52
|
+
fill
|
|
53
|
+
sizes="100vh"
|
|
54
|
+
style={{ objectFit: "cover" }}
|
|
55
|
+
/>
|
|
56
|
+
</Box>
|
|
57
|
+
</Box>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default ParallaxSection;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "@/design-system/elements";
|
|
4
|
+
import Icon from "../Icon";
|
|
5
|
+
|
|
6
|
+
interface PlaceholderProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
icon?: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
iconProps?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Placeholder: React.FC<PlaceholderProps> = ({
|
|
14
|
+
children,
|
|
15
|
+
icon = "IconEmpty",
|
|
16
|
+
iconProps,
|
|
17
|
+
...props
|
|
18
|
+
}) => {
|
|
19
|
+
return (
|
|
20
|
+
<Text
|
|
21
|
+
as="div"
|
|
22
|
+
display="flex"
|
|
23
|
+
flexDirection={"column"}
|
|
24
|
+
justifyContent="center"
|
|
25
|
+
alignItems="center"
|
|
26
|
+
fontSize="small"
|
|
27
|
+
p="medium"
|
|
28
|
+
gap="s"
|
|
29
|
+
{...props}
|
|
30
|
+
color="tertiary"
|
|
31
|
+
>
|
|
32
|
+
{icon && (
|
|
33
|
+
<Box
|
|
34
|
+
as={Icon}
|
|
35
|
+
color={"palette.neutrals.7"}
|
|
36
|
+
size="2.25rem"
|
|
37
|
+
flex="none"
|
|
38
|
+
icon={icon}
|
|
39
|
+
{...iconProps}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
{children ? <>{children}</> : null}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Placeholder;
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box, Button } from "@/design-system/elements";
|
|
3
|
+
import {
|
|
4
|
+
Popover as HeadlessPopover,
|
|
5
|
+
PopoverPanel,
|
|
6
|
+
PopoverButton,
|
|
7
|
+
Portal,
|
|
8
|
+
} from "@headlessui/react";
|
|
9
|
+
import {
|
|
10
|
+
useFloating,
|
|
11
|
+
offset,
|
|
12
|
+
flip,
|
|
13
|
+
shift,
|
|
14
|
+
autoUpdate,
|
|
15
|
+
type Placement as FloatingPlacement,
|
|
16
|
+
} from "@floating-ui/react";
|
|
17
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
18
|
+
import { forwardRef, useState, useRef, useEffect } from "react";
|
|
19
|
+
import type { HTMLMotionProps } from "motion/react";
|
|
20
|
+
|
|
21
|
+
const MotionBox = forwardRef<HTMLDivElement, HTMLMotionProps<"div"> & any>(
|
|
22
|
+
function MotionBox(props, ref) {
|
|
23
|
+
return <Box as={motion.div} ref={ref} {...props} />;
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
MotionBox.displayName = "MotionBox";
|
|
28
|
+
|
|
29
|
+
type PanelProps = {
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
usePortal?: boolean;
|
|
32
|
+
panelRef?: (node: HTMLDivElement | null) => void;
|
|
33
|
+
floatingStyles?: React.CSSProperties;
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function Panel({
|
|
38
|
+
children,
|
|
39
|
+
usePortal = false,
|
|
40
|
+
panelRef,
|
|
41
|
+
floatingStyles,
|
|
42
|
+
...props
|
|
43
|
+
}: PanelProps) {
|
|
44
|
+
const { style: styleFromProps, ...restProps } = props;
|
|
45
|
+
const combinedStyles = {
|
|
46
|
+
...(floatingStyles || {}),
|
|
47
|
+
...(styleFromProps || {}),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const panelContent = (
|
|
51
|
+
<PopoverPanel
|
|
52
|
+
static
|
|
53
|
+
as={MotionBox}
|
|
54
|
+
ref={panelRef}
|
|
55
|
+
{...restProps}
|
|
56
|
+
style={combinedStyles}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</PopoverPanel>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return usePortal ? <Portal>{panelContent}</Portal> : panelContent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalizePlacement = (placement: string): FloatingPlacement => {
|
|
66
|
+
const trimmed = placement.trim();
|
|
67
|
+
// Support both "bottom start" and "bottom-start" style values
|
|
68
|
+
return trimmed.replace(/\s+/g, "-") as FloatingPlacement;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function mergeRefs<T = any>(
|
|
72
|
+
...refs: Array<
|
|
73
|
+
((instance: T | null) => void) | { current: T | null } | null | undefined
|
|
74
|
+
>
|
|
75
|
+
) {
|
|
76
|
+
return (value: T | null) => {
|
|
77
|
+
refs.forEach((ref) => {
|
|
78
|
+
if (!ref) return;
|
|
79
|
+
if (typeof ref === "function") {
|
|
80
|
+
ref(value);
|
|
81
|
+
} else {
|
|
82
|
+
(ref as { current: T | null }).current = value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type PopoverProps = {
|
|
89
|
+
children?:
|
|
90
|
+
| React.ReactNode
|
|
91
|
+
| ((props: { close: () => void; open: boolean }) => React.ReactNode);
|
|
92
|
+
trigger?: string | React.ReactNode;
|
|
93
|
+
placement?: string;
|
|
94
|
+
triggerProps?: any;
|
|
95
|
+
containerProps?: any;
|
|
96
|
+
panelProps?: any;
|
|
97
|
+
triggerMode?: "click" | "hover";
|
|
98
|
+
hoverDelay?: number; // Delay in ms before showing/hiding on hover
|
|
99
|
+
usePortal?: boolean; // Whether to render the popover in a portal
|
|
100
|
+
scrollContainer?: HTMLElement | null; // Optional scroll container that should close the popover on scroll
|
|
101
|
+
renderTrigger?: (props: {
|
|
102
|
+
ref: (node: HTMLElement | null) => void;
|
|
103
|
+
onClick?: () => void;
|
|
104
|
+
}) => React.ReactNode; // Custom trigger renderer for full control
|
|
105
|
+
onOpenChange?: (isOpen: boolean) => void; // Callback when popover open state changes
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default function Popover({
|
|
109
|
+
children,
|
|
110
|
+
trigger,
|
|
111
|
+
triggerProps,
|
|
112
|
+
placement = "bottom end",
|
|
113
|
+
containerProps,
|
|
114
|
+
panelProps,
|
|
115
|
+
triggerMode = "click",
|
|
116
|
+
hoverDelay = 200,
|
|
117
|
+
usePortal = false, // Default to not using portal
|
|
118
|
+
scrollContainer = null,
|
|
119
|
+
renderTrigger,
|
|
120
|
+
onOpenChange,
|
|
121
|
+
}: PopoverProps) {
|
|
122
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
123
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
124
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
125
|
+
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
126
|
+
const isOpenRef = useRef(false);
|
|
127
|
+
const prevOpenRef = useRef(false);
|
|
128
|
+
|
|
129
|
+
const normalizedPlacement = normalizePlacement(placement);
|
|
130
|
+
|
|
131
|
+
const { refs, x, y, strategy } = useFloating({
|
|
132
|
+
placement: normalizedPlacement,
|
|
133
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
134
|
+
strategy: usePortal ? "fixed" : "absolute",
|
|
135
|
+
whileElementsMounted: autoUpdate,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const floatingStyles: React.CSSProperties = {
|
|
139
|
+
position: strategy,
|
|
140
|
+
top: y ?? 0,
|
|
141
|
+
left: x ?? 0,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const triggerRefProp = triggerProps?.ref;
|
|
145
|
+
const { ref: containerRefProp, ...restContainerProps } = containerProps ?? {};
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
// For hover-triggered popovers without an explicit scrollContainer,
|
|
149
|
+
// we do NOT auto-close on window scroll. This avoids flicker when
|
|
150
|
+
// the page scrolls or reflows right as the user hovers.
|
|
151
|
+
if (triggerMode === "hover" && !scrollContainer) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleScroll = () => {
|
|
156
|
+
if (triggerMode === "hover") {
|
|
157
|
+
if (timeoutRef.current) {
|
|
158
|
+
clearTimeout(timeoutRef.current);
|
|
159
|
+
}
|
|
160
|
+
setIsHovering(false);
|
|
161
|
+
} else {
|
|
162
|
+
if (isOpenRef.current && buttonRef.current) {
|
|
163
|
+
// Close the popover by toggling the trigger button
|
|
164
|
+
buttonRef.current.click();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const target: HTMLElement | Window = scrollContainer ?? window;
|
|
170
|
+
target.addEventListener("scroll", handleScroll as EventListener, {
|
|
171
|
+
passive: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
if (timeoutRef.current) {
|
|
176
|
+
clearTimeout(timeoutRef.current);
|
|
177
|
+
}
|
|
178
|
+
target.removeEventListener("scroll", handleScroll as EventListener);
|
|
179
|
+
};
|
|
180
|
+
}, [triggerMode, scrollContainer]);
|
|
181
|
+
|
|
182
|
+
// Handle mouse enter
|
|
183
|
+
const handleMouseEnter = () => {
|
|
184
|
+
if (triggerMode !== "hover") return;
|
|
185
|
+
|
|
186
|
+
if (timeoutRef.current) {
|
|
187
|
+
clearTimeout(timeoutRef.current);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
timeoutRef.current = setTimeout(() => {
|
|
191
|
+
setIsHovering(true);
|
|
192
|
+
}, hoverDelay);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Handle mouse leave
|
|
196
|
+
const handleMouseLeave = () => {
|
|
197
|
+
if (triggerMode !== "hover") return;
|
|
198
|
+
|
|
199
|
+
if (timeoutRef.current) {
|
|
200
|
+
clearTimeout(timeoutRef.current);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
timeoutRef.current = setTimeout(() => {
|
|
204
|
+
setIsHovering(false);
|
|
205
|
+
}, hoverDelay);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Click mode popover
|
|
209
|
+
if (triggerMode === "click") {
|
|
210
|
+
return (
|
|
211
|
+
<Box as={HeadlessPopover} position="relative" {...restContainerProps}>
|
|
212
|
+
{({ open, close }: { open: boolean; close: () => void }) => {
|
|
213
|
+
isOpenRef.current = open;
|
|
214
|
+
// Notify parent of open state changes synchronously
|
|
215
|
+
if (prevOpenRef.current !== open) {
|
|
216
|
+
prevOpenRef.current = open;
|
|
217
|
+
onOpenChange?.(open);
|
|
218
|
+
}
|
|
219
|
+
return (
|
|
220
|
+
<>
|
|
221
|
+
{renderTrigger ? (
|
|
222
|
+
// Custom trigger renderer - gives full control to the consumer
|
|
223
|
+
<PopoverButton
|
|
224
|
+
as="div"
|
|
225
|
+
style={{ outline: "none" }}
|
|
226
|
+
ref={mergeRefs(buttonRef, refs.setReference, triggerRefProp)}
|
|
227
|
+
>
|
|
228
|
+
{renderTrigger({
|
|
229
|
+
ref: () => {
|
|
230
|
+
// Additional ref handling if needed
|
|
231
|
+
},
|
|
232
|
+
})}
|
|
233
|
+
</PopoverButton>
|
|
234
|
+
) : (
|
|
235
|
+
// Default trigger wrapper
|
|
236
|
+
<Button
|
|
237
|
+
as={PopoverButton}
|
|
238
|
+
style={{ outline: "none" }}
|
|
239
|
+
p="0"
|
|
240
|
+
display="flex"
|
|
241
|
+
alignItems="center"
|
|
242
|
+
variant="ghost"
|
|
243
|
+
$size="none"
|
|
244
|
+
//{...(open && { variant: "default" })}
|
|
245
|
+
{...triggerProps}
|
|
246
|
+
ref={mergeRefs(buttonRef, refs.setReference, triggerRefProp)}
|
|
247
|
+
>
|
|
248
|
+
{trigger}
|
|
249
|
+
</Button>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<AnimatePresence>
|
|
253
|
+
{open && (
|
|
254
|
+
<Panel
|
|
255
|
+
usePortal={usePortal}
|
|
256
|
+
panelRef={refs.setFloating}
|
|
257
|
+
floatingStyles={floatingStyles}
|
|
258
|
+
initial={{
|
|
259
|
+
opacity: 0,
|
|
260
|
+
...(normalizedPlacement.startsWith("right")
|
|
261
|
+
? { x: -10 }
|
|
262
|
+
: normalizedPlacement.startsWith("left")
|
|
263
|
+
? { x: 10 }
|
|
264
|
+
: normalizedPlacement.startsWith("top")
|
|
265
|
+
? { y: 10 }
|
|
266
|
+
: { y: -10 }),
|
|
267
|
+
}}
|
|
268
|
+
animate={{ opacity: 1, x: 0, y: 0 }}
|
|
269
|
+
exit={{
|
|
270
|
+
opacity: 0,
|
|
271
|
+
...(normalizedPlacement.startsWith("right")
|
|
272
|
+
? { x: -10 }
|
|
273
|
+
: normalizedPlacement.startsWith("left")
|
|
274
|
+
? { x: 10 }
|
|
275
|
+
: normalizedPlacement.startsWith("top")
|
|
276
|
+
? { y: 10 }
|
|
277
|
+
: { y: 10 }),
|
|
278
|
+
}}
|
|
279
|
+
transition={{ duration: 0.1 }}
|
|
280
|
+
p="xsmall"
|
|
281
|
+
shape="rounded"
|
|
282
|
+
width="15rem"
|
|
283
|
+
$shadow="medium"
|
|
284
|
+
zIndex="999999"
|
|
285
|
+
skin="translucent"
|
|
286
|
+
{...panelProps}
|
|
287
|
+
>
|
|
288
|
+
{typeof children === "function"
|
|
289
|
+
? children({ close, open })
|
|
290
|
+
: children}
|
|
291
|
+
</Panel>
|
|
292
|
+
)}
|
|
293
|
+
</AnimatePresence>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}}
|
|
297
|
+
</Box>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// For hover mode popover, we'll use a custom implementation
|
|
302
|
+
// but still leverage Headless UI's Portal if needed
|
|
303
|
+
return (
|
|
304
|
+
<Box
|
|
305
|
+
as="div"
|
|
306
|
+
position="relative"
|
|
307
|
+
ref={mergeRefs(containerRef, refs.setReference, containerRefProp)}
|
|
308
|
+
onMouseEnter={handleMouseEnter}
|
|
309
|
+
onMouseLeave={handleMouseLeave}
|
|
310
|
+
{...restContainerProps}
|
|
311
|
+
>
|
|
312
|
+
{trigger}
|
|
313
|
+
|
|
314
|
+
<AnimatePresence>
|
|
315
|
+
{isHovering &&
|
|
316
|
+
(usePortal ? (
|
|
317
|
+
<Portal>
|
|
318
|
+
<MotionBox
|
|
319
|
+
ref={refs.setFloating}
|
|
320
|
+
initial={{
|
|
321
|
+
opacity: 0,
|
|
322
|
+
...(normalizedPlacement.startsWith("right")
|
|
323
|
+
? { x: -10 }
|
|
324
|
+
: normalizedPlacement.startsWith("left")
|
|
325
|
+
? { x: 10 }
|
|
326
|
+
: normalizedPlacement.startsWith("top")
|
|
327
|
+
? { y: 10 }
|
|
328
|
+
: { y: -10 }),
|
|
329
|
+
}}
|
|
330
|
+
animate={{ opacity: 1, x: 0, y: 0 }}
|
|
331
|
+
exit={{
|
|
332
|
+
opacity: 0,
|
|
333
|
+
...(normalizedPlacement.startsWith("right")
|
|
334
|
+
? { x: -10 }
|
|
335
|
+
: normalizedPlacement.startsWith("left")
|
|
336
|
+
? { x: 10 }
|
|
337
|
+
: normalizedPlacement.startsWith("top")
|
|
338
|
+
? { y: 10 }
|
|
339
|
+
: { y: 10 }),
|
|
340
|
+
}}
|
|
341
|
+
transition={{ duration: 0.1 }}
|
|
342
|
+
p="xsmall"
|
|
343
|
+
shape="rounded"
|
|
344
|
+
width="15rem"
|
|
345
|
+
$shadow="medium"
|
|
346
|
+
skin="translucent"
|
|
347
|
+
zIndex="999999"
|
|
348
|
+
{...panelProps}
|
|
349
|
+
style={{
|
|
350
|
+
...floatingStyles,
|
|
351
|
+
...(panelProps?.style || {}),
|
|
352
|
+
}}
|
|
353
|
+
onMouseEnter={() => setIsHovering(true)}
|
|
354
|
+
onMouseLeave={() => setIsHovering(false)}
|
|
355
|
+
>
|
|
356
|
+
{children}
|
|
357
|
+
</MotionBox>
|
|
358
|
+
</Portal>
|
|
359
|
+
) : (
|
|
360
|
+
<MotionBox
|
|
361
|
+
ref={refs.setFloating}
|
|
362
|
+
initial={{
|
|
363
|
+
opacity: 0,
|
|
364
|
+
...(normalizedPlacement.startsWith("right")
|
|
365
|
+
? { x: -10 }
|
|
366
|
+
: normalizedPlacement.startsWith("left")
|
|
367
|
+
? { x: 10 }
|
|
368
|
+
: normalizedPlacement.startsWith("top")
|
|
369
|
+
? { y: 10 }
|
|
370
|
+
: { y: -10 }),
|
|
371
|
+
}}
|
|
372
|
+
animate={{ opacity: 1, x: 0, y: 0 }}
|
|
373
|
+
exit={{
|
|
374
|
+
opacity: 0,
|
|
375
|
+
...(normalizedPlacement.startsWith("right")
|
|
376
|
+
? { x: -10 }
|
|
377
|
+
: normalizedPlacement.startsWith("left")
|
|
378
|
+
? { x: 10 }
|
|
379
|
+
: normalizedPlacement.startsWith("top")
|
|
380
|
+
? { y: 10 }
|
|
381
|
+
: { y: 10 }),
|
|
382
|
+
}}
|
|
383
|
+
transition={{ duration: 0.1 }}
|
|
384
|
+
skin="translucent"
|
|
385
|
+
p="xsmall"
|
|
386
|
+
shape="rounded"
|
|
387
|
+
width="15rem"
|
|
388
|
+
$shadow="medium"
|
|
389
|
+
zIndex="999999"
|
|
390
|
+
{...panelProps}
|
|
391
|
+
style={{
|
|
392
|
+
...floatingStyles,
|
|
393
|
+
...(panelProps?.style || {}),
|
|
394
|
+
}}
|
|
395
|
+
>
|
|
396
|
+
{children}
|
|
397
|
+
</MotionBox>
|
|
398
|
+
))}
|
|
399
|
+
</AnimatePresence>
|
|
400
|
+
</Box>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type ProgressType = "negative" | "positive" | "neutral";
|
|
2
|
+
|
|
3
|
+
interface ColorRanges {
|
|
4
|
+
low: string; // 0-lowThreshold
|
|
5
|
+
medium: string; // lowThreshold-mediumThreshold
|
|
6
|
+
high: string; // mediumThreshold-highThreshold
|
|
7
|
+
max: string; // highThreshold-100
|
|
8
|
+
neutral: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ProgressThresholds {
|
|
12
|
+
low: number; // Upper bound for "low" range
|
|
13
|
+
medium: number; // Upper bound for "medium" range
|
|
14
|
+
high: number; // Upper bound for "high" range
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_COLORS: ColorRanges = {
|
|
18
|
+
low: "red",
|
|
19
|
+
medium: "orange",
|
|
20
|
+
high: "palette.brands.5",
|
|
21
|
+
max: "palette.greens.6",
|
|
22
|
+
neutral: "neutral",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_THRESHOLDS: ProgressThresholds = {
|
|
26
|
+
low: 25,
|
|
27
|
+
medium: 50,
|
|
28
|
+
high: 75,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a color string based on progress percentage and type
|
|
33
|
+
*
|
|
34
|
+
* @param progress - The progress value from 0-100
|
|
35
|
+
* @param type - The type of progress ("positive", "negative", "neutral")
|
|
36
|
+
* @param colors - Optional color mappings
|
|
37
|
+
* @param thresholds - Optional threshold values
|
|
38
|
+
* @returns A color string (hex, rgb, etc.)
|
|
39
|
+
*/
|
|
40
|
+
export const getProgressColor = (
|
|
41
|
+
progress: number,
|
|
42
|
+
type: ProgressType,
|
|
43
|
+
colors: ColorRanges = DEFAULT_COLORS,
|
|
44
|
+
thresholds: ProgressThresholds = DEFAULT_THRESHOLDS,
|
|
45
|
+
): string => {
|
|
46
|
+
// For neutral type, always return the neutral color
|
|
47
|
+
if (type === "neutral") {
|
|
48
|
+
return colors.neutral;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Determine color based on progress and thresholds
|
|
52
|
+
if (progress <= thresholds.low) {
|
|
53
|
+
return type === "positive" ? colors.low : colors.max;
|
|
54
|
+
} else if (progress <= thresholds.medium) {
|
|
55
|
+
return type === "positive" ? colors.medium : colors.high;
|
|
56
|
+
} else if (progress <= thresholds.high) {
|
|
57
|
+
return type === "positive" ? colors.high : colors.medium;
|
|
58
|
+
} else {
|
|
59
|
+
return type === "positive" ? colors.max : colors.low;
|
|
60
|
+
}
|
|
61
|
+
};
|