@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,60 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { Box } from "@/design-system/elements";
4
+ import { MenuProvider, useMenu } from "./MenuContext";
5
+ import { MenuItem } from "./MenuItem";
6
+ import { MenuContent } from "./MenuContent";
7
+ import { AnimatePresence } from "motion/react";
8
+
9
+ interface MenuProps {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ // Add an interface for the child props
14
+ interface MenuChildProps {
15
+ id: string | number;
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ const MenuInner: React.FC<{ children: React.ReactNode }> = ({ children }) => {
20
+ const { setSelected, selected, setDir } = useMenu();
21
+
22
+ const selectedContent = React.Children.toArray(children).find(
23
+ (child) =>
24
+ React.isValidElement(child) &&
25
+ (child.props as MenuChildProps).id === selected,
26
+ );
27
+
28
+ return (
29
+ <Box display="flex" alignItems="center" justifyContent="center">
30
+ <Box
31
+ position="relative"
32
+ display="flex"
33
+ alignItems="center"
34
+ onMouseLeave={() => {
35
+ setDir(null);
36
+ setSelected(null);
37
+ }}
38
+ zIndex={9999}
39
+ >
40
+ {children}
41
+
42
+ <AnimatePresence mode="sync">
43
+ {selected !== null &&
44
+ React.isValidElement(selectedContent) &&
45
+ (selectedContent.props as MenuChildProps).children}
46
+ </AnimatePresence>
47
+ </Box>
48
+ </Box>
49
+ );
50
+ };
51
+
52
+ export const Menu: React.FC<MenuProps> = ({ children }) => {
53
+ return (
54
+ <MenuProvider>
55
+ <MenuInner>{children}</MenuInner>
56
+ </MenuProvider>
57
+ );
58
+ };
59
+
60
+ export { MenuItem, MenuContent };
@@ -0,0 +1,268 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Enhanced Modal component
5
+ *
6
+ * Improvements:
7
+ * - Adds robust fallback navigation logic when closing (overlay click or programmatic).
8
+ * - Supports an optional `fallbackHref` prop for deterministic navigation when history is shallow.
9
+ * - Keeps support for user-supplied `onClose` (which always takes precedence).
10
+ * - Derives a parent path for common resource detail routes (/tasks/[id], /activity/[id]) if no fallback supplied.
11
+ * - Adds `closeOnOverlayClick` prop to allow disabling close when clicking the backdrop overlay.
12
+ *
13
+ * Usage:
14
+ * <Modal onClose={navigateBack} fallbackHref="/tasks" closeOnOverlayClick={false}>
15
+ * ...
16
+ * </Modal>
17
+ */
18
+
19
+ import { AnimatePresence, motion } from "motion/react";
20
+ import { Box } from "@/design-system/elements";
21
+ import { useKeypress } from "@/design-system/hooks/useKeypress";
22
+ import { useOverlay } from "@/design-system/hooks/useOverlay";
23
+ import { Backdrop } from "@/design-system/blocks/Overlay";
24
+ import { usePathname, useRouter } from "next/navigation";
25
+ import {
26
+ createContext,
27
+ useCallback,
28
+ useContext,
29
+ useEffect,
30
+ useRef,
31
+ useState,
32
+ type ReactNode,
33
+ } from "react";
34
+ import { createPortal } from "react-dom";
35
+
36
+ // Re-export for consumers that import these directly from Modal
37
+ export { pushThemeColor, popThemeColor } from "@/design-system/utils/overlayTheme";
38
+
39
+ /**
40
+ * Context that exposes the Modal's close function to children.
41
+ * When `animated` is true, this triggers the exit animation before unmounting.
42
+ * When `animated` is false, this calls the close handler directly.
43
+ */
44
+ const ModalCloseContext = createContext<(() => void) | null>(null);
45
+
46
+ /**
47
+ * Hook to access the parent Modal's close function.
48
+ * Returns null if used outside a Modal.
49
+ */
50
+ export function useModalClose() {
51
+ return useContext(ModalCloseContext);
52
+ }
53
+
54
+ interface ModalProps {
55
+ children: ReactNode;
56
+ onClose?: () => void;
57
+ portal?: boolean;
58
+ /**
59
+ * Explicit fallback route used when there is no browser history to return to.
60
+ * Example: "/tasks"
61
+ */
62
+ fallbackHref?: string;
63
+ /**
64
+ * (Optional) Disable using browser history even if available and always use fallback / derived path.
65
+ */
66
+ forceFallback?: boolean;
67
+ /**
68
+ * Whether clicking the backdrop overlay should close the modal.
69
+ * Defaults to true for current behavior. Set to false to prevent overlay clicks from closing.
70
+ */
71
+ closeOnOverlayClick?: boolean;
72
+ /**
73
+ * Whether to show the tinted backdrop overlay behind the modal.
74
+ * Defaults to true. Set to false for a transparent backdrop.
75
+ */
76
+ backdrop?: boolean;
77
+ /**
78
+ * Whether to animate the modal open/close with a scale + fade
79
+ * animation matching the sign-in card style. Defaults to false.
80
+ */
81
+ animated?: boolean;
82
+ /**
83
+ * Additional style/semantic props forwarded to container.
84
+ */
85
+ [key: string]: any;
86
+ }
87
+
88
+ export default function Modal({
89
+ children,
90
+ onClose,
91
+ portal = true,
92
+ fallbackHref,
93
+ forceFallback = false,
94
+ closeOnOverlayClick = true,
95
+ backdrop = true,
96
+ animated = false,
97
+ ...rest
98
+ }: ModalProps) {
99
+ const router = useRouter();
100
+ const pathname = usePathname();
101
+ const { mounted, isStandalone } = useOverlay(true);
102
+
103
+ // Track if we (likely) have a prior history entry.
104
+ // NOTE: history.length heuristic is imperfect but acceptable for typical SPA usage.
105
+ const canGoBackRef = useRef(false);
106
+
107
+ useEffect(() => {
108
+ if (typeof window !== "undefined") {
109
+ canGoBackRef.current = window.history.length > 1;
110
+ }
111
+ }, []);
112
+
113
+ /**
114
+ * Derive a reasonable parent path if no explicit fallback was provided.
115
+ * Extend this logic as new modalized resource routes are introduced.
116
+ */
117
+ const deriveParentPath = useCallback((p: string): string => {
118
+ if (!p) return "/";
119
+ const patterns: Array<{ regex: RegExp; parent: string }> = [
120
+ { regex: /\/tasks\/[^/]+$/, parent: "/tasks" },
121
+ { regex: /\/activity\/[^/]+$/, parent: "/activity" },
122
+ ];
123
+
124
+ for (const { regex, parent } of patterns) {
125
+ if (regex.test(p)) return parent;
126
+ }
127
+ return "/"; // Default root
128
+ }, []);
129
+
130
+ const resolvedFallback =
131
+ fallbackHref || (pathname ? deriveParentPath(pathname) : "/") || "/";
132
+
133
+ /**
134
+ * Close handler:
135
+ * 1. If user supplied onClose -> call it.
136
+ * 2. Else if not forcing fallback and history seems viable -> router.back().
137
+ * 3. Else -> router.push(resolvedFallback).
138
+ */
139
+ const handleClose = useCallback(() => {
140
+ if (onClose) {
141
+ onClose();
142
+ return;
143
+ }
144
+
145
+ if (!forceFallback && canGoBackRef.current) {
146
+ router.back();
147
+ return;
148
+ }
149
+
150
+ router.push(resolvedFallback);
151
+ }, [onClose, forceFallback, router, resolvedFallback]);
152
+
153
+ /**
154
+ * Stop propagation inside main dialog so clicks there don't trigger overlay close.
155
+ */
156
+ const stopPropagation = useCallback(
157
+ (e: React.MouseEvent) => e.stopPropagation(),
158
+ []
159
+ );
160
+
161
+ // For animated modals, manage a closing state to allow exit animations
162
+ // before the portal is removed.
163
+ const [isClosing, setIsClosing] = useState(false);
164
+
165
+ const handleAnimatedClose = useCallback(() => {
166
+ setIsClosing(true);
167
+ }, []);
168
+
169
+ const onExitComplete = useCallback(() => {
170
+ setIsClosing(false);
171
+ handleClose();
172
+ }, [handleClose]);
173
+
174
+ const effectiveClose = animated ? handleAnimatedClose : handleClose;
175
+ const showContent = animated ? !isClosing : true;
176
+
177
+ // Close on Escape key. Uses effectiveClose so animated modals play exit animation.
178
+ useKeypress("Escape", effectiveClose);
179
+
180
+ const animatedTransition = {
181
+ type: "spring" as const,
182
+ stiffness: 500,
183
+ damping: 30,
184
+ mass: 0.8,
185
+ };
186
+
187
+ const modalContent = (
188
+ <Box
189
+ position="fixed"
190
+ top="0"
191
+ left="0"
192
+ width="100%"
193
+ height="100%"
194
+ display={"flex"}
195
+ justifyContent={"center"}
196
+ alignItems={"center"}
197
+ zIndex="100002"
198
+ pb="m"
199
+ px="xs"
200
+ style={{
201
+ paddingTop: isStandalone
202
+ ? "calc(1rem + env(safe-area-inset-top, 0px))"
203
+ : "1rem",
204
+ }}
205
+ >
206
+ <AnimatePresence onExitComplete={animated ? onExitComplete : undefined}>
207
+ {showContent && (
208
+ <>
209
+ {/* Dialog Panel */}
210
+ <Box
211
+ as={animated ? motion.div : undefined}
212
+ key="modal-dialog"
213
+ {...(animated
214
+ ? {
215
+ initial: { opacity: 0, y: "2rem", scale: 0.85 },
216
+ animate: { opacity: 1, y: 0, scale: 1 },
217
+ exit: { opacity: 0, y: "2rem", scale: 0.85 },
218
+ transition: animatedTransition,
219
+ }
220
+ : {})}
221
+ role="dialog"
222
+ aria-modal="true"
223
+ position="relative"
224
+ maxWidth="inherit"
225
+ width="100%"
226
+ zIndex="100001"
227
+ shape="rounded"
228
+ $shadow="medium"
229
+ overflow="hidden"
230
+ height="100%"
231
+ skin="surface"
232
+ onClick={stopPropagation}
233
+ {...rest}
234
+ >
235
+ <Box
236
+ height="100%"
237
+ width="100%"
238
+ overflow="hidden"
239
+ display="flex"
240
+ flexDirection="column"
241
+ >
242
+ <ModalCloseContext.Provider value={effectiveClose}>
243
+ {children}
244
+ </ModalCloseContext.Provider>
245
+ </Box>
246
+ </Box>
247
+
248
+ {/* Clickable overlay (behind dialog) */}
249
+ <Backdrop
250
+ key="modal-overlay"
251
+ animated={animated}
252
+ transition={animated ? animatedTransition : { duration: 0.2 }}
253
+ bg={backdrop ? "transparent.light.9" : "transparent"}
254
+ onClick={
255
+ closeOnOverlayClick ? effectiveClose : undefined
256
+ }
257
+ />
258
+ </>
259
+ )}
260
+ </AnimatePresence>
261
+ </Box>
262
+ );
263
+
264
+ // Avoid rendering portal content during SSR (Next.js) until mounted.
265
+ if (!mounted) return null;
266
+
267
+ return portal ? createPortal(modalContent, document.body) : modalContent;
268
+ }
@@ -0,0 +1,294 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect, useId, useRef, useState } from "react";
4
+ import { Box } from "@/design-system/elements";
5
+ import { createPortal } from "react-dom";
6
+ import { useClickOutside } from "@/design-system/hooks/useClickOutside";
7
+ import {
8
+ AnimatePresence,
9
+ MotionConfig,
10
+ motion,
11
+ type Transition,
12
+ type Variants,
13
+ } from "motion/react";
14
+ import Underlay from "../Underlay";
15
+
16
+ const DEFAULT_TRANSITION: Transition = {
17
+ type: "spring",
18
+ bounce: 0.1,
19
+ duration: 0.4,
20
+ };
21
+
22
+ const DEFAULT_VARIANTS: Variants = {
23
+ initial: { opacity: 0, borderRadius: 9999 },
24
+ animate: { opacity: 1, borderRadius: 16 },
25
+ exit: { opacity: 0, borderRadius: 9999 },
26
+ };
27
+
28
+ type MorphingPopoverRenderTriggerProps = {
29
+ ref: (node: HTMLElement | null) => void;
30
+ onClick?: () => void;
31
+ };
32
+
33
+ export type MorphingPopoverProps = {
34
+ /** Trigger element shown when the popover is closed. */
35
+ trigger?: React.ReactNode;
36
+ /** Props forwarded to the trigger wrapper when not using renderTrigger. */
37
+ triggerProps?: Record<string, any>;
38
+ /** Optional placement hint (kept for API compatibility; currently ignored). */
39
+ placement?: string;
40
+ /** Props forwarded to the morphing panel container. */
41
+ panelProps?: Record<string, any>;
42
+ /** Props forwarded to the outer container Box. */
43
+ containerProps?: Record<string, any>;
44
+ /**
45
+ * Custom trigger renderer for full control over the trigger element.
46
+ * Signature mirrors the standard Popover API.
47
+ */
48
+ renderTrigger?: (props: MorphingPopoverRenderTriggerProps) => React.ReactNode;
49
+ /**
50
+ * Popover content. If a function, receives a close() helper.
51
+ */
52
+ children?:
53
+ | React.ReactNode
54
+ | ((context: { close: () => void }) => React.ReactNode);
55
+ /** Motion transition applied via MotionConfig. */
56
+ transition?: Transition;
57
+ /** Framer Motion variants for the morphing panel. */
58
+ variants?: Variants;
59
+ /** Uncontrolled initial open state. */
60
+ defaultOpen?: boolean;
61
+ /** Controlled open state. */
62
+ open?: boolean;
63
+ /** Callback when open state changes. */
64
+ onOpenChange?: (open: boolean) => void;
65
+ };
66
+
67
+ export default function MorphingPopover({
68
+ trigger,
69
+ triggerProps,
70
+ placement: _placement = "bottom end",
71
+ panelProps,
72
+ containerProps,
73
+ renderTrigger,
74
+ children,
75
+ transition = DEFAULT_TRANSITION,
76
+ variants = DEFAULT_VARIANTS,
77
+ defaultOpen = false,
78
+ open: controlledOpen,
79
+ onOpenChange,
80
+ }: MorphingPopoverProps) {
81
+ const uniqueId = useId();
82
+ const layoutId = `morphing-popover-${uniqueId}`;
83
+
84
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
85
+ const isControlled = controlledOpen !== undefined;
86
+ const isOpen = isControlled ? !!controlledOpen : uncontrolledOpen;
87
+
88
+ const panelRef = useRef<HTMLDivElement | null>(null);
89
+
90
+ const setOpen = useCallback(
91
+ (next: boolean) => {
92
+ if (!isControlled) {
93
+ setUncontrolledOpen(next);
94
+ }
95
+ onOpenChange?.(next);
96
+ },
97
+ [isControlled, onOpenChange]
98
+ );
99
+
100
+ const handleOpen = useCallback(() => {
101
+ setOpen(true);
102
+ }, [setOpen]);
103
+
104
+ const handleClose = useCallback(() => {
105
+ setOpen(false);
106
+ }, [setOpen]);
107
+
108
+ // Close on click outside of the panel when open.
109
+ useClickOutside(panelRef, () => handleClose(), isOpen);
110
+
111
+ // Close on Escape key.
112
+ useEffect(() => {
113
+ if (!isOpen) return;
114
+
115
+ const handleKeyDown = (event: KeyboardEvent) => {
116
+ if (event.key === "Escape") {
117
+ handleClose();
118
+ }
119
+ };
120
+
121
+ document.addEventListener("keydown", handleKeyDown);
122
+ return () => {
123
+ document.removeEventListener("keydown", handleKeyDown);
124
+ };
125
+ }, [isOpen, handleClose]);
126
+
127
+ const triggerRef = useRef<HTMLElement | null>(null);
128
+
129
+ const handleTriggerRef = useCallback((node: HTMLElement | null) => {
130
+ triggerRef.current = node;
131
+ }, []);
132
+
133
+ const renderTriggerNode = () => {
134
+ // If a custom trigger renderer is provided, mirror the Popover API
135
+ // and let the returned element control opening via the provided onClick.
136
+ if (renderTrigger) {
137
+ const rendered = renderTrigger({
138
+ ref: handleTriggerRef,
139
+ onClick: handleOpen,
140
+ });
141
+
142
+ if (!rendered) return null;
143
+
144
+ if (React.isValidElement(rendered)) {
145
+ const MotionComponent = motion.create(
146
+ rendered.type as React.ComponentType<any>
147
+ );
148
+ const childProps = rendered.props as Record<string, any>;
149
+
150
+ return (
151
+ <MotionComponent
152
+ {...childProps}
153
+ layoutId={layoutId}
154
+ onClick={(event: React.MouseEvent<any>) => {
155
+ if (typeof childProps.onClick === "function") {
156
+ childProps.onClick(event);
157
+ }
158
+ // Do not call handleOpen here; we expect the child's onClick
159
+ // (wired via props.onClick) to manage open/close so we avoid
160
+ // double-calling it.
161
+ }}
162
+ />
163
+ );
164
+ }
165
+
166
+ // Fallback: wrap non-element custom triggers.
167
+ return (
168
+ <Box
169
+ as={motion.div}
170
+ layoutId={layoutId}
171
+ onClick={handleOpen}
172
+ {...triggerProps}
173
+ >
174
+ {rendered}
175
+ </Box>
176
+ );
177
+ }
178
+
179
+ // Default trigger path: use the provided trigger node and augment its
180
+ // onClick to also open the popover.
181
+ const rendered = trigger;
182
+
183
+ if (!rendered) return null;
184
+
185
+ if (React.isValidElement(rendered)) {
186
+ const MotionComponent = motion.create(
187
+ rendered.type as React.ComponentType<any>
188
+ );
189
+ const childProps = rendered.props as Record<string, any>;
190
+
191
+ return (
192
+ <MotionComponent
193
+ {...childProps}
194
+ layoutId={layoutId}
195
+ onClick={(event: React.MouseEvent<any>) => {
196
+ if (typeof childProps.onClick === "function") {
197
+ childProps.onClick(event);
198
+ }
199
+ if (!event.defaultPrevented) {
200
+ handleOpen();
201
+ }
202
+ }}
203
+ />
204
+ );
205
+ }
206
+
207
+ // Fallback: wrap non-element triggers.
208
+ return (
209
+ <Box
210
+ as={motion.div}
211
+ layoutId={layoutId}
212
+ onClick={handleOpen}
213
+ {...triggerProps}
214
+ >
215
+ {rendered}
216
+ </Box>
217
+ );
218
+ };
219
+
220
+ const { style: panelStyleFromProps, ...restPanelProps } = panelProps ?? {};
221
+
222
+ const content =
223
+ typeof children === "function"
224
+ ? children({ close: handleClose })
225
+ : children;
226
+
227
+ const overlay = (
228
+ <AnimatePresence>
229
+ {isOpen && (
230
+ <Box
231
+ as={motion.div}
232
+ key={`${layoutId}-overlay`}
233
+ position="fixed"
234
+ top={0}
235
+ left={0}
236
+ right={0}
237
+ bottom={0}
238
+ zIndex="999999"
239
+ display="flex"
240
+ alignItems="center"
241
+ justifyContent="center"
242
+ >
243
+ <Box
244
+ as={motion.div}
245
+ ref={panelRef}
246
+ key={layoutId}
247
+ layoutId={layoutId}
248
+ role="dialog"
249
+ aria-modal="true"
250
+ id={`morphing-popover-panel-${uniqueId}`}
251
+ shape="rounded"
252
+ $shadow="medium"
253
+ skin="surface"
254
+ p="xsmall"
255
+ style={panelStyleFromProps || {}}
256
+ initial="initial"
257
+ animate="animate"
258
+ exit="exit"
259
+ position={"relative"}
260
+ zIndex={"1"}
261
+ variants={variants}
262
+ {...restPanelProps}
263
+ >
264
+ {content}
265
+ </Box>
266
+ <Underlay
267
+ as={motion.div}
268
+ width="100dvw"
269
+ height="100dvh"
270
+ color="transparent"
271
+ top="0"
272
+ left="0"
273
+ position="fixed"
274
+ zIndex="-1"
275
+ initial="initial"
276
+ animate="animate"
277
+ exit="exit"
278
+ />
279
+ </Box>
280
+ )}
281
+ </AnimatePresence>
282
+ );
283
+
284
+ return (
285
+ <MotionConfig transition={transition}>
286
+ <Box position="relative" display="inline-flex" {...containerProps}>
287
+ {renderTriggerNode()}
288
+ {typeof window === "undefined"
289
+ ? overlay
290
+ : createPortal(overlay, document.body)}
291
+ </Box>
292
+ </MotionConfig>
293
+ );
294
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { motion } from "motion/react";
4
+ import { Box } from "@/design-system/elements";
5
+
6
+ interface BackdropProps {
7
+ onClick?: () => void;
8
+ bg?: string;
9
+ zIndex?: number | string;
10
+ /** When true, the backdrop fades in from opacity 0. Otherwise it appears instantly. */
11
+ animated?: boolean;
12
+ /** Custom motion transition. Falls back to a simple 200ms fade. */
13
+ transition?: object;
14
+ [key: string]: any;
15
+ }
16
+
17
+ /**
18
+ * Animated full-screen backdrop overlay used by Modal, Drawer, etc.
19
+ * Must be rendered inside an `<AnimatePresence>` for exit animations.
20
+ */
21
+ export function Backdrop({
22
+ onClick,
23
+ bg = "rgba(0, 0, 0, 0.5)",
24
+ zIndex,
25
+ animated = false,
26
+ transition,
27
+ ...rest
28
+ }: BackdropProps) {
29
+ return (
30
+ <Box
31
+ as={motion.div}
32
+ initial={{ opacity: animated ? 0 : 1 }}
33
+ animate={{ opacity: 1 }}
34
+ exit={{ opacity: 0 }}
35
+ transition={transition ?? { duration: 0.2 }}
36
+ position="fixed"
37
+ top="0"
38
+ left="0"
39
+ width="100%"
40
+ height="100%"
41
+ bg={bg}
42
+ zIndex={zIndex}
43
+ onClick={onClick}
44
+ aria-hidden="true"
45
+ {...rest}
46
+ />
47
+ );
48
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { motion } from "motion/react";
4
+ import { Box } from "@/design-system/elements";
5
+
6
+ interface OverscrollGuardProps {
7
+ zIndex?: number | string;
8
+ }
9
+
10
+ /**
11
+ * Covers the safe-area gap at the bottom and extends well past the viewport
12
+ * so iOS rubber-band overscroll never reveals the page behind an overlay.
13
+ * The 200px buffer exceeds the maximum rubber-band distance on iOS.
14
+ *
15
+ * Must be rendered inside an `<AnimatePresence>` for exit animations.
16
+ */
17
+ export function OverscrollGuard({ zIndex = 10008 }: OverscrollGuardProps) {
18
+ return (
19
+ <Box
20
+ as={motion.div}
21
+ initial={{ opacity: 0 }}
22
+ animate={{ opacity: 1 }}
23
+ exit={{ opacity: 0 }}
24
+ position="fixed"
25
+ left="0"
26
+ right="0"
27
+ zIndex={zIndex}
28
+ bg="surface"
29
+ skin="translucent"
30
+ style={{
31
+ bottom: "-200px",
32
+ height: "calc(env(safe-area-inset-bottom, 0px) + 200px)",
33
+ }}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,2 @@
1
+ export { Backdrop } from "./Backdrop";
2
+ export { OverscrollGuard } from "./OverscrollGuard";