@studiocubics/components 0.0.1
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/CHANGELOG.md +11 -0
- package/README.md +71 -0
- package/eslint.config.js +21 -0
- package/package.json +66 -0
- package/rollup.config.js +34 -0
- package/src/Cards/Card/Card.module.css +27 -0
- package/src/Cards/Card/Card.tsx +105 -0
- package/src/Cards/CollectionItemCard/CollectionItemCard.module.css +84 -0
- package/src/Cards/CollectionItemCard/CollectionItemCard.tsx +170 -0
- package/src/Cards/CollectionItemCard/CollectionItemCardActions.tsx +85 -0
- package/src/Cards/CollectionItemCard/_index.ts +2 -0
- package/src/Cards/GlassCard/GlassCard.module.css +71 -0
- package/src/Cards/GlassCard/GlassCard.tsx +80 -0
- package/src/Cards/_index.ts +3 -0
- package/src/Display/Accordion/Accordion.module.css +69 -0
- package/src/Display/Accordion/Accordion.tsx +61 -0
- package/src/Display/Accordion/AccordionItem.tsx +135 -0
- package/src/Display/Accordion/_index.ts +2 -0
- package/src/Display/Chip/Chip.module.css +64 -0
- package/src/Display/Chip/Chip.tsx +105 -0
- package/src/Display/IdentityDisplay/IdentityDisplay.module.css +95 -0
- package/src/Display/IdentityDisplay/IdentityDisplay.tsx +119 -0
- package/src/Display/InputErrors/InputErrors.module.css +6 -0
- package/src/Display/InputErrors/InputErrors.tsx +52 -0
- package/src/Display/Kbd/Kbd.module.css +29 -0
- package/src/Display/Kbd/Kbd.tsx +39 -0
- package/src/Display/Kbd/_index.ts +2 -0
- package/src/Display/Kbd/buttonList.tsx +246 -0
- package/src/Display/LabeledValue/LabeledValue.module.css +32 -0
- package/src/Display/LabeledValue/LabeledValue.tsx +20 -0
- package/src/Display/List/List.module.css +143 -0
- package/src/Display/List/List.tsx +298 -0
- package/src/Display/PasswordStrength/PasswordStrength.module.css +45 -0
- package/src/Display/PasswordStrength/PasswordStrength.tsx +41 -0
- package/src/Display/PasswordStrength/usePasswordStrength.tsx +77 -0
- package/src/Display/Skeleton/Skeleton.module.css +54 -0
- package/src/Display/Skeleton/Skeleton.tsx +28 -0
- package/src/Display/Toast/Toaster.tsx +58 -0
- package/src/Display/Toast/_index.ts +2 -0
- package/src/Display/Toast/toast.ts +44 -0
- package/src/Display/Tooltip/Tooltip.module.css +128 -0
- package/src/Display/Tooltip/Tooltip.tsx +93 -0
- package/src/Display/Tooltip/getArrowDirection.ts +55 -0
- package/src/Display/Tooltip/useTooltip.tsx +63 -0
- package/src/Display/_index.ts +12 -0
- package/src/Forms/ConfirmationForm/ConfirmationForm.module.css +23 -0
- package/src/Forms/ConfirmationForm/ConfirmationForm.tsx +60 -0
- package/src/Forms/_index.ts +1 -0
- package/src/Inputs/Button/Button.module.css +131 -0
- package/src/Inputs/Button/Button.tsx +178 -0
- package/src/Inputs/Checkbox/Checkbox.module.css +77 -0
- package/src/Inputs/Checkbox/Checkbox.tsx +191 -0
- package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.module.css +10 -0
- package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.tsx +83 -0
- package/src/Inputs/Checkbox/CheckboxSelectAll.tsx +34 -0
- package/src/Inputs/Checkbox/_index.ts +3 -0
- package/src/Inputs/PasswordInput/PasswordInput.module.css +111 -0
- package/src/Inputs/PasswordInput/PasswordInput.tsx +229 -0
- package/src/Inputs/Select/Select.module.css +138 -0
- package/src/Inputs/Select/Select.tsx +136 -0
- package/src/Inputs/Switch/Switch.module.css +119 -0
- package/src/Inputs/Switch/Switch.tsx +195 -0
- package/src/Inputs/TextAreaInput/TextAreaInput.module.css +65 -0
- package/src/Inputs/TextAreaInput/TextAreaInput.tsx +97 -0
- package/src/Inputs/TextInput/TextInput.module.css +112 -0
- package/src/Inputs/TextInput/TextInput.tsx +142 -0
- package/src/Inputs/ThemeToggle/ThemeToggleListItem.tsx +80 -0
- package/src/Inputs/ThemeToggle/_index.ts +1 -0
- package/src/Inputs/_index.ts +8 -0
- package/src/Layout/Dialog/Dialog.module.css +15 -0
- package/src/Layout/Dialog/Dialog.tsx +115 -0
- package/src/Layout/PageLayout/PageLayout.module.css +20 -0
- package/src/Layout/PageLayout/PageLayout.tsx +79 -0
- package/src/Layout/PageLayoutPagination/PageLayoutPagination.module.css +5 -0
- package/src/Layout/PageLayoutPagination/PageLayoutPagination.tsx +40 -0
- package/src/Layout/PageLayoutTabs/PageLayoutTabs.module.css +3 -0
- package/src/Layout/PageLayoutTabs/PageLayoutTabs.tsx +62 -0
- package/src/Layout/Popover/Popover.module.css +9 -0
- package/src/Layout/Popover/Popover.tsx +145 -0
- package/src/Layout/SectionWrapper/SectionWrapper.module.css +31 -0
- package/src/Layout/SectionWrapper/SectionWrapper.tsx +62 -0
- package/src/Layout/Sidebar/Sidebar.module.css +17 -0
- package/src/Layout/Sidebar/Sidebar.tsx +39 -0
- package/src/Layout/Sidebar/SidebarBody/SidebarBody.module.css +31 -0
- package/src/Layout/Sidebar/SidebarBody/SidebarBody.tsx +18 -0
- package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.module.css +20 -0
- package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.tsx +19 -0
- package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.module.css +35 -0
- package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.tsx +19 -0
- package/src/Layout/Sidebar/SidebarHeader/SidebarHeader.tsx +14 -0
- package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.module.css +12 -0
- package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.tsx +11 -0
- package/src/Layout/Sidebar/_index.ts +6 -0
- package/src/Layout/Table/Table.module.css +46 -0
- package/src/Layout/Table/Table.tsx +222 -0
- package/src/Layout/Table/TableFooter.tsx +4 -0
- package/src/Layout/Table/TableHeader.tsx +4 -0
- package/src/Layout/Table/_index.ts +5 -0
- package/src/Layout/Table/tableUtils.ts +142 -0
- package/src/Layout/Table/types.ts +48 -0
- package/src/Layout/_index.ts +8 -0
- package/src/Misc/Cursor/Cursor.module.css +31 -0
- package/src/Misc/Cursor/Cursor.tsx +77 -0
- package/src/Misc/Logos.tsx +230 -0
- package/src/Misc/PoweredByBanner/PoweredByBanner.module.css +20 -0
- package/src/Misc/PoweredByBanner/PoweredByBanner.tsx +17 -0
- package/src/Misc/Ripple/Ripple.module.css +25 -0
- package/src/Misc/Ripple/Ripple.tsx +126 -0
- package/src/Misc/Spinner/Spinner.module.css +38 -0
- package/src/Misc/Spinner/Spinner.tsx +36 -0
- package/src/Misc/TransitionAnimation/TransitionAnimation.module.css +131 -0
- package/src/Misc/TransitionAnimation/TransitionAnimation.tsx +166 -0
- package/src/Misc/_index.ts +6 -0
- package/src/Navigation/Breadcrumbs/Breadcrumbs.module.css +22 -0
- package/src/Navigation/Breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/Navigation/Breadcrumbs/BreadcrumbsItem.tsx +31 -0
- package/src/Navigation/Breadcrumbs/_index.ts +3 -0
- package/src/Navigation/Breadcrumbs/useBreadcrumbs.tsx +74 -0
- package/src/Navigation/Pagination/Pagination.module.css +41 -0
- package/src/Navigation/Pagination/Pagination.tsx +187 -0
- package/src/Navigation/Pagination/PaginationItem.tsx +28 -0
- package/src/Navigation/Pagination/_index.ts +3 -0
- package/src/Navigation/Pagination/usePagination.tsx +65 -0
- package/src/Navigation/Tabs/Tab/Tab.module.css +43 -0
- package/src/Navigation/Tabs/Tab/Tab.tsx +155 -0
- package/src/Navigation/Tabs/Tabs.tsx +37 -0
- package/src/Navigation/Tabs/TabsBar/TabsBar.module.css +47 -0
- package/src/Navigation/Tabs/TabsBar/TabsBar.tsx +92 -0
- package/src/Navigation/Tabs/_index.ts +3 -0
- package/src/Navigation/_index.ts +3 -0
- package/src/Typography/ClampedText/ClampedText.module.css +5 -0
- package/src/Typography/ClampedText/ClampedText.tsx +77 -0
- package/src/Typography/CopyableText/CopyableText.module.css +21 -0
- package/src/Typography/CopyableText/CopyableText.tsx +120 -0
- package/src/Typography/PageTitle/PageTitle.module.css +47 -0
- package/src/Typography/PageTitle/PageTitle.tsx +35 -0
- package/src/Typography/_index.ts +3 -0
- package/src/declaration.d.ts +4 -0
- package/src/index.ts +8 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* Grow Animation - Max-Width/Max-Height based */
|
|
2
|
+
.growEnter {
|
|
3
|
+
transform: scale(0);
|
|
4
|
+
max-width: 0;
|
|
5
|
+
max-height: 0;
|
|
6
|
+
opacity: 0;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.growEnterActive {
|
|
11
|
+
transform: scale(1);
|
|
12
|
+
max-width: 100vw;
|
|
13
|
+
max-height: 100vh;
|
|
14
|
+
opacity: 1;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
transition:
|
|
17
|
+
transform var(--transition-tf),
|
|
18
|
+
max-width var(--transition-tf),
|
|
19
|
+
max-height var(--transition-tf),
|
|
20
|
+
opacity var(--transition-tf);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.growExit {
|
|
24
|
+
transform: scale(1);
|
|
25
|
+
max-width: 100vw;
|
|
26
|
+
max-height: 100vh;
|
|
27
|
+
opacity: 1;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.growExitActive {
|
|
32
|
+
transform: scale(0);
|
|
33
|
+
max-width: 0;
|
|
34
|
+
max-height: 0;
|
|
35
|
+
opacity: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
transition:
|
|
38
|
+
transform var(--transition-tf),
|
|
39
|
+
max-width var(--transition-tf),
|
|
40
|
+
max-height var(--transition-tf),
|
|
41
|
+
opacity var(--transition-tf);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Fade Animation */
|
|
45
|
+
.fadeEnter {
|
|
46
|
+
opacity: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.fadeEnterActive {
|
|
50
|
+
opacity: 1;
|
|
51
|
+
transition: opacity var(--transition-tf);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.fadeExit {
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.fadeExitActive {
|
|
59
|
+
opacity: 0;
|
|
60
|
+
transition: opacity var(--transition-tf);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Slide Animation */
|
|
64
|
+
.slideEnter {
|
|
65
|
+
transform: translateY(-20px);
|
|
66
|
+
opacity: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.slideEnterActive {
|
|
70
|
+
transform: translateY(0);
|
|
71
|
+
opacity: 1;
|
|
72
|
+
transition:
|
|
73
|
+
transform var(--transition-tf),
|
|
74
|
+
opacity var(--transition-tf);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.slideExit {
|
|
78
|
+
transform: translateY(0);
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.slideExitActive {
|
|
83
|
+
transform: translateY(-20px);
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transition:
|
|
86
|
+
transform var(--transition-tf),
|
|
87
|
+
opacity var(--transition-tf);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Zoom Animation - Max-Width/Max-Height based */
|
|
91
|
+
.zoomEnter {
|
|
92
|
+
transform: scale(0.5);
|
|
93
|
+
max-width: 0;
|
|
94
|
+
max-height: 0;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.zoomEnterActive {
|
|
100
|
+
transform: scale(1);
|
|
101
|
+
max-width: 100vw;
|
|
102
|
+
max-height: 100vh;
|
|
103
|
+
opacity: 1;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
transition:
|
|
106
|
+
transform var(--transition-tf),
|
|
107
|
+
max-width var(--transition-tf),
|
|
108
|
+
max-height var(--transition-tf),
|
|
109
|
+
opacity var(--transition-tf);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.zoomExit {
|
|
113
|
+
transform: scale(1);
|
|
114
|
+
max-width: 100vw;
|
|
115
|
+
max-height: 100vh;
|
|
116
|
+
opacity: 1;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.zoomExitActive {
|
|
121
|
+
transform: scale(1.5);
|
|
122
|
+
max-width: 0;
|
|
123
|
+
max-height: 0;
|
|
124
|
+
opacity: 0;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
transition:
|
|
127
|
+
transform var(--transition-tf),
|
|
128
|
+
max-width var(--transition-tf),
|
|
129
|
+
max-height var(--transition-tf),
|
|
130
|
+
opacity var(--transition-tf);
|
|
131
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
cloneElement,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
} from "react";
|
|
11
|
+
import styles from "./TransitionAnimation.module.css";
|
|
12
|
+
import { cn } from "@studiocubics/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Animation types available for the TransitionAnimation component
|
|
16
|
+
*/
|
|
17
|
+
export type AnimationType = "grow" | "fade" | "slide" | "zoom";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the TransitionAnimation component
|
|
21
|
+
*/
|
|
22
|
+
export interface TransitionAnimationProps {
|
|
23
|
+
/**
|
|
24
|
+
* The type of animation to apply on mount/unmount
|
|
25
|
+
* - 'grow': Scales from 0 to 1 (mount) and 1 to 0 (unmount)
|
|
26
|
+
* - 'fade': Fades opacity from 0 to 1 (mount) and 1 to 0 (unmount)
|
|
27
|
+
* - 'slide': Slides down with fade (mount) and slides up with fade (unmount)
|
|
28
|
+
* - 'zoom': Scales from 0.5 to 1 (mount) and 1 to 1.5 (unmount)
|
|
29
|
+
* @default "grow"
|
|
30
|
+
*/
|
|
31
|
+
animation?: AnimationType;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Controls whether the child component should be mounted and animated
|
|
35
|
+
* - When changed to true: triggers entry animation
|
|
36
|
+
* - When changed to false: triggers exit animation then unmounts
|
|
37
|
+
*/
|
|
38
|
+
in: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A single React element to be animated
|
|
42
|
+
* The component will clone this element and apply animation classes to it
|
|
43
|
+
*/
|
|
44
|
+
children: ReactElement<{ className?: string; style?: React.CSSProperties }>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Duration of the animation in milliseconds
|
|
48
|
+
* @default 300
|
|
49
|
+
*/
|
|
50
|
+
duration?: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* If true, the child component will be unmounted after the exit animation completes
|
|
54
|
+
* If false, the child will remain mounted but hidden (opacity 0, scale 0, etc.)
|
|
55
|
+
* @default true
|
|
56
|
+
*/
|
|
57
|
+
unmountOnExit?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* If true, the child component will be unmounted immediately without playing the exit animation
|
|
61
|
+
* This takes precedence over unmountOnExit
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
mountOnly?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The origin of the transform duh
|
|
68
|
+
*/
|
|
69
|
+
transformOrigin?: CSSProperties["transformOrigin"];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* TransitionAnimation Component
|
|
74
|
+
*
|
|
75
|
+
* A component that applies smooth entry and exit animations to its child element.
|
|
76
|
+
* The child is mounted when `in` becomes true with an entry animation,
|
|
77
|
+
* and unmounted when `in` becomes false after playing an exit animation.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* const [open, setOpen] = useState(false);
|
|
82
|
+
*
|
|
83
|
+
* <TransitionAnimation animation="grow" in={open}>
|
|
84
|
+
* <div className="my-content">Hello World</div>
|
|
85
|
+
* </TransitionAnimation>
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const TransitionAnimation: React.FC<TransitionAnimationProps> = ({
|
|
89
|
+
animation = "grow",
|
|
90
|
+
in: inProp,
|
|
91
|
+
children,
|
|
92
|
+
duration = 500,
|
|
93
|
+
unmountOnExit = true,
|
|
94
|
+
mountOnly = false,
|
|
95
|
+
transformOrigin,
|
|
96
|
+
}) => {
|
|
97
|
+
const [shouldRender, setShouldRender] = useState(inProp);
|
|
98
|
+
const [animationState, setAnimationState] = useState<
|
|
99
|
+
"enter" | "entering" | "exit" | "exiting"
|
|
100
|
+
>("enter");
|
|
101
|
+
const timeoutRef = useRef<number>(null);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (inProp) {
|
|
105
|
+
setShouldRender(true);
|
|
106
|
+
setAnimationState("enter");
|
|
107
|
+
|
|
108
|
+
// Trigger enter animation on next frame
|
|
109
|
+
requestAnimationFrame(() => {
|
|
110
|
+
requestAnimationFrame(() => {
|
|
111
|
+
setAnimationState("entering");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
} else if (shouldRender) {
|
|
115
|
+
if (mountOnly) {
|
|
116
|
+
// Immediately unmount without animation
|
|
117
|
+
setShouldRender(false);
|
|
118
|
+
} else {
|
|
119
|
+
setAnimationState("exiting");
|
|
120
|
+
|
|
121
|
+
if (unmountOnExit) {
|
|
122
|
+
// Wait for exit animation to complete before unmounting
|
|
123
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
124
|
+
setShouldRender(false);
|
|
125
|
+
setAnimationState("exit");
|
|
126
|
+
}, duration);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
if (timeoutRef.current) {
|
|
133
|
+
clearTimeout(timeoutRef.current);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [inProp, duration, shouldRender, unmountOnExit, mountOnly]);
|
|
137
|
+
|
|
138
|
+
if (!shouldRender) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const getAnimationClass = () => {
|
|
143
|
+
switch (animationState) {
|
|
144
|
+
case "enter":
|
|
145
|
+
return styles[`${animation}Enter`];
|
|
146
|
+
case "entering":
|
|
147
|
+
return styles[`${animation}EnterActive`];
|
|
148
|
+
case "exiting":
|
|
149
|
+
return styles[`${animation}ExitActive`];
|
|
150
|
+
case "exit":
|
|
151
|
+
return styles[`${animation}Exit`];
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const childClassName = children.props.className || "";
|
|
156
|
+
const childStyle = children.props.style || {};
|
|
157
|
+
|
|
158
|
+
return cloneElement(children, {
|
|
159
|
+
className: cn(childClassName, getAnimationClass()),
|
|
160
|
+
style: {
|
|
161
|
+
...childStyle,
|
|
162
|
+
transitionDuration: `${duration}ms`,
|
|
163
|
+
transformOrigin,
|
|
164
|
+
},
|
|
165
|
+
} as any);
|
|
166
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 1ch;
|
|
5
|
+
& > svg {
|
|
6
|
+
color: var(--color-on-background-faint);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
.item {
|
|
10
|
+
display: content;
|
|
11
|
+
color: var(--color-on-background-faint);
|
|
12
|
+
&:not(.activeItem):hover {
|
|
13
|
+
color: var(--color-primary);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
.activeItem {
|
|
17
|
+
color: var(--color-on-background);
|
|
18
|
+
/* border: 1px solid blue; */
|
|
19
|
+
}
|
|
20
|
+
.ellipses {
|
|
21
|
+
color: var(--color-on-background-faint);
|
|
22
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment, type ReactElement, type ReactNode, useState } from "react";
|
|
4
|
+
import { useBreadcrumbs } from "./useBreadcrumbs";
|
|
5
|
+
import { BreadcrumbsItem, type BreadcrumbsItemProps } from "./BreadcrumbsItem";
|
|
6
|
+
import { cn } from "@studiocubics/utils";
|
|
7
|
+
import styles from "./Breadcrumbs.module.css";
|
|
8
|
+
|
|
9
|
+
export interface BreadCrumbsProps {
|
|
10
|
+
children: ReactElement[];
|
|
11
|
+
/**
|
|
12
|
+
* For controlled Breadcrumbs pass the onChange function
|
|
13
|
+
*/
|
|
14
|
+
onChange?: (pageNumber: number) => void;
|
|
15
|
+
/**
|
|
16
|
+
* How many siblings of the active item should be shown
|
|
17
|
+
* @default 1
|
|
18
|
+
*/
|
|
19
|
+
siblingCount?: number;
|
|
20
|
+
/**
|
|
21
|
+
* How many of the boundary items should be shown
|
|
22
|
+
* @default 0
|
|
23
|
+
*/
|
|
24
|
+
boundaryCount?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Crumb that will be selected by default
|
|
27
|
+
* @default 1
|
|
28
|
+
*/
|
|
29
|
+
defaultActive?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Node to use as separator
|
|
32
|
+
*/
|
|
33
|
+
separator?: ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Node to use as ellipsis
|
|
36
|
+
*/
|
|
37
|
+
ellipsis?: ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Function that can be used to modify the rendered BreadcrumbsItem component
|
|
40
|
+
*/
|
|
41
|
+
renderItem?: (
|
|
42
|
+
props: BreadcrumbsItemProps,
|
|
43
|
+
key?: string | number,
|
|
44
|
+
) => ReactElement;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ChevronRightIcon = (
|
|
48
|
+
<svg
|
|
49
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
+
width="24"
|
|
51
|
+
height="24"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="2"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
className="lucide lucide-chevron-right-icon lucide-chevron-right"
|
|
59
|
+
>
|
|
60
|
+
<path d="m9 18 6-6-6-6" />
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
export const EllipsisIcon = (
|
|
64
|
+
<svg
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
width="20"
|
|
67
|
+
height="20"
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
strokeWidth="2"
|
|
72
|
+
strokeLinecap="round"
|
|
73
|
+
strokeLinejoin="round"
|
|
74
|
+
className="lucide lucide-ellipsis-icon lucide-ellipsis"
|
|
75
|
+
>
|
|
76
|
+
<circle cx="12" cy="12" r="1" />
|
|
77
|
+
<circle cx="19" cy="12" r="1" />
|
|
78
|
+
<circle cx="5" cy="12" r="1" />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
export function Breadcrumbs(props: BreadCrumbsProps) {
|
|
82
|
+
const {
|
|
83
|
+
onChange,
|
|
84
|
+
boundaryCount = 1,
|
|
85
|
+
siblingCount = 0,
|
|
86
|
+
children,
|
|
87
|
+
defaultActive = 0,
|
|
88
|
+
separator = ChevronRightIcon,
|
|
89
|
+
ellipsis = EllipsisIcon,
|
|
90
|
+
renderItem = (props, key?) => <BreadcrumbsItem key={key} {...props} />,
|
|
91
|
+
} = props;
|
|
92
|
+
const [activeCrumb, setActiveCrumb] = useState<number>(defaultActive);
|
|
93
|
+
|
|
94
|
+
const breadcrumbRange = useBreadcrumbs({
|
|
95
|
+
activeCrumb,
|
|
96
|
+
crumbs: children,
|
|
97
|
+
boundaryCount,
|
|
98
|
+
siblingCount,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function handleClick(i: number) {
|
|
102
|
+
setActiveCrumb(i);
|
|
103
|
+
if (onChange) onChange(i);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<span className={styles.root}>
|
|
108
|
+
{breadcrumbRange.map((item, idx) => (
|
|
109
|
+
<Fragment key={idx}>
|
|
110
|
+
{renderItem({
|
|
111
|
+
className: cn(
|
|
112
|
+
styles.item,
|
|
113
|
+
activeCrumb === idx ? styles.activeItem : "",
|
|
114
|
+
item == "ellipsis" ? styles.ellipsis : "",
|
|
115
|
+
),
|
|
116
|
+
onClick: () => handleClick(idx),
|
|
117
|
+
children: item == "ellipsis" ? ellipsis : item,
|
|
118
|
+
})}
|
|
119
|
+
{idx !== breadcrumbRange.length - 1 &&
|
|
120
|
+
item !== "ellipsis" &&
|
|
121
|
+
breadcrumbRange[idx + 1] != "ellipsis" &&
|
|
122
|
+
separator}
|
|
123
|
+
</Fragment>
|
|
124
|
+
))}
|
|
125
|
+
</span>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PolymorphicComponentProps,
|
|
5
|
+
PolymorphicComponentType,
|
|
6
|
+
} from "@studiocubics/types";
|
|
7
|
+
import { type ElementType } from "react";
|
|
8
|
+
|
|
9
|
+
interface BreadcrumbsItemBaseProps {
|
|
10
|
+
href?: "string";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultElement = "span";
|
|
14
|
+
type DefaultElement = typeof defaultElement;
|
|
15
|
+
|
|
16
|
+
export type BreadcrumbsItemProps<C extends ElementType = DefaultElement> =
|
|
17
|
+
PolymorphicComponentProps<C, BreadcrumbsItemBaseProps>;
|
|
18
|
+
|
|
19
|
+
const BreadcrumbsItemBase = <C extends ElementType = DefaultElement>(
|
|
20
|
+
props: BreadcrumbsItemProps<C>
|
|
21
|
+
) => {
|
|
22
|
+
const { as, children, ...rest } = props;
|
|
23
|
+
const Component = (as || defaultElement) as ElementType;
|
|
24
|
+
return <Component {...rest}>{children}</Component>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
BreadcrumbsItemBase.displayName = "BreadcrumbsItem";
|
|
28
|
+
export const BreadcrumbsItem = BreadcrumbsItemBase as PolymorphicComponentType<
|
|
29
|
+
BreadcrumbsItemBaseProps,
|
|
30
|
+
DefaultElement
|
|
31
|
+
>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export function useBreadcrumbs({
|
|
4
|
+
activeCrumb,
|
|
5
|
+
crumbs,
|
|
6
|
+
siblingCount = 0,
|
|
7
|
+
boundaryCount = 1,
|
|
8
|
+
}: {
|
|
9
|
+
activeCrumb: number;
|
|
10
|
+
crumbs: ReactElement[];
|
|
11
|
+
siblingCount?: number;
|
|
12
|
+
boundaryCount?: number;
|
|
13
|
+
}) {
|
|
14
|
+
const crumbsClone = Array.from(crumbs);
|
|
15
|
+
const count = crumbs.length;
|
|
16
|
+
if (count <= boundaryCount * 2 + siblingCount * 2 + 2) {
|
|
17
|
+
// If total items fit without ellipsis, just return all crumbs
|
|
18
|
+
return crumbsClone;
|
|
19
|
+
}
|
|
20
|
+
const resolvedBoundaryCount = Math.min(boundaryCount, count);
|
|
21
|
+
|
|
22
|
+
console.log("crumbs", crumbsClone);
|
|
23
|
+
|
|
24
|
+
const startPages = crumbsClone.slice(0, resolvedBoundaryCount + 1);
|
|
25
|
+
const endPages = crumbsClone.slice(
|
|
26
|
+
count - 1 - resolvedBoundaryCount,
|
|
27
|
+
crumbsClone.length,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const siblingsStart = Math.max(
|
|
31
|
+
Math.min(
|
|
32
|
+
activeCrumb - siblingCount,
|
|
33
|
+
count - boundaryCount - siblingCount * 2 - 1,
|
|
34
|
+
),
|
|
35
|
+
boundaryCount + 2,
|
|
36
|
+
);
|
|
37
|
+
const siblingsEnd = Math.min(
|
|
38
|
+
Math.max(activeCrumb + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
39
|
+
endPages.length > 0 ? count - 1 - endPages.length : count - 1,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
console.log("siblingsStart", siblingsStart);
|
|
43
|
+
console.log("siblingsEnd", siblingsEnd);
|
|
44
|
+
|
|
45
|
+
const itemList: (ReactElement | "ellipsis")[] = [];
|
|
46
|
+
|
|
47
|
+
// Start pages
|
|
48
|
+
itemList.push(...startPages);
|
|
49
|
+
|
|
50
|
+
// Ellipsis after start pages
|
|
51
|
+
if (siblingsStart > boundaryCount + 2) {
|
|
52
|
+
itemList.push("ellipsis");
|
|
53
|
+
} else if (boundaryCount + 1 < count - boundaryCount) {
|
|
54
|
+
itemList.push(crumbs[boundaryCount + 1]);
|
|
55
|
+
}
|
|
56
|
+
// Middle pages
|
|
57
|
+
for (let i = siblingsStart; i <= siblingsEnd; i++) {
|
|
58
|
+
itemList.push(crumbs[i]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ellipsis before end pages
|
|
62
|
+
if (siblingsEnd < count - 1 - boundaryCount) {
|
|
63
|
+
itemList.push("ellipsis");
|
|
64
|
+
} else if (count - boundaryCount > boundaryCount) {
|
|
65
|
+
itemList.push(crumbs[count - boundaryCount]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const endPagesFiltered = endPages.filter((p) => !itemList.includes(p));
|
|
69
|
+
console.log("endPagesFiltered", endPagesFiltered);
|
|
70
|
+
|
|
71
|
+
itemList.push(...endPagesFiltered);
|
|
72
|
+
|
|
73
|
+
return itemList;
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
gap: var(--spacing-gap-2);
|
|
5
|
+
list-style-type: none;
|
|
6
|
+
}
|
|
7
|
+
.item {
|
|
8
|
+
--pagination-item-fs: var(--fs-body2);
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
color: var(--color-on-background);
|
|
11
|
+
border-radius: var(--shape-br-sm);
|
|
12
|
+
font-size: var(--pagination-item-fs);
|
|
13
|
+
transition: all var(--transition-time) var(--transition-tf);
|
|
14
|
+
width: 32px;
|
|
15
|
+
height: 32px;
|
|
16
|
+
min-width: fit-content;
|
|
17
|
+
padding-inline: var(--spacing-gap);
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.disabled {
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
opacity: 0.5;
|
|
26
|
+
}
|
|
27
|
+
.item:not(.disabled):hover {
|
|
28
|
+
background: var(--color-surface);
|
|
29
|
+
}
|
|
30
|
+
.activeItem {
|
|
31
|
+
outline: 1px solid var(--color-primary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.icon > svg {
|
|
35
|
+
--pagination-item-icon-size: calc(1.25 * var(--pagination-item-fs));
|
|
36
|
+
width: var(--pagination-item-icon-size);
|
|
37
|
+
height: var(--pagination-item-icon-size);
|
|
38
|
+
}
|
|
39
|
+
.icon:nth-child(2) ~ .icon {
|
|
40
|
+
transform: rotate(180deg);
|
|
41
|
+
}
|