@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,55 @@
1
+ "use client";
2
+ import Box, { BoxProps } from "@/design-system/elements/box";
3
+ import { ThemeContext } from "styled-components";
4
+
5
+ import React, { useContext } from "react";
6
+
7
+ //const SIDEBAR_PINNED_KEY = "sidebar_pinned";
8
+
9
+ interface ContainerProps extends BoxProps {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export default function Container({ children, ...props }: ContainerProps) {
14
+ const theme: any = useContext(ThemeContext);
15
+ // const [isPinned, setIsPinned] = useState(false);
16
+
17
+ // Load pinned state from localStorage and listen for changes
18
+ // useEffect(() => {
19
+ // const loadPinnedState = () => {
20
+ // const stored = localStorage.getItem(SIDEBAR_PINNED_KEY);
21
+ // if (stored !== null) {
22
+ // setIsPinned(JSON.parse(stored));
23
+ // }
24
+ // };
25
+
26
+ // Load initial state
27
+ // loadPinnedState();
28
+
29
+ // Listen for storage changes (in case it's updated in another component)
30
+ // const handleStorageChange = (e: StorageEvent) => {
31
+ // if (e.key === SIDEBAR_PINNED_KEY) {
32
+ // loadPinnedState();
33
+ // }
34
+ // };
35
+
36
+ // window.addEventListener("storage", handleStorageChange);
37
+
38
+ // Also listen for custom event in the same window
39
+ // const handleCustomEvent = () => {
40
+ // loadPinnedState();
41
+ // };
42
+ // window.addEventListener("sidebarPinnedChange", handleCustomEvent);
43
+
44
+ // return () => {
45
+ // window.removeEventListener("storage", handleStorageChange);
46
+ // window.removeEventListener("sidebarPinnedChange", handleCustomEvent);
47
+ // };
48
+ // }, []);
49
+
50
+ return (
51
+ <Box as="section" {...theme?.container} {...props}>
52
+ {children}
53
+ </Box>
54
+ );
55
+ }
@@ -0,0 +1,67 @@
1
+ import { motion } from "motion/react";
2
+
3
+ import Box, { BoxProps } from "@/design-system/elements/box";
4
+ import { IconArrowLeft, IconArrowRight } from "@/design-system/icons";
5
+ // import { IconDownloadData } from "@/design-system/icons";
6
+ // import { IconLaunch } from "@/design-system/icons";
7
+
8
+ interface ControlProps extends BoxProps {
9
+ children: React.ReactNode;
10
+ as?: any;
11
+ href?: string;
12
+ target?: string;
13
+ title?: string;
14
+ rel?: string;
15
+ onClick?: () => void;
16
+ [key: string]: any;
17
+ }
18
+
19
+ const btnvariants = {
20
+ initial: {},
21
+ active: {},
22
+ };
23
+
24
+ export function Control({ children, ...props }: ControlProps) {
25
+ return (
26
+ <Box
27
+ as={motion.div}
28
+ variants={btnvariants}
29
+ position={"absolute"}
30
+ top="calc(50% - 1.25rem)"
31
+ left="0"
32
+ bg="white"
33
+ $shadow="small"
34
+ size="2.5rem"
35
+ p="xsmall"
36
+ shape="circle"
37
+ cursor="pointer"
38
+ color="black"
39
+ zIndex={99}
40
+ initial="initial"
41
+ animate="initial"
42
+ whileHover="active"
43
+ display="flex"
44
+ alignItems="center"
45
+ opacity={1}
46
+ justifyContent="center"
47
+ {...props}
48
+ >
49
+ {children}
50
+ </Box>
51
+ );
52
+ }
53
+
54
+ export function ControlLeft({ ...props }: any) {
55
+ return (
56
+ <Control {...props}>
57
+ <Box as={IconArrowLeft} size="1.125rem" />
58
+ </Control>
59
+ );
60
+ }
61
+ export function ControlRight({ ...props }: any) {
62
+ return (
63
+ <Control {...props}>
64
+ <Box as={IconArrowRight} size="1.125rem" />
65
+ </Control>
66
+ );
67
+ }
@@ -0,0 +1,11 @@
1
+ import { Control, ControlLeft, ControlRight } from "./Control";
2
+
3
+ // Default export as an object containing all controls
4
+ const Controls = {
5
+ Control,
6
+ ControlLeft,
7
+ ControlRight,
8
+ };
9
+
10
+ export default Controls;
11
+ export { Control, ControlLeft, ControlRight };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { motion, MotionConfig, useInView } from "motion/react";
3
+ import NumberFlow, { useCanAnimate, Format } from "@number-flow/react";
4
+ import { Text } from "@/design-system/elements";
5
+ import { useEffect, useState, useRef } from "react";
6
+
7
+ const MotionNumberFlow = motion.create(NumberFlow);
8
+
9
+ type Props = {
10
+ value: number;
11
+ delay?: number;
12
+ format?: Format; // Use Format type from @number-flow/react
13
+ suffix?: string;
14
+ prefix?: string;
15
+ };
16
+
17
+ export default function CyclingNumber({
18
+ value = 0,
19
+ delay = 500,
20
+ format = {
21
+ style: "currency",
22
+ currency: "USD",
23
+ trailingZeroDisplay: "stripIfInteger",
24
+ } as Format,
25
+ suffix = "",
26
+ prefix = "",
27
+ }: Props) {
28
+ const canAnimate = useCanAnimate();
29
+ const [currentValue, setCurrentValue] = useState(0);
30
+ const ref = useRef(null);
31
+
32
+ const isInView = useInView(ref, {
33
+ once: false, // Only animate once when coming into view
34
+ amount: 0.5,
35
+ });
36
+
37
+ useEffect(() => {
38
+ let timeoutId: NodeJS.Timeout;
39
+
40
+ if (isInView) {
41
+ // Add delay before starting the animation
42
+ timeoutId = setTimeout(() => {
43
+ setCurrentValue(value);
44
+ }, delay);
45
+ }
46
+
47
+ return () => clearTimeout(timeoutId);
48
+ }, [isInView, value, delay]);
49
+
50
+ return (
51
+ <MotionConfig
52
+ transition={
53
+ canAnimate
54
+ ? { duration: 2, bounce: 0, type: "spring" }
55
+ : { duration: 0, type: "tween" }
56
+ }
57
+ >
58
+ <Text
59
+ ref={ref}
60
+ as={motion.span}
61
+ display={"inline-flex"}
62
+ alignItems={"center"}
63
+ color="primary"
64
+ layout
65
+ >
66
+ <MotionNumberFlow
67
+ value={currentValue}
68
+ layout
69
+ layoutRoot
70
+ trend={0}
71
+ format={format}
72
+ suffix={suffix}
73
+ prefix={prefix}
74
+ />
75
+ </Text>
76
+ </MotionConfig>
77
+ );
78
+ }
@@ -0,0 +1,42 @@
1
+ import { Text } from "@/design-system/elements";
2
+ export default function DisplaySet({
3
+ label,
4
+ content,
5
+ flow = "row",
6
+ contentProps = {},
7
+ ...props
8
+ }: {
9
+ label?: string | React.ReactNode;
10
+ content: any;
11
+ flow?: "row" | "column";
12
+ contentProps?: Record<string, any>;
13
+ [key: string]: any;
14
+ }) {
15
+ return (
16
+ <Text
17
+ as="div"
18
+ display={"grid"}
19
+ gap="xxsmall"
20
+ gridTemplateColumns={flow === "column" ? "1fr" : "1fr 1fr"}
21
+ color="secondary"
22
+ fontWeight={700}
23
+ {...props}
24
+ >
25
+ {label && (
26
+ <Text as="h4" m="0">
27
+ {label}
28
+ </Text>
29
+ )}
30
+ <Text
31
+ as="div"
32
+ fontWeight={400}
33
+ color={
34
+ content !== null && content !== undefined ? "primary" : "tertiary"
35
+ }
36
+ {...contentProps}
37
+ >
38
+ {content !== null && content !== undefined ? content : "not set"}
39
+ </Text>
40
+ </Text>
41
+ );
42
+ }
@@ -0,0 +1,14 @@
1
+ import Box from "@/design-system/elements/box";
2
+
3
+ export default function Divider(props: any) {
4
+ return (
5
+ <Box
6
+ height="1px"
7
+ width="100%"
8
+ bg="transparent.light.1"
9
+ my="xxxsmall"
10
+ shape="rounded"
11
+ {...props}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,266 @@
1
+ "use client";
2
+ import React, { useRef, useEffect, useState } from "react";
3
+ import { motion, useDragControls } from "motion/react";
4
+ import { Box, Button, Text } from "@/design-system/elements";
5
+ import { IconMove3 } from "@/design-system/icons";
6
+ // import { IconCInfo } from "@/design-system/icons";
7
+ import { useLocalStorage } from "@/design-system/hooks/use-local-storage";
8
+
9
+ // PanelHeader Component
10
+ interface PanelHeaderProps {
11
+ label: string;
12
+ leftControls?: React.ReactNode;
13
+ rightControls?: React.ReactNode;
14
+ onPointerDown?: (e: React.PointerEvent) => void;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ const PanelHeader: React.FC<PanelHeaderProps> = ({
19
+ label = "Panel Label",
20
+ leftControls,
21
+ rightControls,
22
+ onPointerDown,
23
+ style,
24
+ }) => {
25
+ return (
26
+ <Box
27
+ display="flex"
28
+ alignItems="center"
29
+ justifyContent="center"
30
+ borderBottom="1px solid"
31
+ borderBottomColor="transparent.light.1"
32
+ p="xsmall"
33
+ flex="none"
34
+ onPointerDown={onPointerDown}
35
+ style={style}
36
+ >
37
+ <Box>{leftControls}</Box>
38
+ <Text
39
+ flex="auto"
40
+ display="flex"
41
+ alignItems="center"
42
+ justifyContent="center"
43
+ >
44
+ {label}
45
+ </Text>
46
+ <Box style={{ pointerEvents: "auto" }}>{rightControls}</Box>
47
+ </Box>
48
+ );
49
+ };
50
+
51
+ // DraggablePanel Component
52
+ interface DraggablePanelProps {
53
+ children: React.ReactNode;
54
+ label?: string;
55
+ parentRef: React.RefObject<HTMLDivElement>;
56
+ topSafeZone?: number;
57
+ safeZone?: {
58
+ top?: number;
59
+ left?: number;
60
+ bottom?: number;
61
+ right?: number;
62
+ };
63
+ initial?: { width?: string; maxHeight?: string; x?: number; y?: number };
64
+ [key: string]: any;
65
+ }
66
+
67
+ const DraggablePanel: React.FC<DraggablePanelProps> = ({
68
+ children,
69
+ parentRef,
70
+ label = "Panel",
71
+ topSafeZone = 16,
72
+ safeZone = { top: 16, left: 16, bottom: 16, right: 16 },
73
+ initial = {
74
+ width: "30rem",
75
+ maxHeight: "40rem",
76
+ x: 64,
77
+ y: 64,
78
+ },
79
+ ...rest
80
+ }) => {
81
+ const controls = useDragControls();
82
+ const sidebarRef = useRef<HTMLDivElement>(null);
83
+ const [mounted, setMounted] = useState(false);
84
+ const [constraints, setConstraints] = useState({
85
+ top: 0,
86
+ left: 0,
87
+ right: 0,
88
+ bottom: 0,
89
+ });
90
+
91
+ // Default position for SSR
92
+ const defaultPosition = {
93
+ x: (initial.x || 0) + (safeZone.left || 0),
94
+ y: (safeZone.top || 0) + (initial.y || 0),
95
+ };
96
+
97
+ // Persisted position (per label)
98
+ const [storedPos, setStoredPos] = useLocalStorage<{ x: number; y: number }>(
99
+ `draggable-pos-${label.toLowerCase().replace(/\s+/g, "-")}`,
100
+ defaultPosition,
101
+ );
102
+ const [position, setPosition] = useState<{ x: number; y: number }>(
103
+ defaultPosition,
104
+ );
105
+
106
+ const updateConstraints = () => {
107
+ if (sidebarRef.current && parentRef.current) {
108
+ const parentRect = parentRef.current.getBoundingClientRect();
109
+ const sidebarRect = sidebarRef.current.getBoundingClientRect();
110
+ const newConstraints = {
111
+ top: safeZone.top || 0,
112
+ left: safeZone.left || 0,
113
+ right: parentRect.width - sidebarRect.width - (safeZone.right || 0),
114
+ bottom: parentRect.height - sidebarRect.height - (safeZone.bottom || 0),
115
+ };
116
+ setConstraints(newConstraints);
117
+
118
+ // Clamp current position inside new constraints (e.g. after resize)
119
+ setPosition((prev) => {
120
+ const clampedX = Math.min(
121
+ Math.max(prev.x, newConstraints.left),
122
+ newConstraints.right,
123
+ );
124
+ const clampedY = Math.min(
125
+ Math.max(prev.y, newConstraints.top),
126
+ newConstraints.bottom,
127
+ );
128
+ if (clampedX !== prev.x || clampedY !== prev.y) {
129
+ const next = { x: clampedX, y: clampedY };
130
+ setStoredPos(next);
131
+ return next;
132
+ }
133
+ return prev;
134
+ });
135
+ }
136
+ };
137
+
138
+ useEffect(() => {
139
+ updateConstraints();
140
+ window.addEventListener("resize", updateConstraints);
141
+ const resizeObserver = new ResizeObserver(updateConstraints);
142
+ if (parentRef.current) {
143
+ resizeObserver.observe(parentRef.current);
144
+ }
145
+ return () => {
146
+ window.removeEventListener("resize", updateConstraints);
147
+ resizeObserver.disconnect();
148
+ };
149
+ }, [parentRef, topSafeZone]);
150
+
151
+ // Handle client-side mounting
152
+ useEffect(() => {
153
+ setMounted(true);
154
+ setPosition(storedPos);
155
+ }, []);
156
+
157
+ // Sync local state if stored position changes (external reset)
158
+ useEffect(() => {
159
+ if (mounted) {
160
+ setPosition(storedPos);
161
+ }
162
+ }, [storedPos.x, storedPos.y, mounted]);
163
+
164
+ return (
165
+ <Box
166
+ as={motion.div}
167
+ ref={sidebarRef}
168
+ position="absolute"
169
+ top={0}
170
+ left={0}
171
+ width={initial.width || "30rem"}
172
+ height="auto"
173
+ maxHeight={initial.maxHeight || "40rem"}
174
+ skin="translucent"
175
+ shape="roundedLarge"
176
+ $shadow="medium"
177
+ overflow="hidden"
178
+ display="flex"
179
+ flexDirection="column"
180
+ zIndex={99999}
181
+ drag
182
+ dragControls={controls}
183
+ dragMomentum={false}
184
+ dragElastic={0}
185
+ dragConstraints={constraints}
186
+ dragListener={false}
187
+ initial={false}
188
+ onDragStart={updateConstraints}
189
+ onDragEnd={() => {
190
+ if (sidebarRef.current && parentRef.current) {
191
+ const parentRect = parentRef.current.getBoundingClientRect();
192
+ const panelRect = sidebarRef.current.getBoundingClientRect();
193
+ const x = panelRect.left - parentRect.left;
194
+ const y = panelRect.top - parentRect.top;
195
+ const clampedX = Math.min(
196
+ Math.max(x, constraints.left),
197
+ constraints.right,
198
+ );
199
+ const clampedY = Math.min(
200
+ Math.max(y, constraints.top),
201
+ constraints.bottom,
202
+ );
203
+ const next = { x: clampedX, y: clampedY };
204
+ setPosition(next);
205
+ setStoredPos(next);
206
+ }
207
+ updateConstraints();
208
+ }}
209
+ style={{
210
+ pointerEvents: "auto",
211
+ x: mounted ? position.x : defaultPosition.x,
212
+ y: mounted ? position.y : defaultPosition.y,
213
+ }}
214
+ {...rest}
215
+ >
216
+ <PanelHeader
217
+ label={label}
218
+ leftControls={
219
+ <Button
220
+ $size="icon.xsmall"
221
+ variant="ghost"
222
+ style={{ pointerEvents: "none" }}
223
+ >
224
+ <IconMove3 width="16" height="16" />
225
+ </Button>
226
+ }
227
+ rightControls={
228
+ <Button $size="icon.xsmall" variant="ghost">
229
+ {/*{ <IconCInfo width="16" height="16" />}*/}
230
+ </Button>
231
+ }
232
+ onPointerDown={(e) => {
233
+ e.preventDefault();
234
+ controls.start(e);
235
+ }}
236
+ style={{ cursor: "grab", touchAction: "none", userSelect: "none" }}
237
+ />
238
+ {children}
239
+ </Box>
240
+ );
241
+ };
242
+
243
+ // DraggableContainer Component
244
+ interface DraggableContainerProps {
245
+ children: React.ReactNode;
246
+ }
247
+
248
+ function DraggableContainer({ children }: DraggableContainerProps) {
249
+ const parentRef = useRef<HTMLDivElement>(null!);
250
+
251
+ return (
252
+ <Box
253
+ position="fixed"
254
+ width="100vw"
255
+ height="100vh"
256
+ ref={parentRef}
257
+ zIndex={9999}
258
+ style={{ pointerEvents: "none" }}
259
+ >
260
+ <DraggablePanel parentRef={parentRef}>{children}</DraggablePanel>
261
+ </Box>
262
+ );
263
+ }
264
+
265
+ // Export all components
266
+ export { DraggableContainer, DraggablePanel, PanelHeader };
@@ -0,0 +1,136 @@
1
+ "use client";
2
+ import React, { useRef } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { Box, Button, Text } from "@/design-system/elements";
5
+ import { AnimatePresence, motion } from "motion/react";
6
+ import { IconEAdd, IconERemove } from "@/design-system/icons";
7
+ import { useSidebar } from "@/design-system/hooks/use-sidebar";
8
+ import { useOverlay } from "@/design-system/hooks/useOverlay";
9
+ import { Backdrop, OverscrollGuard } from "@/design-system/blocks/Overlay";
10
+
11
+ interface DrawerProps {
12
+ children: React.ReactNode;
13
+ title?: string;
14
+ id: string;
15
+ width?: string | number | object;
16
+ placement?: "left" | "right";
17
+ }
18
+
19
+ export const Drawer: React.FC<DrawerProps> = ({
20
+ children,
21
+ title = "Drawer",
22
+ id,
23
+ width = "350px",
24
+ placement = "left",
25
+ }) => {
26
+ const { isSidebarOpen, toggleSidebar } = useSidebar(id);
27
+ const drawerRef = useRef<HTMLDivElement>(null);
28
+ const { mounted, isStandalone } = useOverlay(isSidebarOpen);
29
+
30
+ // Compute side-dependent positioning and animation
31
+ const sidePositionProps =
32
+ placement === "right"
33
+ ? { right: 0 as const, left: undefined }
34
+ : { left: 0 as const, right: undefined };
35
+
36
+ const motionProps =
37
+ placement === "right"
38
+ ? { initial: { x: "100%" }, animate: { x: 0 }, exit: { x: "100%" } }
39
+ : { initial: { x: "-100%" }, animate: { x: 0 }, exit: { x: "-100%" } };
40
+
41
+ if (!mounted) return null;
42
+
43
+ const content = (
44
+ <AnimatePresence>
45
+ {isSidebarOpen && (
46
+ <>
47
+ <Backdrop
48
+ bg="rgba(0, 0, 0, 0.5)"
49
+ zIndex={10002}
50
+ onClick={() => toggleSidebar()}
51
+ />
52
+
53
+ {/* Drawer */}
54
+ <Box
55
+ as={motion.div}
56
+ ref={drawerRef}
57
+ position="fixed"
58
+ top="0"
59
+ {...sidePositionProps}
60
+ width={width}
61
+ height="100dvh"
62
+ bg="surface"
63
+ skin="translucent"
64
+ zIndex={10009}
65
+ {...motionProps}
66
+ transition={{ type: "spring", damping: 30, stiffness: 300 }}
67
+ $shadow="medium"
68
+ display="flex"
69
+ flexDirection="column"
70
+ style={{
71
+ paddingTop: isStandalone
72
+ ? "env(safe-area-inset-top, 0px)"
73
+ : undefined,
74
+ paddingBottom: "env(safe-area-inset-bottom, 0px)",
75
+ }}
76
+ >
77
+ {/* Header */}
78
+ <Box
79
+ pt="small"
80
+ px="medium"
81
+ display="flex"
82
+ justifyContent="space-between"
83
+ alignItems="center"
84
+ >
85
+ <Text fontSize="medium" fontWeight="bold" color="primary">
86
+ {title}
87
+ </Text>
88
+ <Button
89
+ onClick={() => toggleSidebar()}
90
+ variant="ghost"
91
+ $size="icon.xsmall"
92
+ aria-label="Close drawer"
93
+ >
94
+ <Box as={IconERemove} size="1.25rem" />
95
+ </Button>
96
+ </Box>
97
+ {children}
98
+ </Box>
99
+
100
+ <OverscrollGuard />
101
+ </>
102
+ )}
103
+ </AnimatePresence>
104
+ );
105
+
106
+ return createPortal(content, document.body);
107
+ };
108
+
109
+ export const DrawerButton: React.FC<{
110
+ drawerId: string;
111
+ label?: string;
112
+ icon?: React.ReactNode;
113
+ [key: string]: any;
114
+ }> = ({
115
+ drawerId,
116
+ label = "Open",
117
+ icon = <Box as={IconEAdd} size="1rem" />,
118
+ ...props
119
+ }) => {
120
+ const { isSidebarOpen, toggleSidebar } = useSidebar(drawerId);
121
+
122
+ return (
123
+ <Button
124
+ variant={isSidebarOpen ? "primary" : "ghost"}
125
+ $size="xxsmall"
126
+ onClick={() => toggleSidebar()}
127
+ display="flex"
128
+ alignItems="center"
129
+ gap="xxsmall"
130
+ {...props}
131
+ >
132
+ {icon}
133
+ <Text display={{ _: "none", md: "initial" }}>{label}</Text>
134
+ </Button>
135
+ );
136
+ };