@reactberry/system 2.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/package.json +74 -0
- package/src/blocks/Accordion/index.tsx +158 -0
- package/src/blocks/AnimatedCarousel/index.tsx +188 -0
- package/src/blocks/AppleGlow/index.tsx +144 -0
- package/src/blocks/Avatar/index.tsx +167 -0
- package/src/blocks/Await/index.tsx +45 -0
- package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
- package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
- package/src/blocks/Cards/InfoCard/index.tsx +206 -0
- package/src/blocks/Cards/TickerCard/index.tsx +125 -0
- package/src/blocks/Carousel/index.tsx +216 -0
- package/src/blocks/Checkbox/index.tsx +101 -0
- package/src/blocks/Collection/index.tsx +59 -0
- package/src/blocks/Container/index.tsx +55 -0
- package/src/blocks/Controls/Control.tsx +67 -0
- package/src/blocks/Controls/index.tsx +11 -0
- package/src/blocks/CyclingNumber/index.tsx +78 -0
- package/src/blocks/DisplaySet/index.tsx +42 -0
- package/src/blocks/Divider/index.tsx +14 -0
- package/src/blocks/Draggable/index.tsx +266 -0
- package/src/blocks/Drawer/index.tsx +136 -0
- package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
- package/src/blocks/DynamicIsland/index.tsx +2 -0
- package/src/blocks/Fader/index.tsx +145 -0
- package/src/blocks/FamilyDrawer/README.md +116 -0
- package/src/blocks/FamilyDrawer/example.tsx +108 -0
- package/src/blocks/FamilyDrawer/index.tsx +119 -0
- package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
- package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
- package/src/blocks/FieldSet/index.tsx +173 -0
- package/src/blocks/Filesystem/index.tsx +198 -0
- package/src/blocks/Gallery/Carousel/index.tsx +257 -0
- package/src/blocks/Gallery/Modal/index.tsx +83 -0
- package/src/blocks/Gallery/index.tsx +57 -0
- package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
- package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
- package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
- package/src/blocks/Gallery/utils/range.ts +11 -0
- package/src/blocks/GradientMesh/index.tsx +106 -0
- package/src/blocks/Group/index.tsx +152 -0
- package/src/blocks/Heading/index.tsx +111 -0
- package/src/blocks/HorizontalScroller/index.tsx +135 -0
- package/src/blocks/Icon/index.tsx +45 -0
- package/src/blocks/Indicator/index.tsx +27 -0
- package/src/blocks/InlineEditor/index.tsx +216 -0
- package/src/blocks/List/index.tsx +657 -0
- package/src/blocks/Main/index.tsx +17 -0
- package/src/blocks/Marquee/index.tsx +116 -0
- package/src/blocks/MaskedField/index.tsx +199 -0
- package/src/blocks/Menu/MenuContent.tsx +246 -0
- package/src/blocks/Menu/MenuContext.tsx +34 -0
- package/src/blocks/Menu/MenuItem.tsx +104 -0
- package/src/blocks/Menu/index.tsx +60 -0
- package/src/blocks/Modal/index.tsx +268 -0
- package/src/blocks/MorphingPopover/index.tsx +294 -0
- package/src/blocks/Overlay/Backdrop.tsx +48 -0
- package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
- package/src/blocks/Overlay/index.ts +2 -0
- package/src/blocks/Parallax/index.tsx +117 -0
- package/src/blocks/ParallaxSection/index.tsx +61 -0
- package/src/blocks/Placeholder/index.tsx +48 -0
- package/src/blocks/Popover/index.tsx +402 -0
- package/src/blocks/Progress/getProgressColor.ts +61 -0
- package/src/blocks/Progress/index.tsx +179 -0
- package/src/blocks/ProgressiveBlur/index.tsx +75 -0
- package/src/blocks/README.md +15 -0
- package/src/blocks/RenderAsset/index.tsx +18 -0
- package/src/blocks/ScrollContainer/index.tsx +93 -0
- package/src/blocks/ShinyText/index.tsx +72 -0
- package/src/blocks/Skeleton/index.tsx +71 -0
- package/src/blocks/Slider/SliderControls.tsx +119 -0
- package/src/blocks/Slider/index.tsx +140 -0
- package/src/blocks/Slider/useSlider.ts +126 -0
- package/src/blocks/Slideshow/index.tsx +177 -0
- package/src/blocks/Spotlight/index.tsx +144 -0
- package/src/blocks/Steps/StepIndicator.tsx +149 -0
- package/src/blocks/Steps/StepProgress.tsx +164 -0
- package/src/blocks/Steps/Steps.tsx +197 -0
- package/src/blocks/Steps/StepsNav.tsx +30 -0
- package/src/blocks/Steps/StepsTracker.tsx +80 -0
- package/src/blocks/Steps/hooks.ts +71 -0
- package/src/blocks/Steps/index.tsx +16 -0
- package/src/blocks/Steps/types.ts +71 -0
- package/src/blocks/StickySectionStack/index.tsx +136 -0
- package/src/blocks/Switch/index.tsx +85 -0
- package/src/blocks/SystemNotice/index.tsx +81 -0
- package/src/blocks/Table/README.md +251 -0
- package/src/blocks/Table/Table.tsx +207 -0
- package/src/blocks/Table/TablePagination.tsx +189 -0
- package/src/blocks/Table/index.ts +33 -0
- package/src/blocks/Table/useTableControls.ts +331 -0
- package/src/blocks/Tag/index.tsx +27 -0
- package/src/blocks/TextBreak/index.tsx +96 -0
- package/src/blocks/TextReveal/index.tsx +104 -0
- package/src/blocks/Thumbnail/index.tsx +26 -0
- package/src/blocks/Ticker/index.tsx +112 -0
- package/src/blocks/Toast/index.tsx +77 -0
- package/src/blocks/Tooltip/index.tsx +174 -0
- package/src/blocks/Underlay/index.tsx +104 -0
- package/src/blocks/Upload/Dropzone.tsx +92 -0
- package/src/blocks/Upload/UploadBtn.tsx +38 -0
- package/src/blocks/Upload/index.tsx +61 -0
- package/src/blocks/Upload/types.ts +37 -0
- package/src/blocks/VideoMarquee/index.tsx +511 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/pagination/Pagination.tsx +148 -0
- package/src/blocks/pagination/PaginationList.tsx +41 -0
- package/src/blocks/pagination/index.ts +2 -0
- package/src/charts/BarChart.tsx +63 -0
- package/src/charts/PieChart.tsx +39 -0
- package/src/charts/index.ts +3 -0
- package/src/charts/utils.ts +103 -0
- package/src/docs/README.md +373 -0
- package/src/docs/reference/README.md +299 -0
- package/src/elements/box.ts +163 -0
- package/src/elements/button.ts +49 -0
- package/src/elements/field.ts +129 -0
- package/src/elements/index.ts +8 -0
- package/src/elements/text.ts +47 -0
- package/src/elements/utils.js +97 -0
- package/src/hooks/use-copy-to-clipboard.tsx +33 -0
- package/src/hooks/use-enter-submit.tsx +23 -0
- package/src/hooks/use-local-storage.ts +42 -0
- package/src/hooks/use-sidebar.tsx +109 -0
- package/src/hooks/useAnimatedText.ts +32 -0
- package/src/hooks/useAutosizeTextArea.ts +45 -0
- package/src/hooks/useBreakpoint.tsx +123 -0
- package/src/hooks/useClickOutside.tsx +38 -0
- package/src/hooks/useHover.tsx +33 -0
- package/src/hooks/useHoverList.tsx +17 -0
- package/src/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/hooks/useKeypress.ts +27 -0
- package/src/hooks/useOverlay.ts +32 -0
- package/src/hooks/useReducedMotion.ts +25 -0
- package/src/hooks/useStandaloneMode.ts +35 -0
- package/src/hooks/useTouchDevice.ts +34 -0
- package/src/icons/index.tsx +129 -0
- package/src/index.ts +12 -0
- package/src/providers/DesignSystemProvider.tsx +35 -0
- package/src/providers/StyledComponentsRegistry.tsx +30 -0
- package/src/providers/index.ts +2 -0
- package/src/themes/README.md +30 -0
- package/src/themes/default/assets/badge-avatar.tsx +45 -0
- package/src/themes/default/assets/logo.tsx +42 -0
- package/src/themes/default/global.ts +138 -0
- package/src/themes/default/modes/dark/config.js +49 -0
- package/src/themes/default/modes/dark/skins.js +631 -0
- package/src/themes/default/modes/dark/theme.js +87 -0
- package/src/themes/default/modes/light/config.js +48 -0
- package/src/themes/default/modes/light/skins.js +1026 -0
- package/src/themes/default/modes/light/theme.js +74 -0
- package/src/themes/default/tokens/controls.js +53 -0
- package/src/themes/default/tokens/shadows.js +63 -0
- package/src/themes/default/tokens/shapes.js +37 -0
- package/src/themes/default/tokens/space.js +143 -0
- package/src/themes/default/tokens/spectre.js +16 -0
- package/src/themes/default/utils.js +523 -0
- package/src/themes/index.ts +11 -0
- package/src/types.ts +394 -0
- package/src/utils/overlayTheme.ts +61 -0
- package/src/utils/pickColor.ts +15 -0
- package/tsconfig.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# reactberry
|
|
2
|
+
|
|
3
|
+
`@reactberry/system` is the single-package Next.js UI library for Reactberry projects.
|
|
4
|
+
|
|
5
|
+
## Package model
|
|
6
|
+
|
|
7
|
+
- This package intentionally depends on Next.js APIs like `next/image` and `next/navigation`.
|
|
8
|
+
- It ships source files from `src/` for Next.js apps to transpile.
|
|
9
|
+
- Consumer apps should add `@reactberry/system` to `transpilePackages`.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Install `@reactberry/system` together with its peer dependencies in your consuming app:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @reactberry/system@beta styled-components next react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If you want the exact current prerelease, install `@reactberry/system@2.0.0-beta` instead.
|
|
20
|
+
|
|
21
|
+
- `next`
|
|
22
|
+
- `react`
|
|
23
|
+
- `react-dom`
|
|
24
|
+
- `styled-components`
|
|
25
|
+
|
|
26
|
+
## Next.js setup
|
|
27
|
+
|
|
28
|
+
Add `@reactberry/system` to `transpilePackages` in your app's `next.config.js` or `next.config.ts`.
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
Import components from `@reactberry/system` and wrap your app with `DesignSystemProvider`.
|
|
33
|
+
|
|
34
|
+
## Exports
|
|
35
|
+
|
|
36
|
+
- `@reactberry/system`
|
|
37
|
+
- `@reactberry/system/providers`
|
|
38
|
+
- `@reactberry/system/themes`
|
|
39
|
+
- `@reactberry/system/blocks`
|
|
40
|
+
- `@reactberry/system/elements`
|
|
41
|
+
- `@reactberry/system/icons`
|
|
42
|
+
- `@reactberry/system/charts`
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
- `src/index.ts` is the root barrel export.
|
|
47
|
+
- `DesignSystemProvider` includes the styled-components registry for Next.js usage.
|
|
48
|
+
- The package keeps the extracted design-system source as the repo's source of truth.
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactberry/system",
|
|
3
|
+
"version": "2.0.0-beta",
|
|
4
|
+
"description": "Reusable Next.js UI library package for Reactberry projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/**/*.ts",
|
|
9
|
+
"src/**/*.tsx",
|
|
10
|
+
"src/**/*.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"tsconfig.json"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": "20.x",
|
|
16
|
+
"npm": ">=9.0.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
20
|
+
"docs:dev": "npm --prefix docs run dev",
|
|
21
|
+
"docs:build": "npm --prefix docs run build",
|
|
22
|
+
"docs:typecheck": "npm --prefix docs run typecheck"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"next": "^16.0.0",
|
|
26
|
+
"react": "^19.0.0",
|
|
27
|
+
"react-dom": "^19.0.0",
|
|
28
|
+
"styled-components": "^6.1.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@floating-ui/react": "^0.26.28",
|
|
32
|
+
"@headlessui/react": "^2.2.9",
|
|
33
|
+
"@nivo/bar": "^0.99.0",
|
|
34
|
+
"@nivo/pie": "^0.99.0",
|
|
35
|
+
"@number-flow/react": "^0.5.14",
|
|
36
|
+
"@paper-design/shaders-react": "^0.0.69",
|
|
37
|
+
"chroma-js": "^2.4.2",
|
|
38
|
+
"motion": "^12.35.2",
|
|
39
|
+
"react-dropzone": "^14.4.1",
|
|
40
|
+
"react-number-format": "^5.4.4",
|
|
41
|
+
"react-swipeable": "^7.0.2",
|
|
42
|
+
"react-textarea-autosize": "^8.5.9",
|
|
43
|
+
"react-use-measure": "^2.1.7",
|
|
44
|
+
"styled-system": "^5.1.5"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20",
|
|
48
|
+
"@types/react": "^19.2.14",
|
|
49
|
+
"@types/react-dom": "^19.2.3",
|
|
50
|
+
"@types/styled-system": "^5.1.22",
|
|
51
|
+
"next": "^16.1.6",
|
|
52
|
+
"react": "^19.2.4",
|
|
53
|
+
"react-dom": "^19.2.4",
|
|
54
|
+
"styled-components": "^6.1.1",
|
|
55
|
+
"typescript": "^5"
|
|
56
|
+
},
|
|
57
|
+
"exports": {
|
|
58
|
+
".": "./src/index.ts",
|
|
59
|
+
"./providers": "./src/providers/index.ts",
|
|
60
|
+
"./themes": "./src/themes/index.ts",
|
|
61
|
+
"./blocks": "./src/blocks/index.ts",
|
|
62
|
+
"./elements": "./src/elements/index.ts",
|
|
63
|
+
"./icons": "./src/icons/index.tsx",
|
|
64
|
+
"./charts": "./src/charts/index.ts",
|
|
65
|
+
"./package.json": "./package.json"
|
|
66
|
+
},
|
|
67
|
+
"keywords": [
|
|
68
|
+
"reactberry",
|
|
69
|
+
"design-system",
|
|
70
|
+
"nextjs",
|
|
71
|
+
"react",
|
|
72
|
+
"styled-components"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box } from "@/design-system/elements";
|
|
3
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { useLocalStorage } from "@/design-system/hooks/use-local-storage";
|
|
6
|
+
|
|
7
|
+
import { IconArrowSmDown } from "@/design-system/icons";
|
|
8
|
+
import Group from "../Group";
|
|
9
|
+
|
|
10
|
+
interface AccordionItem {
|
|
11
|
+
id: string;
|
|
12
|
+
trigger: React.ReactNode;
|
|
13
|
+
content: React.ReactNode;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AccordionProps {
|
|
18
|
+
items: AccordionItem[];
|
|
19
|
+
allowMultiple?: boolean;
|
|
20
|
+
defaultOpen?: string[];
|
|
21
|
+
onToggle?: (itemId: string, isOpen: boolean, openItems: string[]) => void;
|
|
22
|
+
fontSize?: string;
|
|
23
|
+
spacing?: string;
|
|
24
|
+
persistKey?: string;
|
|
25
|
+
iconPosition?: "start" | "end";
|
|
26
|
+
containerProps?: {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
};
|
|
29
|
+
itemProps?: {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
};
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Accordion: React.FC<AccordionProps> = ({
|
|
36
|
+
items,
|
|
37
|
+
allowMultiple = true,
|
|
38
|
+
defaultOpen = [],
|
|
39
|
+
onToggle,
|
|
40
|
+
persistKey,
|
|
41
|
+
iconPosition = "end",
|
|
42
|
+
containerProps = {},
|
|
43
|
+
itemProps = {},
|
|
44
|
+
}) => {
|
|
45
|
+
const [persistedOpenItems, setPersistedOpenItems] = useLocalStorage<string[]>(
|
|
46
|
+
persistKey ? `accordion-${persistKey}` : "",
|
|
47
|
+
defaultOpen
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const [openItems, setOpenItems] = useState<Set<string>>(() => {
|
|
51
|
+
return new Set(persistKey ? persistedOpenItems : defaultOpen);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handleToggle = (itemId: string) => {
|
|
55
|
+
const item = items.find((item) => item.id === itemId);
|
|
56
|
+
if (item?.disabled) return;
|
|
57
|
+
|
|
58
|
+
setOpenItems((prevOpenItems) => {
|
|
59
|
+
const newOpenItems = new Set(prevOpenItems);
|
|
60
|
+
const isCurrentlyOpen = newOpenItems.has(itemId);
|
|
61
|
+
|
|
62
|
+
if (isCurrentlyOpen) {
|
|
63
|
+
newOpenItems.delete(itemId);
|
|
64
|
+
} else {
|
|
65
|
+
if (!allowMultiple) {
|
|
66
|
+
newOpenItems.clear();
|
|
67
|
+
}
|
|
68
|
+
newOpenItems.add(itemId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const openItemsArray = Array.from(newOpenItems);
|
|
72
|
+
onToggle?.(itemId, !isCurrentlyOpen, openItemsArray);
|
|
73
|
+
|
|
74
|
+
// Persist to localStorage if persistKey is provided
|
|
75
|
+
if (persistKey) {
|
|
76
|
+
setPersistedOpenItems(openItemsArray);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return newOpenItems;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Box
|
|
85
|
+
width="100%"
|
|
86
|
+
display="flex"
|
|
87
|
+
flexDirection={"column"}
|
|
88
|
+
{...containerProps}
|
|
89
|
+
>
|
|
90
|
+
{items.map((item) => {
|
|
91
|
+
const isOpen = openItems.has(item.id);
|
|
92
|
+
const isDisabled = item.disabled;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box
|
|
96
|
+
key={item.id}
|
|
97
|
+
as={motion.div}
|
|
98
|
+
opacity={isDisabled ? 0.5 : 1}
|
|
99
|
+
{...itemProps}
|
|
100
|
+
>
|
|
101
|
+
<Box
|
|
102
|
+
width="100%"
|
|
103
|
+
cursor={isDisabled ? "not-allowed" : "pointer"}
|
|
104
|
+
display="flex"
|
|
105
|
+
alignItems="center"
|
|
106
|
+
justifyContent={"space-between"}
|
|
107
|
+
gap="mini"
|
|
108
|
+
disabled={isDisabled}
|
|
109
|
+
onClick={() => handleToggle(item.id)}
|
|
110
|
+
>
|
|
111
|
+
<Group
|
|
112
|
+
as={motion.div}
|
|
113
|
+
justifyContent="center"
|
|
114
|
+
flex="none"
|
|
115
|
+
order={iconPosition === "start" ? 0 : 1}
|
|
116
|
+
animate={{
|
|
117
|
+
rotate: isOpen
|
|
118
|
+
? iconPosition === "start"
|
|
119
|
+
? 0
|
|
120
|
+
: 180
|
|
121
|
+
: iconPosition === "start"
|
|
122
|
+
? -90
|
|
123
|
+
: 0,
|
|
124
|
+
opacity: isOpen ? 1 : 0.5,
|
|
125
|
+
}}
|
|
126
|
+
transition={{ duration: 0.1 }}
|
|
127
|
+
>
|
|
128
|
+
<Box as={IconArrowSmDown} size="1.75em" />
|
|
129
|
+
</Group>
|
|
130
|
+
<Box flex="1" order={iconPosition === "start" ? 1 : 0}>
|
|
131
|
+
{item.trigger}
|
|
132
|
+
</Box>
|
|
133
|
+
</Box>
|
|
134
|
+
|
|
135
|
+
<AnimatePresence mode="sync">
|
|
136
|
+
{isOpen && (
|
|
137
|
+
<Box
|
|
138
|
+
as={motion.div}
|
|
139
|
+
initial={{ height: 0, opacity: 0 }}
|
|
140
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
141
|
+
exit={{ height: 0, opacity: 0 }}
|
|
142
|
+
transition={{
|
|
143
|
+
duration: 0.2,
|
|
144
|
+
ease: "easeInOut",
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{item.content}
|
|
148
|
+
</Box>
|
|
149
|
+
)}
|
|
150
|
+
</AnimatePresence>
|
|
151
|
+
</Box>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</Box>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default Accordion;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// carousel.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { Box } from "@/design-system/elements";
|
|
5
|
+
import {
|
|
6
|
+
AnimatePresence,
|
|
7
|
+
MotionConfig,
|
|
8
|
+
motion,
|
|
9
|
+
useMotionTemplate,
|
|
10
|
+
useSpring,
|
|
11
|
+
} from "motion/react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
import type { ImageType } from "@/design-system/types";
|
|
14
|
+
import { ControlLeft, ControlRight } from "../Controls/Control";
|
|
15
|
+
|
|
16
|
+
const COLLAPSED_ASPECT_RATIO = 0.5;
|
|
17
|
+
const FULL_ASPECT_RATIO = 3 / 2;
|
|
18
|
+
const MARGIN = 24;
|
|
19
|
+
const GAP = 2;
|
|
20
|
+
|
|
21
|
+
type CarouselProps = {
|
|
22
|
+
images: ImageType[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ThumbnailsProps = {
|
|
26
|
+
images: ImageType[];
|
|
27
|
+
index: number;
|
|
28
|
+
setIndex: (value: number) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default function AnimatedCarousel({ images }: CarouselProps) {
|
|
32
|
+
const [index, setIndex] = useState(0);
|
|
33
|
+
|
|
34
|
+
const x = index * 100;
|
|
35
|
+
const xSpring = useSpring(x, { bounce: 0 });
|
|
36
|
+
const xPercentage = useMotionTemplate`-${xSpring}%`;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
xSpring.set(x);
|
|
40
|
+
}, [x, xSpring]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
function handleKeyPress(e: KeyboardEvent) {
|
|
44
|
+
if (e.key === "ArrowLeft") {
|
|
45
|
+
if (index > 0) {
|
|
46
|
+
setIndex(index - 1);
|
|
47
|
+
}
|
|
48
|
+
} else if (e.key === "ArrowRight") {
|
|
49
|
+
if (index < images.length - 1) {
|
|
50
|
+
setIndex(index + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener("keydown", handleKeyPress);
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
document.removeEventListener("keydown", handleKeyPress);
|
|
59
|
+
};
|
|
60
|
+
}, [index, images.length]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<MotionConfig transition={{ type: "spring", bounce: 0 }}>
|
|
64
|
+
<Box
|
|
65
|
+
display={"flex"}
|
|
66
|
+
flexDirection={"column"}
|
|
67
|
+
justifyContent={"space-between"}
|
|
68
|
+
height={"100%"}
|
|
69
|
+
>
|
|
70
|
+
<Box position="relative" overflow={"hidden"}>
|
|
71
|
+
<Box as={motion.div} style={{ x: xPercentage }} display={"flex"}>
|
|
72
|
+
{images.map((image, i) => (
|
|
73
|
+
<Box
|
|
74
|
+
as={motion.img}
|
|
75
|
+
key={image.id}
|
|
76
|
+
src={image.url}
|
|
77
|
+
animate={{ opacity: i === index ? 1 : 0.4 }}
|
|
78
|
+
aspect={"1/0.85"}
|
|
79
|
+
width="100%"
|
|
80
|
+
height="100vh"
|
|
81
|
+
maxHeight={"70vh"}
|
|
82
|
+
style={{ objectFit: "cover" }}
|
|
83
|
+
flex="none"
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</Box>
|
|
87
|
+
|
|
88
|
+
<AnimatePresence initial={false}>
|
|
89
|
+
{index > 0 && (
|
|
90
|
+
<ControlLeft
|
|
91
|
+
initial={{ opacity: 0 }}
|
|
92
|
+
animate={{ opacity: 0.7 }}
|
|
93
|
+
exit={{ opacity: 0, pointerEvents: "none" }}
|
|
94
|
+
whileHover={{ opacity: 1, scale: 1.1 }}
|
|
95
|
+
ml="small"
|
|
96
|
+
onClick={() => setIndex(index - 1)}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</AnimatePresence>
|
|
100
|
+
|
|
101
|
+
<AnimatePresence initial={false}>
|
|
102
|
+
{index + 1 < images.length && (
|
|
103
|
+
<ControlRight
|
|
104
|
+
initial={{ opacity: 0 }}
|
|
105
|
+
animate={{ opacity: 0.7 }}
|
|
106
|
+
exit={{ opacity: 0, pointerEvents: "none" }}
|
|
107
|
+
whileHover={{ opacity: 1, scale: 1.1 }}
|
|
108
|
+
mr="small"
|
|
109
|
+
left="auto"
|
|
110
|
+
right="0"
|
|
111
|
+
onClick={() => setIndex(index + 1)}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
</AnimatePresence>
|
|
115
|
+
</Box>
|
|
116
|
+
|
|
117
|
+
<Thumbnails images={images} index={index} setIndex={setIndex} />
|
|
118
|
+
</Box>
|
|
119
|
+
</MotionConfig>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Thumbnails({ images, index, setIndex }: ThumbnailsProps) {
|
|
124
|
+
const x =
|
|
125
|
+
index * 100 * (COLLAPSED_ASPECT_RATIO / FULL_ASPECT_RATIO) +
|
|
126
|
+
MARGIN +
|
|
127
|
+
index * GAP;
|
|
128
|
+
const xSpring = useSpring(x, { bounce: 0 });
|
|
129
|
+
const xPercentage = useMotionTemplate`-${xSpring}%`;
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
xSpring.set(x);
|
|
133
|
+
}, [x, xSpring]);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Box
|
|
137
|
+
display="flex"
|
|
138
|
+
justifyContent={"center"}
|
|
139
|
+
overflow={"hidden"}
|
|
140
|
+
height="3rem"
|
|
141
|
+
>
|
|
142
|
+
<Box
|
|
143
|
+
as={motion.div}
|
|
144
|
+
style={{
|
|
145
|
+
aspectRatio: FULL_ASPECT_RATIO,
|
|
146
|
+
gap: `${GAP}%`,
|
|
147
|
+
x: xPercentage,
|
|
148
|
+
}}
|
|
149
|
+
display="flex"
|
|
150
|
+
minWidth={"0"}
|
|
151
|
+
>
|
|
152
|
+
{images.map((image, i) => (
|
|
153
|
+
<Box
|
|
154
|
+
as={motion.div}
|
|
155
|
+
onClick={() => setIndex(i)}
|
|
156
|
+
initial={false}
|
|
157
|
+
animate={i === index ? "active" : "inactive"}
|
|
158
|
+
cursor="pointer"
|
|
159
|
+
variants={{
|
|
160
|
+
active: {
|
|
161
|
+
aspectRatio: FULL_ASPECT_RATIO,
|
|
162
|
+
marginLeft: `${MARGIN}%`,
|
|
163
|
+
marginRight: `${MARGIN}%`,
|
|
164
|
+
},
|
|
165
|
+
inactive: {
|
|
166
|
+
aspectRatio: COLLAPSED_ASPECT_RATIO,
|
|
167
|
+
marginLeft: 0,
|
|
168
|
+
marginRight: 0,
|
|
169
|
+
},
|
|
170
|
+
}}
|
|
171
|
+
height="100%"
|
|
172
|
+
flex="none"
|
|
173
|
+
key={image.id}
|
|
174
|
+
>
|
|
175
|
+
<Box
|
|
176
|
+
as="img"
|
|
177
|
+
alt=""
|
|
178
|
+
src={image.url}
|
|
179
|
+
width="100%"
|
|
180
|
+
height="100vh"
|
|
181
|
+
style={{ objectFit: "cover" }}
|
|
182
|
+
/>
|
|
183
|
+
</Box>
|
|
184
|
+
))}
|
|
185
|
+
</Box>
|
|
186
|
+
</Box>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useCallback } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "motion/react"
|
|
5
|
+
import Box, { BoxProps } from "@/design-system/elements/box"
|
|
6
|
+
import { useReducedMotion } from "@/design-system/hooks/useReducedMotion"
|
|
7
|
+
|
|
8
|
+
// Default Apple Intelligence colors
|
|
9
|
+
const DEFAULT_COLORS = [
|
|
10
|
+
"#3B82F6", // Blue
|
|
11
|
+
"#A855F7", // Purple
|
|
12
|
+
"#7A84FF", // Red
|
|
13
|
+
"#35B3DF", // Cyan
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export type AppleGlowProps = Omit<BoxProps, "children"> & {
|
|
17
|
+
colors?: string[]
|
|
18
|
+
borderRadius?: string | number
|
|
19
|
+
intensity?: "sm" | "md" | "lg" | "xl"
|
|
20
|
+
preview?: boolean // Controls visibility
|
|
21
|
+
blurAmount?: number // Custom blur amount in pixels
|
|
22
|
+
backgroundColor?: string // Background color for the mask
|
|
23
|
+
rotationSpeed?: number // ms per tick (default 50)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Blur intensity mapping
|
|
27
|
+
const BLUR_INTENSITY = {
|
|
28
|
+
sm: 8,
|
|
29
|
+
md: 16,
|
|
30
|
+
lg: 24,
|
|
31
|
+
xl: 32,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Generate linear gradient — unrolled for common color counts to avoid .join()
|
|
35
|
+
function createLinearGradient(colors: string[], angle: number): string {
|
|
36
|
+
switch (colors.length) {
|
|
37
|
+
case 2:
|
|
38
|
+
return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]})`
|
|
39
|
+
case 3:
|
|
40
|
+
return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]}, ${colors[2]})`
|
|
41
|
+
case 4:
|
|
42
|
+
return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`
|
|
43
|
+
default:
|
|
44
|
+
return `linear-gradient(${angle}deg, ${colors.join(", ")})`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function AppleGlow({
|
|
49
|
+
colors = DEFAULT_COLORS,
|
|
50
|
+
borderRadius,
|
|
51
|
+
intensity = "xl",
|
|
52
|
+
preview = false,
|
|
53
|
+
blurAmount,
|
|
54
|
+
backgroundColor = "base",
|
|
55
|
+
rotationSpeed = 50,
|
|
56
|
+
...boxProps
|
|
57
|
+
}: AppleGlowProps) {
|
|
58
|
+
const prefersReducedMotion = useReducedMotion()
|
|
59
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
60
|
+
const angleRef = useRef(234.576)
|
|
61
|
+
const rafRef = useRef<number | null>(null)
|
|
62
|
+
const lastTimeRef = useRef(0)
|
|
63
|
+
const blur = blurAmount ?? BLUR_INTENSITY[intensity]
|
|
64
|
+
|
|
65
|
+
const applyFrame = useCallback(() => {
|
|
66
|
+
if (containerRef.current) {
|
|
67
|
+
containerRef.current.style.background = createLinearGradient(
|
|
68
|
+
colors,
|
|
69
|
+
angleRef.current,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}, [colors])
|
|
73
|
+
|
|
74
|
+
// Rotate gradient angle over time (rAF-driven, frame-synced)
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!preview || prefersReducedMotion) {
|
|
77
|
+
// Show a static gradient when reduced motion is preferred
|
|
78
|
+
if (preview) applyFrame()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tick = (now: number) => {
|
|
83
|
+
const delta = lastTimeRef.current ? now - lastTimeRef.current : 0
|
|
84
|
+
lastTimeRef.current = now
|
|
85
|
+
|
|
86
|
+
if (delta > 0 && rotationSpeed > 0) {
|
|
87
|
+
angleRef.current =
|
|
88
|
+
(angleRef.current + delta / rotationSpeed) % 360
|
|
89
|
+
}
|
|
90
|
+
applyFrame()
|
|
91
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lastTimeRef.current = 0
|
|
95
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
if (rafRef.current != null) {
|
|
99
|
+
cancelAnimationFrame(rafRef.current)
|
|
100
|
+
rafRef.current = null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, [preview, rotationSpeed, applyFrame, prefersReducedMotion])
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<AnimatePresence>
|
|
107
|
+
{preview && (
|
|
108
|
+
<Box
|
|
109
|
+
as={motion.div}
|
|
110
|
+
ref={containerRef}
|
|
111
|
+
position="absolute"
|
|
112
|
+
top={0}
|
|
113
|
+
left={0}
|
|
114
|
+
right={0}
|
|
115
|
+
bottom={0}
|
|
116
|
+
initial={{ opacity: 0 }}
|
|
117
|
+
animate={{ opacity: 1 }}
|
|
118
|
+
exit={{ opacity: 0 }}
|
|
119
|
+
transition={{ duration: 0.3 }}
|
|
120
|
+
style={{
|
|
121
|
+
pointerEvents: "none",
|
|
122
|
+
borderRadius,
|
|
123
|
+
willChange: "background",
|
|
124
|
+
}}
|
|
125
|
+
{...boxProps}
|
|
126
|
+
>
|
|
127
|
+
{/* Blurred inset mask - the blur on the edges reveals the gradient */}
|
|
128
|
+
<Box
|
|
129
|
+
position="absolute"
|
|
130
|
+
top="2px"
|
|
131
|
+
left="2px"
|
|
132
|
+
right="2px"
|
|
133
|
+
bottom="2px"
|
|
134
|
+
bg={backgroundColor}
|
|
135
|
+
style={{
|
|
136
|
+
filter: `blur(${blur}px)`,
|
|
137
|
+
borderRadius: borderRadius ? `inherit` : undefined,
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
</Box>
|
|
141
|
+
)}
|
|
142
|
+
</AnimatePresence>
|
|
143
|
+
)
|
|
144
|
+
}
|