@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,187 @@
1
+ "use client";
2
+ import { useState, type ReactElement, type ReactNode } from "react";
3
+ import styles from "./Pagination.module.css";
4
+ import { usePagination } from "./usePagination";
5
+ import { type PaginationItemProps, PaginationItem } from "./PaginationItem";
6
+ import { cn } from "@studiocubics/utils";
7
+
8
+ export interface PaginationProps {
9
+ /**
10
+ * Total number of pages
11
+ */
12
+ count: number;
13
+ /**
14
+ * For controlled Pagination pass the state
15
+ */
16
+ page?: number;
17
+ /**
18
+ * For controlled Pagination pass the onChange function
19
+ */
20
+ onChange?: (pageNumber: number) => void;
21
+ /**
22
+ * Page that will be selected by default
23
+ * @default 1
24
+ */
25
+ defaultPage?: number;
26
+ /**
27
+ * How many siblings of the active item should be shown
28
+ * @default 1
29
+ */
30
+ siblingCount?: number;
31
+ /**
32
+ * How many of the boundary items should be shown
33
+ * @default 1
34
+ */
35
+ boundaryCount?: number;
36
+ /**
37
+ * Function that can be used to modify the rendered PaginationItem component
38
+ */
39
+ renderItem?: (
40
+ props: PaginationItemProps,
41
+ key?: string | number
42
+ ) => ReactElement;
43
+ /**
44
+ * Shows the skip to first button
45
+ */
46
+ showFirstButton?: boolean;
47
+ /**
48
+ * Shows the skip to last button
49
+ */
50
+ showLastButton?: boolean;
51
+ /**
52
+ * Icon for first button and the last button icon which will be 180deg
53
+ */
54
+ firstLastButtonIcon?: ReactNode;
55
+ /**
56
+ * Icon for prev button and the next button icon which will be 180deg
57
+ */
58
+ prevNextButtonIcon?: ReactNode;
59
+ }
60
+ const arrowLeftIcon = (
61
+ <svg
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ width="24"
64
+ height="24"
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth="2"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ className="lucide lucide-arrow-left-icon lucide-arrow-left"
72
+ >
73
+ <path d="m12 19-7-7 7-7" />
74
+ <path d="M19 12H5" />
75
+ </svg>
76
+ );
77
+ const ArrowLeftToLineIcon = (
78
+ <svg
79
+ xmlns="http://www.w3.org/2000/svg"
80
+ width="24"
81
+ height="24"
82
+ viewBox="0 0 24 24"
83
+ fill="none"
84
+ stroke="currentColor"
85
+ strokeWidth="2"
86
+ strokeLinecap="round"
87
+ strokeLinejoin="round"
88
+ className="lucide lucide-arrow-left-to-line-icon lucide-arrow-left-to-line"
89
+ >
90
+ <path d="M3 19V5" />
91
+ <path d="m13 6-6 6 6 6" />
92
+ <path d="M7 12h14" />
93
+ </svg>
94
+ );
95
+ export function Pagination(props: PaginationProps) {
96
+ const {
97
+ count,
98
+ page: pageProp,
99
+ onChange,
100
+ defaultPage = 1,
101
+ boundaryCount = 1,
102
+ siblingCount = 1,
103
+ showFirstButton = true,
104
+ showLastButton = true,
105
+ firstLastButtonIcon = ArrowLeftToLineIcon,
106
+ prevNextButtonIcon = arrowLeftIcon,
107
+ renderItem = (props, key?) => <PaginationItem key={key} {...props} />,
108
+ } = props;
109
+
110
+ const [page, setPage] = useState(defaultPage);
111
+ const activePage = pageProp ?? page;
112
+
113
+ const paginationRange = usePagination({
114
+ count,
115
+ page: activePage,
116
+ siblingCount,
117
+ boundaryCount,
118
+ });
119
+
120
+ function renderIconButton({
121
+ disabled,
122
+ onClick,
123
+ icon,
124
+ }: {
125
+ disabled: boolean;
126
+ onClick: () => void;
127
+ icon: React.ReactNode;
128
+ }) {
129
+ return renderItem({
130
+ className: cn(styles.item, styles.icon, disabled ? styles.disabled : ""),
131
+ onClick: !disabled ? onClick : undefined,
132
+ children: icon,
133
+ });
134
+ }
135
+
136
+ function handleClick(i: number) {
137
+ if (i < 1 || i > count) return;
138
+ setPage(i);
139
+ if (onChange) onChange(i);
140
+ }
141
+
142
+ return (
143
+ <ul className={styles.root}>
144
+ {showFirstButton &&
145
+ renderIconButton({
146
+ disabled: activePage === 1,
147
+ onClick: () => handleClick(1),
148
+ icon: firstLastButtonIcon,
149
+ })}
150
+ {renderIconButton({
151
+ disabled: activePage === 1,
152
+ onClick: () => handleClick(activePage - 1),
153
+ icon: prevNextButtonIcon,
154
+ })}
155
+
156
+ {paginationRange.map((item, idx) =>
157
+ item === "ellipses" ? (
158
+ <li key={`dots-${idx}`}>…</li>
159
+ ) : (
160
+ renderItem(
161
+ {
162
+ className: cn(
163
+ styles.item,
164
+ activePage === item ? styles.activeItem : ""
165
+ ),
166
+ onClick: () => handleClick(item),
167
+ children: item,
168
+ },
169
+ item
170
+ )
171
+ )
172
+ )}
173
+ {renderIconButton({
174
+ disabled: activePage === count,
175
+ onClick: () => handleClick(activePage + 1),
176
+ icon: prevNextButtonIcon,
177
+ })}
178
+
179
+ {showLastButton &&
180
+ renderIconButton({
181
+ disabled: activePage === count,
182
+ onClick: () => handleClick(count),
183
+ icon: firstLastButtonIcon,
184
+ })}
185
+ </ul>
186
+ );
187
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import type {
4
+ PolymorphicComponentProps,
5
+ PolymorphicComponentType,
6
+ } from "@studiocubics/types";
7
+ import { type ElementType } from "react";
8
+
9
+ interface PaginationItemBaseProps {
10
+ to?: "string";
11
+ }
12
+
13
+ export type PaginationItemProps<C extends ElementType = "li"> =
14
+ PolymorphicComponentProps<C, PaginationItemBaseProps>;
15
+
16
+ const PaginationItemBase = <C extends ElementType = "button">(
17
+ props: PaginationItemProps<C>
18
+ ) => {
19
+ const { to, as, children, ...rest } = props;
20
+ const Component = (as || to ? "a" : "li") as ElementType;
21
+ return <Component {...rest}>{children}</Component>;
22
+ };
23
+
24
+ PaginationItemBase.displayName = "PaginationItem";
25
+ export const PaginationItem = PaginationItemBase as PolymorphicComponentType<
26
+ PaginationItemBaseProps,
27
+ "li"
28
+ >;
@@ -0,0 +1,3 @@
1
+ export * from "./Pagination";
2
+ export * from "./PaginationItem";
3
+ export * from "./usePagination";
@@ -0,0 +1,65 @@
1
+ export function usePagination({
2
+ count,
3
+ page,
4
+ siblingCount = 1,
5
+ boundaryCount = 1,
6
+ }: {
7
+ count: number;
8
+ page: number;
9
+ siblingCount?: number;
10
+ boundaryCount?: number;
11
+ }) {
12
+ const startPages = Array.from(
13
+ { length: Math.min(boundaryCount, count) },
14
+ (_, i) => i + 1
15
+ );
16
+
17
+ const endPages = Array.from(
18
+ { length: Math.min(boundaryCount, count) },
19
+ (_, i) => count - i
20
+ ).reverse();
21
+
22
+ // siblingsStart determines the first page number in the sibling range:
23
+ // - Math.min ensures we don't go beyond the range where siblings would overlap with end pages.
24
+ // - Math.max ensures we don't go below the first possible sibling after the boundary pages.
25
+ const siblingsStart = Math.max(
26
+ Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
27
+ boundaryCount + 2
28
+ );
29
+
30
+ // The upper bound ensures siblings do not overlap with end pages; if endPages exist, use the first end page minus 2, otherwise use count - 1.
31
+ const siblingsEnd = Math.min(
32
+ Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
33
+ endPages.length > 0 ? endPages[0] - 2 : count - 1
34
+ );
35
+
36
+ const itemList: (number | "ellipses")[] = [];
37
+
38
+ // Start pages
39
+ itemList.push(...startPages);
40
+
41
+ // Ellipses after start pages
42
+ if (siblingsStart > boundaryCount + 2) {
43
+ itemList.push("ellipses");
44
+ } else if (boundaryCount + 1 < count - boundaryCount) {
45
+ itemList.push(boundaryCount + 1);
46
+ }
47
+
48
+ // Middle pages
49
+ for (let i = siblingsStart; i <= siblingsEnd; i++) {
50
+ itemList.push(i);
51
+ }
52
+
53
+ // Ellipses before end pages
54
+ if (siblingsEnd < count - boundaryCount - 1) {
55
+ itemList.push("ellipses");
56
+ } else if (count - boundaryCount > boundaryCount) {
57
+ itemList.push(count - boundaryCount);
58
+ }
59
+
60
+ // End pages
61
+ const endPagesFiltered = endPages.filter((p) => !itemList.includes(p));
62
+ itemList.push(...endPagesFiltered);
63
+
64
+ return itemList;
65
+ }
@@ -0,0 +1,43 @@
1
+ .root {
2
+ cursor: pointer;
3
+ position: relative;
4
+ padding: var(--spacing-gap) var(--spacing-gap-2);
5
+ display: flex;
6
+ align-items: center;
7
+ gap: var(--spacing-gap);
8
+
9
+ font-size: var(--fs-body2);
10
+ font-family: var(--font-p);
11
+ color: var(--color-on-background);
12
+ text-wrap: nowrap;
13
+
14
+ border-radius: var(--shape-br-sm);
15
+ border: none;
16
+ background-color: transparent;
17
+
18
+ transition: background-color var(--transition-time) var(--transition-tf);
19
+ }
20
+ .clickable:not(.selected):hover {
21
+ background-color: var(--color-background-faint);
22
+ color: var(--color-on-surface);
23
+ }
24
+
25
+ .selected {
26
+ font-weight: bold;
27
+ color: var(--color-primary);
28
+ &:hover {
29
+ background-color: transparent;
30
+ color: var(--color-on-surface);
31
+ }
32
+ }
33
+
34
+ .disabled {
35
+ color: var(--color-on-background-faint);
36
+ cursor: not-allowed;
37
+ }
38
+ .iconContainer {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ color: inherit;
43
+ }
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ComponentProps,
5
+ type ElementType,
6
+ type ReactNode,
7
+ useEffect,
8
+ useRef,
9
+ type MouseEvent,
10
+ } from "react";
11
+ import styles from "./Tab.module.css";
12
+ import { useTabs } from "../Tabs";
13
+ import { cn, mergeRefs } from "@studiocubics/utils";
14
+ import type {
15
+ PolymorphicComponentProps,
16
+ PolymorphicComponentType,
17
+ } from "@studiocubics/types";
18
+
19
+ /**
20
+ * Props specific to the Tab component.
21
+ *
22
+ * These extend the intrinsic element props of whatever element is passed via `as`.
23
+ * @group Tab
24
+ * @category inputs
25
+ */
26
+ export interface TabBaseProps {
27
+ selected?: boolean;
28
+ startIcon?: ReactNode;
29
+ endIcon?: ReactNode;
30
+ disabled?: boolean;
31
+ href?: ComponentProps<"a">["href"];
32
+ slotProps?: {
33
+ startIcon?: ComponentProps<"span">;
34
+ endIcon?: ComponentProps<"span">;
35
+ };
36
+ }
37
+ const defaultElement = "button";
38
+ type DefaultElement = typeof defaultElement;
39
+ /**
40
+ * Polymorphic props for the Tab component.
41
+ *
42
+ * `C` defines the element type rendered by the component (e.g. `"Tab"`, `"a"`, `"div"`).
43
+ * All intrinsic props for `C` are supported unless overridden by `TabBaseProps`.
44
+ *
45
+ * @group Tab
46
+ * @category inputs
47
+ */
48
+ export type TabProps<C extends ElementType = DefaultElement> =
49
+ PolymorphicComponentProps<C, TabBaseProps>;
50
+
51
+ /**
52
+ * Base implementation for the Tab component.
53
+ *
54
+ * This is a polymorphic component that defaults to rendering a `<Tab>`.
55
+ * Use the `as` prop to change the underlying element.
56
+ *
57
+ * @typeParam C - The intrinsic or custom element type to render.
58
+ *
59
+ * @group Tab
60
+ * @category inputs
61
+ */
62
+ function TabBase<C extends ElementType = DefaultElement>(props: TabProps<C>) {
63
+ const {
64
+ as,
65
+ className,
66
+ selected: _selected,
67
+ startIcon,
68
+ endIcon,
69
+ disabled,
70
+ href,
71
+ onClick,
72
+ onTouchStart,
73
+ children,
74
+ slotProps = {},
75
+ ref,
76
+ ...restProps
77
+ } = props;
78
+ const { activeTab, setActiveTab } = useTabs();
79
+ const tabRef = useRef<HTMLButtonElement>(null);
80
+
81
+ const clickable = !disabled && (!!href || !!onClick);
82
+ const selected = tabRef.current != null ? activeTab == tabRef.current : false;
83
+ const Component = (as || defaultElement) as ElementType;
84
+
85
+ function handleClick(e: MouseEvent<HTMLButtonElement>) {
86
+ if (disabled) return;
87
+ if (tabRef.current)
88
+ tabRef.current.scrollIntoView({
89
+ block: "nearest",
90
+ inline: "center",
91
+ behavior: "smooth",
92
+ });
93
+ setActiveTab(tabRef.current);
94
+ if (onClick) onClick(e);
95
+ }
96
+
97
+ useEffect(() => {
98
+ if (_selected) setActiveTab(tabRef.current);
99
+ }, [_selected]);
100
+
101
+ const componentProps = {
102
+ className: cn(
103
+ className,
104
+ styles.root,
105
+ selected ? styles.selected : undefined,
106
+ clickable ? styles.clickable : undefined,
107
+ disabled ? styles.disabled : undefined,
108
+ ),
109
+ onClick: handleClick,
110
+ disabled,
111
+ href: disabled ? "" : href,
112
+ ref: mergeRefs(ref, tabRef),
113
+ ...restProps,
114
+ };
115
+
116
+ return (
117
+ <Component {...componentProps}>
118
+ {startIcon && (
119
+ <span
120
+ {...slotProps.startIcon}
121
+ className={cn(styles.iconContainer, slotProps.startIcon?.className)}
122
+ >
123
+ {startIcon}
124
+ </span>
125
+ )}
126
+ {children}
127
+ {endIcon && (
128
+ <span
129
+ {...slotProps.endIcon}
130
+ className={cn(styles.iconContainer, slotProps.endIcon?.className)}
131
+ >
132
+ {endIcon}
133
+ </span>
134
+ )}
135
+ </Component>
136
+ );
137
+ }
138
+ TabBase.displayName = "Tab";
139
+
140
+ /**
141
+ * A polymorphic Tab component.
142
+ *
143
+ * By default it renders a `<Tab>`, but any element can be used via the `as` prop:
144
+ *
145
+ * ```tsx
146
+ * <Tab as="a" href="/docs">Read docs</Tab>
147
+ * ```
148
+ *
149
+ * @group Tab
150
+ * @category inputs
151
+ */
152
+ export const Tab = TabBase as PolymorphicComponentType<
153
+ TabBaseProps,
154
+ DefaultElement
155
+ >;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import type { SetState } from "@studiocubics/types";
4
+ import { createContext, type ReactNode, useContext, useState } from "react";
5
+
6
+ interface TabsContextProps {
7
+ activeTab: HTMLButtonElement | null;
8
+ setActiveTab: SetState<HTMLButtonElement | null>;
9
+ }
10
+ export interface TabsProps {
11
+ children: ReactNode;
12
+ }
13
+
14
+ const TabsContext = createContext<TabsContextProps | null>(null);
15
+
16
+ export function useTabs() {
17
+ const c = useContext(TabsContext);
18
+ if (!c) throw new Error("Components must be wrapped in <Tabs/>");
19
+ return c;
20
+ }
21
+
22
+ export function Tabs(props: TabsProps) {
23
+ const { children } = props;
24
+ const [activeTab, setActiveTab] =
25
+ useState<TabsContextProps["activeTab"]>(null);
26
+
27
+ return (
28
+ <TabsContext.Provider
29
+ value={{
30
+ activeTab,
31
+ setActiveTab,
32
+ }}
33
+ >
34
+ {children}
35
+ </TabsContext.Provider>
36
+ );
37
+ }
@@ -0,0 +1,47 @@
1
+ .root {
2
+ overflow: hidden;
3
+ display: flex;
4
+ background: var(--color-background-alpha);
5
+ border-radius: var(--shape-br-sm);
6
+ backdrop-filter: blur(5px);
7
+ pointer-events: all;
8
+ }
9
+ .overflowContainer {
10
+ isolation: isolate;
11
+ display: flex;
12
+ gap: var(--spacing-gap);
13
+ position: relative;
14
+ }
15
+ .rowContainer {
16
+ width: max-content;
17
+ justify-content: flex-start;
18
+ padding: var(--spacing-gap) var(--spacing-gap-2);
19
+ overflow: auto hidden;
20
+ }
21
+
22
+ .columnContainer {
23
+ height: 100%;
24
+ width: 100%;
25
+ flex-direction: column;
26
+ padding: var(--spacing-gap-2);
27
+ overflow: hidden auto;
28
+ gap: var(--spacing-gap);
29
+ }
30
+ .marker {
31
+ position: absolute;
32
+ left: 0;
33
+ width: 0;
34
+ z-index: -1;
35
+ transition: all var(--transition-time) var(--transition-tf);
36
+ }
37
+ .markerGlass {
38
+ width: 100%;
39
+ height: 100%;
40
+ --glass-border-radius: var(--shape-br-sm);
41
+ --glass-backdrop-blur: 0;
42
+ /* background-color: color-mix(
43
+ in srgb,
44
+ var(--color-background) 50%,
45
+ transparent
46
+ ); */
47
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, type ReactNode, type ComponentProps } from "react";
4
+ import { useTabs } from "../Tabs";
5
+ import styles from "./TabsBar.module.css";
6
+ import { cn } from "@studiocubics/utils";
7
+ import {
8
+ GlassCard,
9
+ type GlassCardProps,
10
+ } from "../../../Cards/GlassCard/GlassCard";
11
+
12
+ interface TabsBarProps extends ComponentProps<"div"> {
13
+ column?: boolean;
14
+ slotProps?: {
15
+ overflowContainer?: ComponentProps<"div">;
16
+ marker?: ComponentProps<"span">;
17
+ markerGlass?: GlassCardProps;
18
+ };
19
+ }
20
+
21
+ export function TabsBar(props: TabsBarProps) {
22
+ const {
23
+ children,
24
+ className,
25
+ column = false,
26
+ slotProps = {},
27
+ ...rest
28
+ } = props;
29
+
30
+ const { activeTab } = useTabs();
31
+ const markerRef = useRef<HTMLSpanElement>(null);
32
+ const rootRef = useRef<HTMLDivElement>(null);
33
+
34
+ useEffect(() => {
35
+ if (!rootRef.current || !markerRef.current || !activeTab) return;
36
+
37
+ const updateMarkerPosition = () => {
38
+ if (!rootRef.current || !markerRef.current || !activeTab) return;
39
+
40
+ const marker = markerRef.current;
41
+ const tabRect = activeTab.getBoundingClientRect();
42
+ const rootRect = rootRef.current.getBoundingClientRect();
43
+
44
+ // Account for scroll offset
45
+ const scrollLeft = rootRef.current.scrollLeft;
46
+ const scrollTop = rootRef.current.scrollTop;
47
+
48
+ marker.style.width = `${tabRect.width}px`;
49
+ marker.style.height = `${tabRect.height}px`;
50
+ marker.style.left = `${tabRect.left - rootRect.left + scrollLeft}px`;
51
+ marker.style.top = `${tabRect.top - rootRect.top + scrollTop}px`;
52
+ };
53
+
54
+ updateMarkerPosition();
55
+
56
+ const container = rootRef.current;
57
+
58
+ // Update marker position when container resizes
59
+ const resizeObserver = new ResizeObserver(updateMarkerPosition);
60
+ resizeObserver.observe(container);
61
+
62
+ return () => {
63
+ resizeObserver.disconnect();
64
+ };
65
+ }, [activeTab]);
66
+
67
+ return (
68
+ <nav {...rest} className={cn(styles.root, className)}>
69
+ <div
70
+ {...slotProps.overflowContainer}
71
+ ref={rootRef}
72
+ className={cn(
73
+ styles.overflowContainer,
74
+ column ? styles.columnContainer : styles.rowContainer,
75
+ slotProps.overflowContainer?.className,
76
+ )}
77
+ >
78
+ {children as ReactNode}
79
+ <span
80
+ {...slotProps.marker}
81
+ className={cn(styles.marker, slotProps.marker?.className)}
82
+ ref={markerRef}
83
+ >
84
+ <GlassCard
85
+ {...slotProps.markerGlass}
86
+ className={cn(styles.markerGlass, slotProps.markerGlass?.className)}
87
+ />
88
+ </span>
89
+ </div>
90
+ </nav>
91
+ );
92
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Tabs";
2
+ export * from "./Tab/Tab";
3
+ export * from "./TabsBar/TabsBar";
@@ -0,0 +1,3 @@
1
+ export * from "./Pagination/_index";
2
+ export * from "./Breadcrumbs/_index";
3
+ export * from "./Tabs/_index";
@@ -0,0 +1,5 @@
1
+ .root {
2
+ display: -webkit-box;
3
+ -webkit-box-orient: vertical;
4
+ overflow: hidden;
5
+ }