@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.
Files changed (165) hide show
  1. package/README.md +48 -0
  2. package/package.json +74 -0
  3. package/src/blocks/Accordion/index.tsx +158 -0
  4. package/src/blocks/AnimatedCarousel/index.tsx +188 -0
  5. package/src/blocks/AppleGlow/index.tsx +144 -0
  6. package/src/blocks/Avatar/index.tsx +167 -0
  7. package/src/blocks/Await/index.tsx +45 -0
  8. package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
  9. package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
  10. package/src/blocks/Cards/InfoCard/index.tsx +206 -0
  11. package/src/blocks/Cards/TickerCard/index.tsx +125 -0
  12. package/src/blocks/Carousel/index.tsx +216 -0
  13. package/src/blocks/Checkbox/index.tsx +101 -0
  14. package/src/blocks/Collection/index.tsx +59 -0
  15. package/src/blocks/Container/index.tsx +55 -0
  16. package/src/blocks/Controls/Control.tsx +67 -0
  17. package/src/blocks/Controls/index.tsx +11 -0
  18. package/src/blocks/CyclingNumber/index.tsx +78 -0
  19. package/src/blocks/DisplaySet/index.tsx +42 -0
  20. package/src/blocks/Divider/index.tsx +14 -0
  21. package/src/blocks/Draggable/index.tsx +266 -0
  22. package/src/blocks/Drawer/index.tsx +136 -0
  23. package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
  24. package/src/blocks/DynamicIsland/index.tsx +2 -0
  25. package/src/blocks/Fader/index.tsx +145 -0
  26. package/src/blocks/FamilyDrawer/README.md +116 -0
  27. package/src/blocks/FamilyDrawer/example.tsx +108 -0
  28. package/src/blocks/FamilyDrawer/index.tsx +119 -0
  29. package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
  30. package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
  31. package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
  32. package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
  33. package/src/blocks/FieldSet/index.tsx +173 -0
  34. package/src/blocks/Filesystem/index.tsx +198 -0
  35. package/src/blocks/Gallery/Carousel/index.tsx +257 -0
  36. package/src/blocks/Gallery/Modal/index.tsx +83 -0
  37. package/src/blocks/Gallery/index.tsx +57 -0
  38. package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
  39. package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
  40. package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
  41. package/src/blocks/Gallery/utils/range.ts +11 -0
  42. package/src/blocks/GradientMesh/index.tsx +106 -0
  43. package/src/blocks/Group/index.tsx +152 -0
  44. package/src/blocks/Heading/index.tsx +111 -0
  45. package/src/blocks/HorizontalScroller/index.tsx +135 -0
  46. package/src/blocks/Icon/index.tsx +45 -0
  47. package/src/blocks/Indicator/index.tsx +27 -0
  48. package/src/blocks/InlineEditor/index.tsx +216 -0
  49. package/src/blocks/List/index.tsx +657 -0
  50. package/src/blocks/Main/index.tsx +17 -0
  51. package/src/blocks/Marquee/index.tsx +116 -0
  52. package/src/blocks/MaskedField/index.tsx +199 -0
  53. package/src/blocks/Menu/MenuContent.tsx +246 -0
  54. package/src/blocks/Menu/MenuContext.tsx +34 -0
  55. package/src/blocks/Menu/MenuItem.tsx +104 -0
  56. package/src/blocks/Menu/index.tsx +60 -0
  57. package/src/blocks/Modal/index.tsx +268 -0
  58. package/src/blocks/MorphingPopover/index.tsx +294 -0
  59. package/src/blocks/Overlay/Backdrop.tsx +48 -0
  60. package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
  61. package/src/blocks/Overlay/index.ts +2 -0
  62. package/src/blocks/Parallax/index.tsx +117 -0
  63. package/src/blocks/ParallaxSection/index.tsx +61 -0
  64. package/src/blocks/Placeholder/index.tsx +48 -0
  65. package/src/blocks/Popover/index.tsx +402 -0
  66. package/src/blocks/Progress/getProgressColor.ts +61 -0
  67. package/src/blocks/Progress/index.tsx +179 -0
  68. package/src/blocks/ProgressiveBlur/index.tsx +75 -0
  69. package/src/blocks/README.md +15 -0
  70. package/src/blocks/RenderAsset/index.tsx +18 -0
  71. package/src/blocks/ScrollContainer/index.tsx +93 -0
  72. package/src/blocks/ShinyText/index.tsx +72 -0
  73. package/src/blocks/Skeleton/index.tsx +71 -0
  74. package/src/blocks/Slider/SliderControls.tsx +119 -0
  75. package/src/blocks/Slider/index.tsx +140 -0
  76. package/src/blocks/Slider/useSlider.ts +126 -0
  77. package/src/blocks/Slideshow/index.tsx +177 -0
  78. package/src/blocks/Spotlight/index.tsx +144 -0
  79. package/src/blocks/Steps/StepIndicator.tsx +149 -0
  80. package/src/blocks/Steps/StepProgress.tsx +164 -0
  81. package/src/blocks/Steps/Steps.tsx +197 -0
  82. package/src/blocks/Steps/StepsNav.tsx +30 -0
  83. package/src/blocks/Steps/StepsTracker.tsx +80 -0
  84. package/src/blocks/Steps/hooks.ts +71 -0
  85. package/src/blocks/Steps/index.tsx +16 -0
  86. package/src/blocks/Steps/types.ts +71 -0
  87. package/src/blocks/StickySectionStack/index.tsx +136 -0
  88. package/src/blocks/Switch/index.tsx +85 -0
  89. package/src/blocks/SystemNotice/index.tsx +81 -0
  90. package/src/blocks/Table/README.md +251 -0
  91. package/src/blocks/Table/Table.tsx +207 -0
  92. package/src/blocks/Table/TablePagination.tsx +189 -0
  93. package/src/blocks/Table/index.ts +33 -0
  94. package/src/blocks/Table/useTableControls.ts +331 -0
  95. package/src/blocks/Tag/index.tsx +27 -0
  96. package/src/blocks/TextBreak/index.tsx +96 -0
  97. package/src/blocks/TextReveal/index.tsx +104 -0
  98. package/src/blocks/Thumbnail/index.tsx +26 -0
  99. package/src/blocks/Ticker/index.tsx +112 -0
  100. package/src/blocks/Toast/index.tsx +77 -0
  101. package/src/blocks/Tooltip/index.tsx +174 -0
  102. package/src/blocks/Underlay/index.tsx +104 -0
  103. package/src/blocks/Upload/Dropzone.tsx +92 -0
  104. package/src/blocks/Upload/UploadBtn.tsx +38 -0
  105. package/src/blocks/Upload/index.tsx +61 -0
  106. package/src/blocks/Upload/types.ts +37 -0
  107. package/src/blocks/VideoMarquee/index.tsx +511 -0
  108. package/src/blocks/index.ts +119 -0
  109. package/src/blocks/pagination/Pagination.tsx +148 -0
  110. package/src/blocks/pagination/PaginationList.tsx +41 -0
  111. package/src/blocks/pagination/index.ts +2 -0
  112. package/src/charts/BarChart.tsx +63 -0
  113. package/src/charts/PieChart.tsx +39 -0
  114. package/src/charts/index.ts +3 -0
  115. package/src/charts/utils.ts +103 -0
  116. package/src/docs/README.md +373 -0
  117. package/src/docs/reference/README.md +299 -0
  118. package/src/elements/box.ts +163 -0
  119. package/src/elements/button.ts +49 -0
  120. package/src/elements/field.ts +129 -0
  121. package/src/elements/index.ts +8 -0
  122. package/src/elements/text.ts +47 -0
  123. package/src/elements/utils.js +97 -0
  124. package/src/hooks/use-copy-to-clipboard.tsx +33 -0
  125. package/src/hooks/use-enter-submit.tsx +23 -0
  126. package/src/hooks/use-local-storage.ts +42 -0
  127. package/src/hooks/use-sidebar.tsx +109 -0
  128. package/src/hooks/useAnimatedText.ts +32 -0
  129. package/src/hooks/useAutosizeTextArea.ts +45 -0
  130. package/src/hooks/useBreakpoint.tsx +123 -0
  131. package/src/hooks/useClickOutside.tsx +38 -0
  132. package/src/hooks/useHover.tsx +33 -0
  133. package/src/hooks/useHoverList.tsx +17 -0
  134. package/src/hooks/useKeyboardShortcuts.ts +91 -0
  135. package/src/hooks/useKeypress.ts +27 -0
  136. package/src/hooks/useOverlay.ts +32 -0
  137. package/src/hooks/useReducedMotion.ts +25 -0
  138. package/src/hooks/useStandaloneMode.ts +35 -0
  139. package/src/hooks/useTouchDevice.ts +34 -0
  140. package/src/icons/index.tsx +129 -0
  141. package/src/index.ts +12 -0
  142. package/src/providers/DesignSystemProvider.tsx +35 -0
  143. package/src/providers/StyledComponentsRegistry.tsx +30 -0
  144. package/src/providers/index.ts +2 -0
  145. package/src/themes/README.md +30 -0
  146. package/src/themes/default/assets/badge-avatar.tsx +45 -0
  147. package/src/themes/default/assets/logo.tsx +42 -0
  148. package/src/themes/default/global.ts +138 -0
  149. package/src/themes/default/modes/dark/config.js +49 -0
  150. package/src/themes/default/modes/dark/skins.js +631 -0
  151. package/src/themes/default/modes/dark/theme.js +87 -0
  152. package/src/themes/default/modes/light/config.js +48 -0
  153. package/src/themes/default/modes/light/skins.js +1026 -0
  154. package/src/themes/default/modes/light/theme.js +74 -0
  155. package/src/themes/default/tokens/controls.js +53 -0
  156. package/src/themes/default/tokens/shadows.js +63 -0
  157. package/src/themes/default/tokens/shapes.js +37 -0
  158. package/src/themes/default/tokens/space.js +143 -0
  159. package/src/themes/default/tokens/spectre.js +16 -0
  160. package/src/themes/default/utils.js +523 -0
  161. package/src/themes/index.ts +11 -0
  162. package/src/types.ts +394 -0
  163. package/src/utils/overlayTheme.ts +61 -0
  164. package/src/utils/pickColor.ts +15 -0
  165. 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
+ };