@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,15 @@
1
+ .root[open] {
2
+ background-color: var(--color-surface);
3
+ width: 50%;
4
+ max-height: 80%;
5
+ max-width: 45rem;
6
+ min-width: 350px;
7
+ backdrop-filter: var(--backdrop-blur);
8
+ margin: auto;
9
+ border-radius: var(--shape-br-lg);
10
+ overflow: hidden;
11
+ padding: var(--spacing-gap-6);
12
+ }
13
+ .root::backdrop {
14
+ backdrop-filter: var(--backdrop-blur);
15
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ComponentProps,
5
+ type ReactNode,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import ReactDOM from "react-dom";
11
+ import styles from "./Dialog.module.css";
12
+ import { cn } from "@studiocubics/utils";
13
+
14
+ export type CloseReason = "backdropClick" | "escapeKeyDown";
15
+
16
+ type OmittedProps = "onClose" | "onCancel";
17
+
18
+ export interface DialogProps
19
+ extends Omit<ComponentProps<"dialog">, OmittedProps> {
20
+ open: boolean;
21
+ onClose: (event: Event | MouseEvent, reason: CloseReason) => void;
22
+ children?: ReactNode;
23
+ fullWidth?: boolean;
24
+ fullScreen?: boolean;
25
+ disablePortal?: boolean;
26
+ }
27
+
28
+ export function Dialog({
29
+ open,
30
+ onClose,
31
+ children,
32
+ fullWidth = false,
33
+ fullScreen = false,
34
+ disablePortal = false,
35
+ className = "",
36
+ ...rest
37
+ }: DialogProps) {
38
+ const dialogRef = useRef<HTMLDialogElement>(null);
39
+ const [mounted, setMounted] = useState(false);
40
+ // Only mark as mounted on client
41
+ useEffect(() => {
42
+ setMounted(true);
43
+ }, []);
44
+
45
+ // Show or close dialog using native API
46
+ useEffect(() => {
47
+ if (!mounted) return;
48
+
49
+ const dialog = dialogRef.current;
50
+ if (!dialog) return;
51
+
52
+ if (open && !dialog.open) {
53
+ dialog.showModal();
54
+ } else if (!open && dialog.open) {
55
+ dialog.close();
56
+ }
57
+ }, [open, mounted]);
58
+
59
+ // Close handlers
60
+ useEffect(() => {
61
+ if (!mounted) return;
62
+
63
+ const dialog = dialogRef.current;
64
+ if (!dialog) return;
65
+
66
+ function handleCancel(e: Event) {
67
+ e.preventDefault();
68
+ onClose(e, "escapeKeyDown");
69
+ }
70
+
71
+ function handleClick(e: MouseEvent) {
72
+ if (!dialog) return;
73
+ const rect = dialog.getBoundingClientRect();
74
+ const isInDialog =
75
+ rect.top <= e.clientY &&
76
+ e.clientY <= rect.top + rect.height &&
77
+ rect.left <= e.clientX &&
78
+ e.clientX <= rect.left + rect.width;
79
+
80
+ if (!isInDialog) {
81
+ onClose(e, "backdropClick");
82
+ }
83
+ }
84
+
85
+ dialog.addEventListener("cancel", handleCancel);
86
+ dialog.addEventListener("click", handleClick);
87
+
88
+ return () => {
89
+ dialog.removeEventListener("cancel", handleCancel);
90
+ dialog.removeEventListener("click", handleClick);
91
+ };
92
+ }, [onClose, mounted]);
93
+
94
+ const dialogContent = (
95
+ <dialog
96
+ {...rest}
97
+ ref={dialogRef}
98
+ style={{
99
+ width: fullWidth || fullScreen ? "100%" : undefined,
100
+ height: fullScreen ? "100%" : undefined,
101
+ }}
102
+ className={cn(className, styles.root)}
103
+ >
104
+ {children}
105
+ </dialog>
106
+ );
107
+
108
+ // Only render portal on client
109
+ if (!mounted) return null;
110
+ if (!open) return null;
111
+
112
+ if (disablePortal) return dialogContent;
113
+
114
+ return ReactDOM.createPortal(dialogContent, document.body);
115
+ }
@@ -0,0 +1,20 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ }
7
+ .titleBar {
8
+ padding-inline: var(--spacing-gap-3);
9
+ }
10
+ .body {
11
+ flex: 1;
12
+ overflow: hidden auto;
13
+ padding-block: var(--spacing-gap-2);
14
+ }
15
+ .footer {
16
+ padding-top: var(--spacing-gap-2);
17
+ padding-bottom: 3dvh;
18
+ display: flex;
19
+ justify-content: center;
20
+ }
@@ -0,0 +1,79 @@
1
+ import styles from "./PageLayout.module.css";
2
+ import type { ComponentProps, ElementType, ReactNode } from "react";
3
+ import { cn } from "@studiocubics/utils";
4
+ import type {
5
+ PolymorphicComponentProps,
6
+ PolymorphicComponentType,
7
+ } from "@studiocubics/types";
8
+ import {
9
+ PageTitle,
10
+ type PageTitleProps,
11
+ } from "../../Typography/PageTitle/PageTitle";
12
+
13
+ export interface PageLayoutBaseProps extends PageTitleProps {
14
+ children: ReactNode;
15
+ titleBar?: ReactNode;
16
+ footer?: ReactNode;
17
+ slotProps?: {
18
+ root?: ComponentProps<"main">;
19
+ body?: ComponentProps<"div">;
20
+ footer?: ComponentProps<"div">;
21
+ titleBar?: ComponentProps<"div">;
22
+ };
23
+ }
24
+ const defaultElement = "div";
25
+ type DefaultElement = typeof defaultElement;
26
+
27
+ export type PageLayoutProps<C extends ElementType = DefaultElement> =
28
+ PolymorphicComponentProps<C, PageLayoutBaseProps>;
29
+
30
+ function PageLayoutBase(props: PageLayoutProps) {
31
+ const { as, children, titleBar, footer, slotProps = {}, ...rest } = props;
32
+ const Component = (as || defaultElement) as ElementType;
33
+ return (
34
+ <Component
35
+ {...slotProps.root}
36
+ className={cn(styles.root, slotProps.root?.className)}
37
+ >
38
+ <PageTitle {...rest} />
39
+ {titleBar && (
40
+ <div
41
+ {...slotProps.titleBar}
42
+ className={cn(styles.titleBar, slotProps.titleBar?.className)}
43
+ >
44
+ {titleBar}
45
+ </div>
46
+ )}
47
+
48
+ <div
49
+ {...slotProps.body}
50
+ className={cn(styles.body, slotProps.body?.className)}
51
+ >
52
+ {children}
53
+ </div>
54
+ {footer && (
55
+ <div
56
+ {...slotProps.footer}
57
+ className={cn(styles.footer, slotProps.footer?.className)}
58
+ >
59
+ {footer}
60
+ </div>
61
+ )}
62
+ </Component>
63
+ );
64
+ }
65
+ PageLayoutBase.displayName = "PageLayout";
66
+
67
+ /**
68
+ * A polymorphic PageLayout component.
69
+ *
70
+ * By default it renders a `<main>`, but any element can be used via the `as` prop:
71
+ *
72
+ * ```tsx
73
+ * <PageLayout as="a" href="/docs">Read docs</PageLayout>
74
+ * ```
75
+ */
76
+ export const PageLayout = PageLayoutBase as PolymorphicComponentType<
77
+ PageLayoutBaseProps,
78
+ DefaultElement
79
+ >;
@@ -0,0 +1,5 @@
1
+ .list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-gap);
5
+ }
@@ -0,0 +1,40 @@
1
+ import { type ReactNode, Suspense } from "react";
2
+ import { PageLayout, type PageLayoutProps } from "../PageLayout/PageLayout";
3
+ import styles from "./PageLayoutPagination.module.css";
4
+ import { Skeleton } from "../../Display/Skeleton/Skeleton";
5
+
6
+ export interface PageLayoutPaginationProps {
7
+ skeletonHeight?: number;
8
+ paginationComponent?: ReactNode;
9
+ }
10
+
11
+ export function PageLayoutPagination(
12
+ props: PageLayoutPaginationProps & Omit<PageLayoutProps, "footer">,
13
+ ) {
14
+ const {
15
+ skeletonHeight = 107,
16
+ paginationComponent,
17
+ children,
18
+ ...rest
19
+ } = props;
20
+ return (
21
+ <PageLayout
22
+ footer={
23
+ <Suspense fallback={<Skeleton height={32} />}>
24
+ {paginationComponent}
25
+ </Suspense>
26
+ }
27
+ {...rest}
28
+ >
29
+ <div className={styles.list}>
30
+ <Suspense
31
+ fallback={Array.from({ length: 3 }).map((_, i) => (
32
+ <Skeleton key={i} height={skeletonHeight} />
33
+ ))}
34
+ >
35
+ {children}
36
+ </Suspense>
37
+ </div>
38
+ </PageLayout>
39
+ );
40
+ }
@@ -0,0 +1,3 @@
1
+ .body {
2
+ padding-inline: var(--spacing-gap-3);
3
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { type PageTitleProps } from "../../Typography/PageTitle/PageTitle";
4
+ import type { ComponentProps, ElementType, ReactNode } from "react";
5
+ import { Tabs } from "../../Navigation/Tabs/Tabs";
6
+ import { TabsBar } from "../../Navigation/Tabs/TabsBar/TabsBar";
7
+ import { Tab, type TabProps } from "../../Navigation/Tabs/Tab/Tab";
8
+ import { PageLayout, type PageLayoutProps } from "../PageLayout/PageLayout";
9
+ import { cn } from "@studiocubics/utils";
10
+ import styles from "./PageLayoutTabs.module.css";
11
+
12
+ export interface PageLayoutTabsProps extends PageTitleProps {
13
+ children: ReactNode;
14
+ menuItems?: TabProps[];
15
+ LinkComponent?: ElementType<ComponentProps<any>>;
16
+ selectedIndex?: number;
17
+ slotProps?: PageLayoutProps["slotProps"] & {
18
+ tab?: TabProps;
19
+ };
20
+ }
21
+
22
+ export function PageLayoutTabs(props: PageLayoutTabsProps) {
23
+ const {
24
+ children,
25
+ menuItems = [],
26
+ slotProps = {},
27
+ selectedIndex = 0,
28
+ LinkComponent = "a",
29
+ ...rest
30
+ } = props;
31
+
32
+ return (
33
+ <PageLayout
34
+ {...rest}
35
+ slotProps={{
36
+ ...slotProps,
37
+ body: {
38
+ ...slotProps.body,
39
+ className: cn(slotProps.body?.className, styles.body),
40
+ },
41
+ }}
42
+ titleBar={
43
+ <Tabs>
44
+ <TabsBar>
45
+ {menuItems.map((mi, i) => (
46
+ <Tab
47
+ key={i}
48
+ {...slotProps.tab}
49
+ as={LinkComponent}
50
+ selected={i == selectedIndex}
51
+ {...mi}
52
+ />
53
+ ))}
54
+ </TabsBar>
55
+ </Tabs>
56
+ }
57
+ noBorders
58
+ >
59
+ {children}
60
+ </PageLayout>
61
+ );
62
+ }
@@ -0,0 +1,9 @@
1
+ .root {
2
+ position: absolute;
3
+ background: none;
4
+ border: none;
5
+ }
6
+ .root.open::backdrop {
7
+ /* background-color: red; */
8
+ cursor: initial;
9
+ }
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, useRef, useEffect, type ComponentProps } from "react";
4
+ import styles from "./Popover.module.css";
5
+ import { cn } from "@studiocubics/utils";
6
+
7
+ export interface PositionType {
8
+ vertical: "top" | "center" | "bottom";
9
+ horizontal: "left" | "center" | "right";
10
+ }
11
+
12
+ export interface PopoverProps extends ComponentProps<"div"> {
13
+ children: ReactNode;
14
+ anchorEl: HTMLElement | null;
15
+ open: boolean;
16
+ onClose: () => void;
17
+ anchorPosition?: PositionType;
18
+ transformOrigin?: PositionType;
19
+ }
20
+
21
+ export function Popover({
22
+ children,
23
+ anchorEl,
24
+ open,
25
+ className,
26
+ onClose,
27
+ anchorPosition = { vertical: "bottom", horizontal: "center" },
28
+ transformOrigin = { vertical: "top", horizontal: "left" },
29
+ ...rest
30
+ }: PopoverProps) {
31
+ const popoverRef = useRef<HTMLDivElement>(null);
32
+
33
+ // Sync controlled `open` state with native API
34
+ useEffect(() => {
35
+ const popover = popoverRef.current;
36
+ if (!popover) return;
37
+
38
+ if (open && !popover.matches(":popover-open")) {
39
+ popover.showPopover();
40
+ } else if (!open && popover.matches(":popover-open")) {
41
+ popover.hidePopover();
42
+ }
43
+ }, [open]);
44
+
45
+ // Handle outside click + escape
46
+ useEffect(() => {
47
+ const popover = popoverRef.current;
48
+ if (!popover) return;
49
+
50
+ const handleToggle = () => {
51
+ if (!popover.matches(":popover-open")) {
52
+ onClose();
53
+ }
54
+ };
55
+
56
+ popover.addEventListener("toggle", handleToggle);
57
+ return () => {
58
+ popover.removeEventListener("toggle", handleToggle);
59
+ };
60
+ }, [onClose]);
61
+
62
+ // JS positioning
63
+ useEffect(() => {
64
+ const popover = popoverRef.current;
65
+ if (!popover || !anchorEl || !open) return;
66
+
67
+ const anchorRect = anchorEl.getBoundingClientRect();
68
+ const popoverRect = popover.getBoundingClientRect();
69
+ const margin = 8;
70
+
71
+ let top = 0;
72
+ let left = 0;
73
+
74
+ // Compute anchor-based position
75
+ switch (anchorPosition.vertical) {
76
+ case "top":
77
+ top = anchorRect.top;
78
+ break;
79
+ case "center":
80
+ top = anchorRect.top + anchorRect.height / 2;
81
+ break;
82
+ case "bottom":
83
+ top = anchorRect.bottom;
84
+ break;
85
+ }
86
+
87
+ switch (anchorPosition.horizontal) {
88
+ case "left":
89
+ left = anchorRect.left;
90
+ break;
91
+ case "center":
92
+ left = anchorRect.left + anchorRect.width / 2;
93
+ break;
94
+ case "right":
95
+ left = anchorRect.right;
96
+ break;
97
+ }
98
+
99
+ // Apply transform origin
100
+ switch (transformOrigin.vertical) {
101
+ case "center":
102
+ top -= popoverRect.height / 2;
103
+ break;
104
+ case "bottom":
105
+ top -= popoverRect.height;
106
+ break;
107
+ // top -> no change
108
+ }
109
+
110
+ switch (transformOrigin.horizontal) {
111
+ case "center":
112
+ left -= popoverRect.width / 2;
113
+ break;
114
+ case "right":
115
+ left -= popoverRect.width;
116
+ break;
117
+ // left -> no change
118
+ }
119
+
120
+ // Clamp so the popover stays fully in viewport
121
+ const viewportWidth = document.documentElement.clientWidth;
122
+ const viewportHeight = document.documentElement.clientHeight;
123
+
124
+ const maxLeft = viewportWidth - popoverRect.width - margin;
125
+ const maxTop = viewportHeight - popoverRect.height - margin;
126
+
127
+ left = Math.min(Math.max(left, margin), maxLeft);
128
+ top = Math.min(Math.max(top, margin), maxTop);
129
+
130
+ // Apply scroll offset
131
+ popover.style.top = `${top + window.scrollY}px`;
132
+ popover.style.left = `${left + window.scrollX}px`;
133
+ }, [open, anchorEl, anchorPosition, transformOrigin]);
134
+
135
+ return (
136
+ <div
137
+ {...rest}
138
+ className={cn(styles.root, className, open ? styles.open : "")}
139
+ ref={popoverRef}
140
+ popover="auto"
141
+ >
142
+ {children}
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,31 @@
1
+ .root {
2
+ display: grid;
3
+ grid-template-columns: 1fr;
4
+ padding-inline: var(--spacing-gap-4);
5
+ padding-block: var(--spacing-gap-3);
6
+ gap: var(--spacing-gap-2);
7
+ }
8
+ .header {
9
+ /* outline: 1px solid red; */
10
+ & > h4 {
11
+ font-size: 1em;
12
+ }
13
+ & > p {
14
+ font-size: var(--fs-body2);
15
+ color: var(--color-on-background-faint);
16
+ }
17
+ }
18
+ .root:not(.noBorders) {
19
+ border-top: 1px solid var(--color-outline);
20
+ }
21
+ .content {
22
+ display: flex;
23
+ flex-direction: column;
24
+ font-size: var(--fs-body2);
25
+ container-type: inline-size;
26
+ }
27
+ @container (min-width:600px) {
28
+ .root {
29
+ grid-template-columns: 0.4fr 1fr;
30
+ }
31
+ }
@@ -0,0 +1,62 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ import styles from "./SectionWrapper.module.css";
3
+ import { cn, cssSafeString } from "@studiocubics/utils";
4
+
5
+ export interface SectionWrapperProps
6
+ extends Omit<ComponentProps<"section">, "title"> {
7
+ title: ReactNode;
8
+ subtitle?: ReactNode;
9
+ noBorders?: boolean;
10
+ containerClassname?: string;
11
+ slotProps?: {
12
+ header?: ComponentProps<"div">;
13
+ title?: ComponentProps<"h4">;
14
+ subtitle?: ComponentProps<"p">;
15
+ content?: ComponentProps<"div">;
16
+ };
17
+ }
18
+ export function SectionWrapper(props: SectionWrapperProps) {
19
+ const {
20
+ title,
21
+ subtitle,
22
+ children,
23
+ noBorders,
24
+ slotProps = {},
25
+ containerClassname,
26
+ className,
27
+ ...rest
28
+ } = props;
29
+ return (
30
+ <section
31
+ className={cn(
32
+ containerClassname,
33
+ styles.root,
34
+ noBorders ? styles.noBorders : ""
35
+ )}
36
+ id={typeof title == "string" ? cssSafeString(title) : undefined}
37
+ {...rest}
38
+ >
39
+ <div
40
+ {...slotProps.header}
41
+ className={cn(styles.header, slotProps.header?.className)}
42
+ >
43
+ {typeof title == "string" ? (
44
+ <h4 {...slotProps.title}>{title}</h4>
45
+ ) : (
46
+ title
47
+ )}
48
+ {subtitle && typeof subtitle == "string" ? (
49
+ <p {...slotProps.subtitle}>{subtitle}</p>
50
+ ) : (
51
+ subtitle
52
+ )}
53
+ </div>
54
+ <div
55
+ {...slotProps.content}
56
+ className={cn(styles.content, slotProps.content?.className, className)}
57
+ >
58
+ {children}
59
+ </div>
60
+ </section>
61
+ );
62
+ }
@@ -0,0 +1,17 @@
1
+ .root {
2
+ --sidebar-closed-width: 4rem;
3
+ --sidebar-open-width: 20rem;
4
+ --sidebar-body-height: 65dvh;
5
+ --sidebar-header-height: 4.875rem;
6
+ --sidebar-color-surface: var(--color-surface-alpha);
7
+ display: flex;
8
+ height: 100%;
9
+ flex-direction: column;
10
+ gap: var(--spacing-gap);
11
+ isolation: isolate;
12
+ }
13
+ @media (min-width: 600px) {
14
+ .root {
15
+ flex-direction: row;
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { createContext, type ReactNode, useContext, useState } from "react";
4
+ import styles from "./Sidebar.module.css";
5
+
6
+ interface SidebarContextProps {
7
+ sidebarOpen: boolean;
8
+ toggleSidebar(): void;
9
+ }
10
+ interface SidebarProps {
11
+ children: ReactNode;
12
+ defaultOpen?: boolean;
13
+ storageKey?: string;
14
+ }
15
+
16
+ const SidebarContext = createContext<SidebarContextProps | null>(null);
17
+
18
+ export function useSidebar() {
19
+ const c = useContext(SidebarContext);
20
+ if (!c) throw new Error("Components must be wrapped in <SidebarProvider/>");
21
+ return c;
22
+ }
23
+ export const SIDEBAR_COOKIE = "sidebarOpen";
24
+ export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
25
+
26
+ export function Sidebar({ children, defaultOpen }: SidebarProps) {
27
+ const [sidebarOpen, setSidebarOpen] = useState(defaultOpen ?? false);
28
+
29
+ async function toggleSidebar() {
30
+ setSidebarOpen((v) => !v);
31
+ document.cookie = `${SIDEBAR_COOKIE}=${!sidebarOpen}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
32
+ }
33
+
34
+ return (
35
+ <SidebarContext.Provider value={{ sidebarOpen, toggleSidebar }}>
36
+ <div className={styles.root}>{children}</div>
37
+ </SidebarContext.Provider>
38
+ );
39
+ }
@@ -0,0 +1,31 @@
1
+ .root {
2
+ /* display: none; */
3
+ position: fixed;
4
+ left: -100%;
5
+ top: var(--sidebar-header-height);
6
+ width: var(--sidebar-open-width);
7
+ background-color: var(--color-surface-alpha);
8
+ backdrop-filter: var(--backdrop-blur);
9
+ height: var(--sidebar-body-height);
10
+ transition: left var(--transition-time) var(--transition-tf);
11
+ overflow-x: hidden;
12
+ }
13
+ .open {
14
+ left: 0;
15
+ overflow-x: auto;
16
+ }
17
+ @media (min-width: 600px) {
18
+ .root {
19
+ flex: 1;
20
+ left: 0;
21
+ position: relative;
22
+ height: unset;
23
+ width: unset;
24
+ top: 0;
25
+ background-color: transparent;
26
+ backdrop-filter: none;
27
+ }
28
+ .open {
29
+ left: 0;
30
+ }
31
+ }