@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.
Files changed (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +71 -0
  3. package/eslint.config.js +21 -0
  4. package/package.json +66 -0
  5. package/rollup.config.js +34 -0
  6. package/src/Cards/Card/Card.module.css +27 -0
  7. package/src/Cards/Card/Card.tsx +105 -0
  8. package/src/Cards/CollectionItemCard/CollectionItemCard.module.css +84 -0
  9. package/src/Cards/CollectionItemCard/CollectionItemCard.tsx +170 -0
  10. package/src/Cards/CollectionItemCard/CollectionItemCardActions.tsx +85 -0
  11. package/src/Cards/CollectionItemCard/_index.ts +2 -0
  12. package/src/Cards/GlassCard/GlassCard.module.css +71 -0
  13. package/src/Cards/GlassCard/GlassCard.tsx +80 -0
  14. package/src/Cards/_index.ts +3 -0
  15. package/src/Display/Accordion/Accordion.module.css +69 -0
  16. package/src/Display/Accordion/Accordion.tsx +61 -0
  17. package/src/Display/Accordion/AccordionItem.tsx +135 -0
  18. package/src/Display/Accordion/_index.ts +2 -0
  19. package/src/Display/Chip/Chip.module.css +64 -0
  20. package/src/Display/Chip/Chip.tsx +105 -0
  21. package/src/Display/IdentityDisplay/IdentityDisplay.module.css +95 -0
  22. package/src/Display/IdentityDisplay/IdentityDisplay.tsx +119 -0
  23. package/src/Display/InputErrors/InputErrors.module.css +6 -0
  24. package/src/Display/InputErrors/InputErrors.tsx +52 -0
  25. package/src/Display/Kbd/Kbd.module.css +29 -0
  26. package/src/Display/Kbd/Kbd.tsx +39 -0
  27. package/src/Display/Kbd/_index.ts +2 -0
  28. package/src/Display/Kbd/buttonList.tsx +246 -0
  29. package/src/Display/LabeledValue/LabeledValue.module.css +32 -0
  30. package/src/Display/LabeledValue/LabeledValue.tsx +20 -0
  31. package/src/Display/List/List.module.css +143 -0
  32. package/src/Display/List/List.tsx +298 -0
  33. package/src/Display/PasswordStrength/PasswordStrength.module.css +45 -0
  34. package/src/Display/PasswordStrength/PasswordStrength.tsx +41 -0
  35. package/src/Display/PasswordStrength/usePasswordStrength.tsx +77 -0
  36. package/src/Display/Skeleton/Skeleton.module.css +54 -0
  37. package/src/Display/Skeleton/Skeleton.tsx +28 -0
  38. package/src/Display/Toast/Toaster.tsx +58 -0
  39. package/src/Display/Toast/_index.ts +2 -0
  40. package/src/Display/Toast/toast.ts +44 -0
  41. package/src/Display/Tooltip/Tooltip.module.css +128 -0
  42. package/src/Display/Tooltip/Tooltip.tsx +93 -0
  43. package/src/Display/Tooltip/getArrowDirection.ts +55 -0
  44. package/src/Display/Tooltip/useTooltip.tsx +63 -0
  45. package/src/Display/_index.ts +12 -0
  46. package/src/Forms/ConfirmationForm/ConfirmationForm.module.css +23 -0
  47. package/src/Forms/ConfirmationForm/ConfirmationForm.tsx +60 -0
  48. package/src/Forms/_index.ts +1 -0
  49. package/src/Inputs/Button/Button.module.css +131 -0
  50. package/src/Inputs/Button/Button.tsx +178 -0
  51. package/src/Inputs/Checkbox/Checkbox.module.css +77 -0
  52. package/src/Inputs/Checkbox/Checkbox.tsx +191 -0
  53. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.module.css +10 -0
  54. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.tsx +83 -0
  55. package/src/Inputs/Checkbox/CheckboxSelectAll.tsx +34 -0
  56. package/src/Inputs/Checkbox/_index.ts +3 -0
  57. package/src/Inputs/PasswordInput/PasswordInput.module.css +111 -0
  58. package/src/Inputs/PasswordInput/PasswordInput.tsx +229 -0
  59. package/src/Inputs/Select/Select.module.css +138 -0
  60. package/src/Inputs/Select/Select.tsx +136 -0
  61. package/src/Inputs/Switch/Switch.module.css +119 -0
  62. package/src/Inputs/Switch/Switch.tsx +195 -0
  63. package/src/Inputs/TextAreaInput/TextAreaInput.module.css +65 -0
  64. package/src/Inputs/TextAreaInput/TextAreaInput.tsx +97 -0
  65. package/src/Inputs/TextInput/TextInput.module.css +112 -0
  66. package/src/Inputs/TextInput/TextInput.tsx +142 -0
  67. package/src/Inputs/ThemeToggle/ThemeToggleListItem.tsx +80 -0
  68. package/src/Inputs/ThemeToggle/_index.ts +1 -0
  69. package/src/Inputs/_index.ts +8 -0
  70. package/src/Layout/Dialog/Dialog.module.css +15 -0
  71. package/src/Layout/Dialog/Dialog.tsx +115 -0
  72. package/src/Layout/PageLayout/PageLayout.module.css +20 -0
  73. package/src/Layout/PageLayout/PageLayout.tsx +79 -0
  74. package/src/Layout/PageLayoutPagination/PageLayoutPagination.module.css +5 -0
  75. package/src/Layout/PageLayoutPagination/PageLayoutPagination.tsx +40 -0
  76. package/src/Layout/PageLayoutTabs/PageLayoutTabs.module.css +3 -0
  77. package/src/Layout/PageLayoutTabs/PageLayoutTabs.tsx +62 -0
  78. package/src/Layout/Popover/Popover.module.css +9 -0
  79. package/src/Layout/Popover/Popover.tsx +145 -0
  80. package/src/Layout/SectionWrapper/SectionWrapper.module.css +31 -0
  81. package/src/Layout/SectionWrapper/SectionWrapper.tsx +62 -0
  82. package/src/Layout/Sidebar/Sidebar.module.css +17 -0
  83. package/src/Layout/Sidebar/Sidebar.tsx +39 -0
  84. package/src/Layout/Sidebar/SidebarBody/SidebarBody.module.css +31 -0
  85. package/src/Layout/Sidebar/SidebarBody/SidebarBody.tsx +18 -0
  86. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.module.css +20 -0
  87. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.tsx +19 -0
  88. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.module.css +35 -0
  89. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.tsx +19 -0
  90. package/src/Layout/Sidebar/SidebarHeader/SidebarHeader.tsx +14 -0
  91. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.module.css +12 -0
  92. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.tsx +11 -0
  93. package/src/Layout/Sidebar/_index.ts +6 -0
  94. package/src/Layout/Table/Table.module.css +46 -0
  95. package/src/Layout/Table/Table.tsx +222 -0
  96. package/src/Layout/Table/TableFooter.tsx +4 -0
  97. package/src/Layout/Table/TableHeader.tsx +4 -0
  98. package/src/Layout/Table/_index.ts +5 -0
  99. package/src/Layout/Table/tableUtils.ts +142 -0
  100. package/src/Layout/Table/types.ts +48 -0
  101. package/src/Layout/_index.ts +8 -0
  102. package/src/Misc/Cursor/Cursor.module.css +31 -0
  103. package/src/Misc/Cursor/Cursor.tsx +77 -0
  104. package/src/Misc/Logos.tsx +230 -0
  105. package/src/Misc/PoweredByBanner/PoweredByBanner.module.css +20 -0
  106. package/src/Misc/PoweredByBanner/PoweredByBanner.tsx +17 -0
  107. package/src/Misc/Ripple/Ripple.module.css +25 -0
  108. package/src/Misc/Ripple/Ripple.tsx +126 -0
  109. package/src/Misc/Spinner/Spinner.module.css +38 -0
  110. package/src/Misc/Spinner/Spinner.tsx +36 -0
  111. package/src/Misc/TransitionAnimation/TransitionAnimation.module.css +131 -0
  112. package/src/Misc/TransitionAnimation/TransitionAnimation.tsx +166 -0
  113. package/src/Misc/_index.ts +6 -0
  114. package/src/Navigation/Breadcrumbs/Breadcrumbs.module.css +22 -0
  115. package/src/Navigation/Breadcrumbs/Breadcrumbs.tsx +127 -0
  116. package/src/Navigation/Breadcrumbs/BreadcrumbsItem.tsx +31 -0
  117. package/src/Navigation/Breadcrumbs/_index.ts +3 -0
  118. package/src/Navigation/Breadcrumbs/useBreadcrumbs.tsx +74 -0
  119. package/src/Navigation/Pagination/Pagination.module.css +41 -0
  120. package/src/Navigation/Pagination/Pagination.tsx +187 -0
  121. package/src/Navigation/Pagination/PaginationItem.tsx +28 -0
  122. package/src/Navigation/Pagination/_index.ts +3 -0
  123. package/src/Navigation/Pagination/usePagination.tsx +65 -0
  124. package/src/Navigation/Tabs/Tab/Tab.module.css +43 -0
  125. package/src/Navigation/Tabs/Tab/Tab.tsx +155 -0
  126. package/src/Navigation/Tabs/Tabs.tsx +37 -0
  127. package/src/Navigation/Tabs/TabsBar/TabsBar.module.css +47 -0
  128. package/src/Navigation/Tabs/TabsBar/TabsBar.tsx +92 -0
  129. package/src/Navigation/Tabs/_index.ts +3 -0
  130. package/src/Navigation/_index.ts +3 -0
  131. package/src/Typography/ClampedText/ClampedText.module.css +5 -0
  132. package/src/Typography/ClampedText/ClampedText.tsx +77 -0
  133. package/src/Typography/CopyableText/CopyableText.module.css +21 -0
  134. package/src/Typography/CopyableText/CopyableText.tsx +120 -0
  135. package/src/Typography/PageTitle/PageTitle.module.css +47 -0
  136. package/src/Typography/PageTitle/PageTitle.tsx +35 -0
  137. package/src/Typography/_index.ts +3 -0
  138. package/src/declaration.d.ts +4 -0
  139. package/src/index.ts +8 -0
  140. 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,6 @@
1
+ export * from "./Ripple/Ripple";
2
+ export * from "./PoweredByBanner/PoweredByBanner";
3
+ export * from "./Cursor/Cursor";
4
+ export * from "./Logos";
5
+ export * from "./Spinner/Spinner";
6
+ export * from "./TransitionAnimation/TransitionAnimation";
@@ -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,3 @@
1
+ export * from "./Breadcrumbs";
2
+ export * from "./BreadcrumbsItem";
3
+ export * from "./useBreadcrumbs";
@@ -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
+ }