@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,116 @@
1
+ "use client";
2
+ import React from "react";
3
+ import styled, { css, keyframes } from "styled-components";
4
+ import { Box } from "@/design-system/elements";
5
+
6
+ type MarqueeProps = {
7
+ children: React.ReactNode;
8
+ speed?: number;
9
+ direction?: "left" | "right";
10
+ pauseOnHover?: boolean;
11
+ gap?: string | number;
12
+ repeat?: number;
13
+ fadeEdges?: boolean;
14
+ className?: string;
15
+ };
16
+
17
+ const scrollMarquee = keyframes`
18
+ from {
19
+ transform: translate3d(0, 0, 0);
20
+ }
21
+
22
+ to {
23
+ transform: translate3d(calc(-100% / var(--marquee-repeat)), 0, 0);
24
+ }
25
+ `;
26
+
27
+ const MarqueeRoot = styled(Box)<{ $pauseOnHover: boolean; $fadeEdges: boolean }>`
28
+ overflow: hidden;
29
+
30
+ ${(props) =>
31
+ props.$fadeEdges &&
32
+ css`
33
+ mask-image: linear-gradient(
34
+ to right,
35
+ transparent 0%,
36
+ black 10%,
37
+ black 90%,
38
+ transparent 100%
39
+ );
40
+ -webkit-mask-image: linear-gradient(
41
+ to right,
42
+ transparent 0%,
43
+ black 10%,
44
+ black 90%,
45
+ transparent 100%
46
+ );
47
+ `}
48
+
49
+ ${(props) =>
50
+ props.$pauseOnHover &&
51
+ css`
52
+ &:hover [data-marquee-track="true"] {
53
+ animation-play-state: paused;
54
+ }
55
+ `}
56
+ `;
57
+
58
+ const MarqueeTrack = styled.div<{
59
+ $duration: number;
60
+ $direction: "left" | "right";
61
+ }>`
62
+ display: flex;
63
+ width: max-content;
64
+ will-change: transform;
65
+ animation: ${scrollMarquee} ${(props) => props.$duration}s linear infinite;
66
+ animation-direction: ${(props) =>
67
+ props.$direction === "right" ? "reverse" : "normal"};
68
+ `;
69
+
70
+ export default function Marquee({
71
+ children,
72
+ speed = 50,
73
+ direction = "left",
74
+ pauseOnHover = false,
75
+ gap = "1rem",
76
+ repeat = 2,
77
+ fadeEdges = true,
78
+ className,
79
+ }: MarqueeProps) {
80
+ const duration = 20 * (100 / speed);
81
+ const safeRepeat = Math.max(2, repeat);
82
+ const rootStyle = {
83
+ "--marquee-repeat": String(safeRepeat),
84
+ } as React.CSSProperties;
85
+
86
+ return (
87
+ <MarqueeRoot
88
+ position="relative"
89
+ width="100%"
90
+ className={className}
91
+ style={rootStyle}
92
+ $pauseOnHover={pauseOnHover}
93
+ $fadeEdges={fadeEdges}
94
+ >
95
+ <MarqueeTrack
96
+ data-marquee-track="true"
97
+ $duration={duration}
98
+ $direction={direction}
99
+ >
100
+ {Array.from({ length: safeRepeat }).map((_, index) => (
101
+ <Box
102
+ key={index}
103
+ display="flex"
104
+ flexWrap="nowrap"
105
+ gap={gap}
106
+ flexShrink={0}
107
+ pr={gap}
108
+ aria-hidden={index > 0}
109
+ >
110
+ {children}
111
+ </Box>
112
+ ))}
113
+ </MarqueeTrack>
114
+ </MarqueeRoot>
115
+ );
116
+ }
@@ -0,0 +1,199 @@
1
+ "use client"
2
+ import React, { forwardRef } from "react"
3
+ import {
4
+ NumericFormat,
5
+ PatternFormat,
6
+ NumericFormatProps,
7
+ PatternFormatProps,
8
+ } from "react-number-format"
9
+ import { Field } from "@/design-system/elements"
10
+
11
+ // Preset configurations for common mask types
12
+ export const maskPresets = {
13
+ currency: {
14
+ type: "numeric" as const,
15
+ prefix: "$",
16
+ thousandSeparator: ",",
17
+ decimalSeparator: ".",
18
+ decimalScale: 2,
19
+ fixedDecimalScale: true,
20
+ allowNegative: false,
21
+ },
22
+ percentage: {
23
+ type: "numeric" as const,
24
+ suffix: "%",
25
+ decimalScale: 2,
26
+ fixedDecimalScale: false,
27
+ allowNegative: false,
28
+ isAllowed: (values: any) => {
29
+ const { floatValue } = values
30
+ return floatValue === undefined || (floatValue >= 0 && floatValue <= 100)
31
+ },
32
+ },
33
+ phone: {
34
+ type: "pattern" as const,
35
+ format: "(###) ###-####",
36
+ mask: "_",
37
+ allowEmptyFormatting: false,
38
+ },
39
+ date: {
40
+ type: "pattern" as const,
41
+ format: "##/##/####",
42
+ placeholder: "MM/DD/YYYY",
43
+ mask: "_",
44
+ allowEmptyFormatting: false,
45
+ },
46
+ zip: {
47
+ type: "pattern" as const,
48
+ format: "#####",
49
+ mask: "_",
50
+ allowEmptyFormatting: false,
51
+ },
52
+ zipPlus4: {
53
+ type: "pattern" as const,
54
+ format: "#####-####",
55
+ mask: "_",
56
+ allowEmptyFormatting: false,
57
+ },
58
+ ssn: {
59
+ type: "pattern" as const,
60
+ format: "###-##-####",
61
+ mask: "_",
62
+ allowEmptyFormatting: false,
63
+ },
64
+ ein: {
65
+ type: "pattern" as const,
66
+ format: "##-#######",
67
+ mask: "_",
68
+ allowEmptyFormatting: false,
69
+ },
70
+ creditCard: {
71
+ type: "pattern" as const,
72
+ format: "#### #### #### ####",
73
+ mask: "_",
74
+ allowEmptyFormatting: false,
75
+ },
76
+ time12: {
77
+ type: "pattern" as const,
78
+ format: "##:## ##",
79
+ placeholder: "HH:MM AM",
80
+ mask: "_",
81
+ allowEmptyFormatting: false,
82
+ },
83
+ time24: {
84
+ type: "pattern" as const,
85
+ format: "##:##",
86
+ placeholder: "HH:MM",
87
+ mask: "_",
88
+ allowEmptyFormatting: false,
89
+ },
90
+ decimal: {
91
+ type: "numeric" as const,
92
+ decimalScale: 4,
93
+ fixedDecimalScale: false,
94
+ allowNegative: true,
95
+ thousandSeparator: ",",
96
+ },
97
+ integer: {
98
+ type: "numeric" as const,
99
+ decimalScale: 0,
100
+ allowNegative: false,
101
+ thousandSeparator: ",",
102
+ },
103
+ year: {
104
+ type: "numeric" as const,
105
+ decimalScale: 0,
106
+ allowNegative: false,
107
+ isAllowed: (values: any) => {
108
+ const { floatValue } = values
109
+ return (
110
+ floatValue === undefined || (floatValue >= 1900 && floatValue <= 2100)
111
+ )
112
+ },
113
+ },
114
+ }
115
+
116
+ export type MaskPresetType = keyof typeof maskPresets
117
+
118
+ interface BaseMaskedFieldProps {
119
+ variant?: string
120
+ $size?: string
121
+ width?: string
122
+ preset?: MaskPresetType
123
+ // Design-system styling props that we want to forward through to Field
124
+ shape?: string
125
+ bg?: string
126
+ fontSize?: string
127
+ fontWeight?: number
128
+ textAlign?: string
129
+ }
130
+
131
+ type NumericMaskedFieldProps = BaseMaskedFieldProps &
132
+ Omit<NumericFormatProps, "customInput" | "getInputRef"> & {
133
+ maskType?: "numeric"
134
+ }
135
+
136
+ type PatternMaskedFieldProps = BaseMaskedFieldProps &
137
+ Omit<PatternFormatProps, "customInput" | "getInputRef"> & {
138
+ maskType?: "pattern"
139
+ }
140
+
141
+ export type MaskedFieldProps = NumericMaskedFieldProps | PatternMaskedFieldProps
142
+
143
+ // Create a single StyledField component that accepts styling props
144
+ const StyledField = forwardRef<HTMLInputElement, any>(
145
+ ({ variant, $size, width, ...props }, ref) => {
146
+ return (
147
+ <Field
148
+ as="input"
149
+ variant={variant}
150
+ $size={$size}
151
+ width={width}
152
+ {...props}
153
+ ref={ref}
154
+ />
155
+ )
156
+ }
157
+ )
158
+
159
+ StyledField.displayName = "StyledField"
160
+
161
+ export function MaskedField({
162
+ variant = "ghost",
163
+ $size = "medium",
164
+ width = "100%",
165
+ preset,
166
+ maskType,
167
+ ...rest
168
+ }: MaskedFieldProps) {
169
+ // Get preset configuration if provided
170
+ const presetConfig = preset ? maskPresets[preset] : null
171
+
172
+ // Determine which format type to use
173
+ const formatType = maskType || presetConfig?.type || "numeric"
174
+
175
+ // Merge preset config with custom props (custom props override preset)
176
+ const mergedProps = {
177
+ customInput: StyledField,
178
+ variant,
179
+ $size,
180
+ width,
181
+ ...(presetConfig || {}),
182
+ ...rest,
183
+ }
184
+
185
+ // Remove the 'type' property as it's not a valid prop for NumericFormat/PatternFormat
186
+ const { type, ...finalProps } = mergedProps as any
187
+ // 'type' is intentionally removed from finalProps
188
+ void type
189
+
190
+ // Render NumericFormat for currency, percentage, amounts
191
+ if (formatType === "numeric") {
192
+ return <NumericFormat {...finalProps} />
193
+ }
194
+
195
+ // Render PatternFormat for phone, date, SSN, etc.
196
+ return <PatternFormat {...finalProps} />
197
+ }
198
+
199
+ export default MaskedField
@@ -0,0 +1,246 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { motion, AnimatePresence } from "motion/react";
3
+ import { Box, Text } from "@/design-system/elements";
4
+ import { useMenu } from "./MenuContext";
5
+
6
+ interface MenuItem {
7
+ documentId: string;
8
+ title: string;
9
+ path?: string;
10
+ items?: MenuItem[];
11
+ additionalFields?: {
12
+ description?: string;
13
+ divider?: boolean;
14
+ };
15
+ menuAttached?: boolean;
16
+ order?: number;
17
+ }
18
+
19
+ interface MenuContentProps {
20
+ items?: MenuItem[];
21
+ children?: React.ReactNode;
22
+ level?: number;
23
+ }
24
+
25
+ const transition = {
26
+ duration: 0.15,
27
+ ease: "easeInOut",
28
+ };
29
+
30
+ const MenuItems: React.FC<{ items: MenuItem[]; level: number }> = ({
31
+ items,
32
+ level,
33
+ }) => {
34
+ return (
35
+ <Box
36
+ display="grid"
37
+ gridTemplateColumns={
38
+ level === 0 ? `repeat(${items.length}, 1fr)` : ` 1fr`
39
+ }
40
+ gap="small"
41
+ >
42
+ {items.map((item, index) => (
43
+ <Box
44
+ key={item.documentId}
45
+ borderLeft={level === 0 && index > 0 ? "1px solid" : undefined}
46
+ borderColor="transparent.light.1"
47
+ pl={level === 0 && index > 0 ? "small" : undefined}
48
+ width="100%"
49
+ display="flex"
50
+ flexDirection="column"
51
+ gap="xxxsmall"
52
+ >
53
+ {item.items && item.items.length > 0 ? (
54
+ // Category header with nested items
55
+ <Box p="small" display="flex" flexDirection="column" gap="xxxsmall">
56
+ <Text
57
+ as="h6"
58
+ color="tertiary"
59
+ fontSize="small"
60
+ fontWeight="600"
61
+ m="0"
62
+ style={{ cursor: "default" }}
63
+ >
64
+ {item.title}
65
+ </Text>
66
+ {item.additionalFields?.description && (
67
+ <Text color="tertiary" fontSize="xsmall">
68
+ {item.additionalFields.description}
69
+ </Text>
70
+ )}
71
+
72
+ <MenuItems items={item.items} level={level + 1} />
73
+ </Box>
74
+ ) : (
75
+ // Regular link
76
+ <Box
77
+ as="a"
78
+ href={item.path}
79
+ display="flex"
80
+ flexDirection="column"
81
+ gap="xxxsmall"
82
+ shape="rounded"
83
+ p="xsmall"
84
+ width={"100%"}
85
+ minWidth={"15rem"}
86
+ interactive={{
87
+ hover: { bg: "transparent.light.0" },
88
+ }}
89
+ >
90
+ <Text
91
+ // as="a"
92
+ // href={item.path}
93
+ color="primary"
94
+ fontSize="small"
95
+ fontWeight="bold"
96
+ >
97
+ {item.title}
98
+ </Text>
99
+ {item.additionalFields?.description && (
100
+ <Text color="tertiary" fontSize="xsmall">
101
+ {item.additionalFields.description}
102
+ </Text>
103
+ )}
104
+ </Box>
105
+ )}
106
+ </Box>
107
+ ))}
108
+ </Box>
109
+ );
110
+ };
111
+
112
+ export const MenuContent: React.FC<MenuContentProps> = ({
113
+ items,
114
+ children,
115
+ level = 0,
116
+ }) => {
117
+ const { selected, dir } = useMenu();
118
+ const [left, setLeft] = useState(0);
119
+ //console.log("MenuContent rendered:", { items, selected, dir });
120
+ useEffect(() => {
121
+ if (selected) {
122
+ const hoveredTab = document.getElementById(`shift-tab-${selected}`);
123
+ const overlayContent = document.getElementById("overlay-content");
124
+
125
+ if (!hoveredTab || !overlayContent) return;
126
+
127
+ const tabRect = hoveredTab.getBoundingClientRect();
128
+ const { left: contentLeft } = overlayContent.getBoundingClientRect();
129
+ const tabCenter = tabRect.left + tabRect.width / 2 - contentLeft;
130
+
131
+ setLeft(tabCenter);
132
+ }
133
+ }, [selected]);
134
+
135
+ const getInitialX = () => {
136
+ switch (dir) {
137
+ case "left":
138
+ return 20;
139
+ case "right":
140
+ return -20;
141
+ case "initial":
142
+ return 0;
143
+ default:
144
+ return 0;
145
+ }
146
+ };
147
+
148
+ return (
149
+ <Box
150
+ as={motion.div}
151
+ id="overlay-content"
152
+ initial={{
153
+ opacity: 0,
154
+ y: dir === "initial" ? 10 : 0,
155
+ scale: 0.98,
156
+ }}
157
+ animate={{
158
+ opacity: 1,
159
+ y: 0,
160
+ x: 0,
161
+ scale: 1,
162
+ }}
163
+ exit={{
164
+ opacity: 0,
165
+ y: dir === "initial" ? 10 : 0,
166
+ scale: 0.98,
167
+ }}
168
+ transition={transition}
169
+ position="absolute"
170
+ left="0"
171
+ top="calc(100% + 0.75rem)"
172
+ minWidth="25rem"
173
+ width="40rem"
174
+ p="xxsmall"
175
+ border="1px solid"
176
+ bg="overlay"
177
+ borderColor="transparent.light.1"
178
+ shape="roundedLarge"
179
+ $shadow="large"
180
+ >
181
+ <Bridge />
182
+ <Nub left={left} />
183
+ <Box
184
+ as={motion.div}
185
+ p="xxsmall"
186
+ border="1px solid"
187
+ borderColor="transparent.light.1"
188
+ bg="surface"
189
+ shape="rounded"
190
+ >
191
+ <AnimatePresence mode="popLayout">
192
+ <Box
193
+ as={motion.div}
194
+ key={selected}
195
+ initial={{
196
+ opacity: 0,
197
+ x: getInitialX(),
198
+ }}
199
+ animate={{
200
+ opacity: 1,
201
+ x: 0,
202
+ }}
203
+ exit={{
204
+ opacity: 0,
205
+ x: 0,
206
+ }}
207
+ transition={{
208
+ duration: 0.125,
209
+ ease: "easeOut",
210
+ }}
211
+ >
212
+ {items ? <MenuItems items={items} level={level} /> : children}
213
+ </Box>
214
+ </AnimatePresence>
215
+ </Box>
216
+ </Box>
217
+ );
218
+ };
219
+ const Bridge = () => (
220
+ <Box position="absolute" top="-16px" left="0" right="0" height="16px" />
221
+ );
222
+
223
+ const Nub = ({ left }: { left: number }) => {
224
+ return (
225
+ <Box
226
+ as={motion.span}
227
+ position="absolute"
228
+ left="50%"
229
+ top="0"
230
+ width="1rem"
231
+ height="1rem"
232
+ style={{
233
+ clipPath: "polygon(0 0, 100% 0, 50% 50%, 0% 100%)",
234
+ transform: "translate(-50%, -50%) rotate(45deg)",
235
+ }}
236
+ animate={{ left: `${left}px` }}
237
+ transition={transition}
238
+ border="1px solid"
239
+ bg="overlay"
240
+ borderColor="transparent.light.1"
241
+ borderRadius={"2px"}
242
+ />
243
+ );
244
+ };
245
+
246
+ export default MenuContent;
@@ -0,0 +1,34 @@
1
+ // src/design-system/blocks/Menu/MenuContext.tsx
2
+ import React, { createContext, useContext, useState } from "react";
3
+
4
+ type Direction = "left" | "right" | "initial" | null;
5
+
6
+ interface MenuContextValue {
7
+ selected: string | number | null;
8
+ setSelected: (id: string | number | null) => void;
9
+ dir: Direction;
10
+ setDir: (dir: Direction) => void;
11
+ }
12
+
13
+ const MenuContext = createContext<MenuContextValue | undefined>(undefined);
14
+
15
+ export const useMenu = () => {
16
+ const context = useContext(MenuContext);
17
+ if (!context) {
18
+ throw new Error("useMenu must be used within a MenuProvider");
19
+ }
20
+ return context;
21
+ };
22
+
23
+ export const MenuProvider: React.FC<{ children: React.ReactNode }> = ({
24
+ children,
25
+ }) => {
26
+ const [selected, setSelected] = useState<string | number | null>(null);
27
+ const [dir, setDir] = useState<Direction>(null);
28
+ // console.log("DIRECTION", dir);
29
+ return (
30
+ <MenuContext.Provider value={{ selected, setSelected, dir, setDir }}>
31
+ {children}
32
+ </MenuContext.Provider>
33
+ );
34
+ };
@@ -0,0 +1,104 @@
1
+ import React, { useState, useContext } from "react";
2
+ import { motion } from "motion/react";
3
+ import { Box, Text } from "@/design-system/elements";
4
+ import { useMenu } from "./MenuContext";
5
+ import Link from "next/link";
6
+ import { ThemeContext } from "styled-components";
7
+
8
+ interface MenuItemProps {
9
+ id: string | number;
10
+ title: string;
11
+ path?: string;
12
+ children?: React.ReactNode;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export const MenuItem: React.FC<MenuItemProps> = ({
17
+ id,
18
+ title,
19
+ path,
20
+ children,
21
+ disabled,
22
+ }) => {
23
+ const { selected, setSelected, setDir } = useMenu();
24
+ const [isHovered, setIsHovered] = useState(false);
25
+ const theme: any = useContext(ThemeContext);
26
+ const handleSelect = () => {
27
+ if (!children) {
28
+ setSelected(null);
29
+ return;
30
+ }
31
+
32
+ if (selected === null) {
33
+ setDir("initial");
34
+ } else {
35
+ setDir(selected > id ? "right" : "left");
36
+ }
37
+ setSelected(id);
38
+ };
39
+
40
+ return (
41
+ <Box
42
+ position="relative"
43
+ px="small"
44
+ py="xxsmall"
45
+ display="flex"
46
+ alignItems="center"
47
+ cursor="pointer"
48
+ flex="none"
49
+ onClick={handleSelect}
50
+ onMouseEnter={() => {
51
+ setIsHovered(true);
52
+ handleSelect();
53
+ }}
54
+ onMouseLeave={() => {
55
+ setIsHovered(false);
56
+ }}
57
+ disabled={disabled}
58
+ >
59
+ {path ? (
60
+ <Text
61
+ as={Link}
62
+ href={path}
63
+ id={`shift-tab-${id}`}
64
+ cursor="pointer"
65
+ textDecoration="none"
66
+ color={isHovered ? "base" : "primary"}
67
+ interactive={{ hover: { color: "base" } }}
68
+ fontSize="small"
69
+ fontWeight="600"
70
+ onMouseEnter={() => setSelected(null)}
71
+ >
72
+ {title}
73
+ </Text>
74
+ ) : (
75
+ <Text
76
+ id={`shift-tab-${id}`}
77
+ color={selected === id || isHovered ? "base" : "primary"}
78
+ fontSize="small"
79
+ fontWeight="600"
80
+ >
81
+ {title}
82
+ </Text>
83
+ )}
84
+
85
+ {(selected === id || isHovered) && (
86
+ <Box
87
+ as={motion.div}
88
+ layout
89
+ layoutId="menu-item"
90
+ bg="brand"
91
+ position="absolute"
92
+ width="100%"
93
+ height="100%"
94
+ top="0"
95
+ left="0"
96
+ shape="pill"
97
+ zIndex={-1}
98
+ transition={{ duration: 0.15, ease: "easeInOut" }}
99
+ {...theme?.header?.public?.menu?.item?.highlight}
100
+ />
101
+ )}
102
+ </Box>
103
+ );
104
+ };