@studiocubics/cms 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 (85) hide show
  1. package/.turbo/turbo-build.log +7 -0
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +15 -0
  4. package/eslint.config.js +21 -0
  5. package/package.json +79 -0
  6. package/rollup.config.js +48 -0
  7. package/src/clerk/_index.ts +6 -0
  8. package/src/clerk/actions/_index.ts +2 -0
  9. package/src/clerk/actions/invitations.ts +78 -0
  10. package/src/clerk/actions/systemUsers.ts +94 -0
  11. package/src/clerk/auth.ts +34 -0
  12. package/src/clerk/clerk.d.ts +105 -0
  13. package/src/clerk/hasPermission.ts +96 -0
  14. package/src/clerk/rbacConfig.ts +68 -0
  15. package/src/clerk/schemas/_index.ts +1 -0
  16. package/src/clerk/schemas/invitation.ts +17 -0
  17. package/src/clerk/schemas/systemUser.ts +16 -0
  18. package/src/clerk/toClientSafeUser.ts +77 -0
  19. package/src/constants/_index.ts +2 -0
  20. package/src/constants/defaults.tsx +62 -0
  21. package/src/constants/pageLimits.ts +2 -0
  22. package/src/declaration.d.ts +5 -0
  23. package/src/index.ts +5 -0
  24. package/src/providers/CMSRootProviders.tsx +13 -0
  25. package/src/providers/_index.ts +1 -0
  26. package/src/routes.d.ts +96 -0
  27. package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.module.css +4 -0
  28. package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.tsx +16 -0
  29. package/src/ui/Inputs/_index.ts +1 -0
  30. package/src/ui/Layout/CMSSecurityLayout.tsx +27 -0
  31. package/src/ui/Layout/CMSSidebar/CMSSidebar.tsx +39 -0
  32. package/src/ui/Layout/CMSSidebar/CMSSidebarBody.tsx +43 -0
  33. package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.module.css +7 -0
  34. package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.tsx +59 -0
  35. package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.module.css +44 -0
  36. package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.tsx +30 -0
  37. package/src/ui/Layout/CMSSidebar/_index.ts +4 -0
  38. package/src/ui/Layout/_index.ts +2 -0
  39. package/src/ui/System/Auth/SignIn/SignIn.module.css +50 -0
  40. package/src/ui/System/Auth/SignIn/SignIn.tsx +79 -0
  41. package/src/ui/System/Auth/SignIn/_index.ts +2 -0
  42. package/src/ui/System/Auth/SignIn/useSignInForm.tsx +42 -0
  43. package/src/ui/System/Auth/SignUp/SignUp.module.css +48 -0
  44. package/src/ui/System/Auth/SignUp/SignUp.tsx +138 -0
  45. package/src/ui/System/Auth/SignUp/_index.ts +2 -0
  46. package/src/ui/System/Auth/SignUp/useSignUpForm.tsx +54 -0
  47. package/src/ui/System/Auth/_index.ts +2 -0
  48. package/src/ui/System/Invitations/InvitationList.tsx +9 -0
  49. package/src/ui/System/Invitations/InvitationListActions.tsx +167 -0
  50. package/src/ui/System/Invitations/InvitationListCard.tsx +79 -0
  51. package/src/ui/System/Invitations/InvitationListPage.tsx +32 -0
  52. package/src/ui/System/Invitations/InvitationListPagination.tsx +19 -0
  53. package/src/ui/System/Invitations/_index.ts +5 -0
  54. package/src/ui/System/Permissions/RoleListCard.tsx +33 -0
  55. package/src/ui/System/Permissions/RolePermissionsPage.tsx +18 -0
  56. package/src/ui/System/Permissions/RolePermissionsTable.tsx +36 -0
  57. package/src/ui/System/Permissions/_index.ts +3 -0
  58. package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.module.css +5 -0
  59. package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.tsx +102 -0
  60. package/src/ui/System/SystemUser/CurrentSystemUserPage.tsx +12 -0
  61. package/src/ui/System/SystemUser/SystemUserActions.tsx +45 -0
  62. package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.module.css +6 -0
  63. package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.tsx +71 -0
  64. package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.module.css +7 -0
  65. package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.tsx +114 -0
  66. package/src/ui/System/SystemUser/SystemUserList.tsx +18 -0
  67. package/src/ui/System/SystemUser/SystemUserListActions.tsx +17 -0
  68. package/src/ui/System/SystemUser/SystemUserListCard.tsx +85 -0
  69. package/src/ui/System/SystemUser/SystemUserListPage.tsx +33 -0
  70. package/src/ui/System/SystemUser/SystemUserListPagination.tsx +19 -0
  71. package/src/ui/System/SystemUser/SystemUserPage.tsx +30 -0
  72. package/src/ui/System/SystemUser/SystemUserPageContent.tsx +54 -0
  73. package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.module.css +17 -0
  74. package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.tsx +64 -0
  75. package/src/ui/System/SystemUser/SystemUserRoleForm/SystemUserRoleForm.tsx +51 -0
  76. package/src/ui/System/SystemUser/SystemUserTimestamps.tsx +56 -0
  77. package/src/ui/System/SystemUser/_index.ts +14 -0
  78. package/src/ui/System/WelcomePage/WelcomePage.module.css +18 -0
  79. package/src/ui/System/WelcomePage/WelcomePage.tsx +43 -0
  80. package/src/ui/System/_index.ts +6 -0
  81. package/src/ui/System/types.ts +7 -0
  82. package/src/ui/_index.ts +3 -0
  83. package/src/utils/_index.ts +1 -0
  84. package/src/utils/proxyFunctions.ts +37 -0
  85. package/tsconfig.json +32 -0
@@ -0,0 +1,17 @@
1
+ import z from "zod";
2
+ import { roles } from "../rbacConfig";
3
+
4
+ export const createInvitationSchema = z.object({
5
+ emailAddress: z.string(),
6
+ expiresInDays: z
7
+ .preprocess((val) => {
8
+ if (typeof val === "string") {
9
+ return Number.parseInt(val);
10
+ }
11
+ return val;
12
+ }, z.number())
13
+ .optional(),
14
+ ignoreExisting: z.boolean().default(false).optional(),
15
+ notify: z.boolean().default(true).optional(),
16
+ role: z.enum(roles),
17
+ });
@@ -0,0 +1,16 @@
1
+ import z from "zod";
2
+ const MAX_UPLOAD_SIZE = 1024 * 1024 * 5; // 10MB
3
+ const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png"];
4
+ export const systemUserUpdateSchema = z.object({
5
+ firstName: z.string().max(25).min(2),
6
+ lastName: z.string().max(25).min(2),
7
+ imageFile: z
8
+ .file()
9
+ .refine((f) => f.size <= MAX_UPLOAD_SIZE, {
10
+ error: "Image size is larger than >10MB",
11
+ })
12
+ .refine(
13
+ (f) => ACCEPTED_IMAGE_TYPES.includes(f.type),
14
+ "Only .jpg and .png formats are supported.",
15
+ ),
16
+ });
@@ -0,0 +1,77 @@
1
+ import type { User } from "@clerk/nextjs/server";
2
+
3
+ /**
4
+ * Client-safe representation of a Clerk User.
5
+ *
6
+ * This intentionally excludes any sensitive or server-only fields
7
+ * (e.g. privateMetadata, externalAccounts with tokens, etc.)
8
+ * and contains only JSON-serializable data that is safe to pass
9
+ * from Server Components to Client Components.
10
+ */
11
+ export type ClientSafeUser = {
12
+ id: string;
13
+ username: string | null;
14
+ firstName: string | null;
15
+ lastName: string | null;
16
+ fullName: string | null;
17
+ imageUrl: string;
18
+ primaryEmailAddress: {
19
+ id: string;
20
+ emailAddress: string;
21
+ verified: boolean;
22
+ };
23
+ emailAddresses: Array<{
24
+ id: string;
25
+ emailAddress: string;
26
+ verified: boolean;
27
+ }>;
28
+ publicMetadata: CustomJwtSessionClaims["metadata"];
29
+ createdAt: Date | null;
30
+ updatedAt: Date | null;
31
+ lastSignInAt: Date | null;
32
+ };
33
+
34
+ /**
35
+ * Converts a Clerk `User` (server-side) into a client-safe, serializable object.
36
+ *
37
+ * Why this exists:
38
+ * - The Clerk `User` class contains methods and non-serializable fields.
39
+ * - Passing it directly to a Client Component will break React Server Components.
40
+ * - This function strips the object down to stable, safe primitives only.
41
+ *
42
+ * Rules enforced here:
43
+ * - No functions
44
+ * - No circular references
45
+ * - No private or sensitive data
46
+ *
47
+ * @param user Clerk User instance from `@clerk/nextjs/server`
48
+ * @returns ClientSafeUser plain object
49
+ */
50
+ export function toClientSafeUser(user: User): ClientSafeUser {
51
+ const pea = user.emailAddresses.find(
52
+ (ea) => ea.id == user.primaryEmailAddressId
53
+ );
54
+ if (!pea) throw new Error("Missing primaryEmailAddress");
55
+ return {
56
+ id: user.id,
57
+ username: user.username,
58
+ firstName: user.firstName,
59
+ lastName: user.lastName,
60
+ fullName: user.fullName,
61
+ imageUrl: user.imageUrl,
62
+ primaryEmailAddress: {
63
+ id: pea.id,
64
+ emailAddress: pea.emailAddress,
65
+ verified: pea.verification?.status === "verified",
66
+ },
67
+ emailAddresses: user.emailAddresses.map((email) => ({
68
+ id: email.id,
69
+ emailAddress: email.emailAddress,
70
+ verified: email.verification?.status === "verified",
71
+ })),
72
+ publicMetadata: user.publicMetadata ?? {},
73
+ createdAt: new Date(user.createdAt),
74
+ updatedAt: new Date(user.updatedAt),
75
+ lastSignInAt: user.lastSignInAt ? new Date(user.lastSignInAt) : null,
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./pageLimits";
2
+ export * from "./defaults";
@@ -0,0 +1,62 @@
1
+ import type { ListItemProps, TabProps } from "@studiocubics/ui";
2
+ import { Titillium_Web, Outfit } from "next/font/google";
3
+ import {
4
+ Newspaper,
5
+ Settings,
6
+ ShieldUser,
7
+ SquareUser,
8
+ DoorClosedLocked,
9
+ Users,
10
+ Send,
11
+ } from "lucide-react";
12
+
13
+ export const defaultNavLinks: ListItemProps[] = [
14
+ {
15
+ startIcon: <Newspaper />,
16
+ children: "Content",
17
+ href: "/dashboard/content",
18
+ },
19
+ {
20
+ startIcon: <ShieldUser />,
21
+ href: "/dashboard/security",
22
+ children: "Security",
23
+ },
24
+ {
25
+ startIcon: <Settings />,
26
+ href: "/dashboard/settings",
27
+ children: "Settings",
28
+ },
29
+ ];
30
+ export const defaultSecurityLinks: TabProps[] = [
31
+ {
32
+ children: "Your Account",
33
+ startIcon: <SquareUser />,
34
+ href: "/dashboard/security/account",
35
+ },
36
+ {
37
+ children: "Roles & Permissions",
38
+ startIcon: <DoorClosedLocked />,
39
+ href: "/dashboard/security/permissions",
40
+ },
41
+ {
42
+ children: "System Users",
43
+ startIcon: <Users />,
44
+ href: "/dashboard/security/systemUsers",
45
+ },
46
+ {
47
+ children: "Invitations",
48
+ startIcon: <Send />,
49
+ href: "/dashboard/security/invitations",
50
+ },
51
+ ];
52
+ export const fontH = Outfit({
53
+ variable: "--font-h",
54
+ subsets: ["latin"],
55
+ });
56
+
57
+ export const fontP = Titillium_Web({
58
+ variable: "--font-p",
59
+ weight: ["200", "400", "600", "700", "900"],
60
+ subsets: ["latin"],
61
+ });
62
+ export const defaultBodyClassName = `${fontH.variable} ${fontP.variable}`;
@@ -0,0 +1,2 @@
1
+ export const SYSTEM_USERS_PAGE_LIMIT = 10;
2
+ export const INVITATIONS_PAGE_LIMIT = 10;
@@ -0,0 +1,5 @@
1
+ declare module "*.module.css" {
2
+ const classes: { [key: string]: string };
3
+ export default classes;
4
+ }
5
+ declare module "*.css";
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./clerk/_index";
2
+ export * from "./constants/_index";
3
+ export * from "./providers/_index";
4
+ export * from "./ui/_index";
5
+ export * from "./utils/_index";
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import { ThemeProvider } from "next-themes";
4
+ import type { ReactNode } from "react";
5
+ import { ClerkProvider } from "@clerk/nextjs";
6
+
7
+ export function CMSRootProviders({ children }: { children: ReactNode }) {
8
+ return (
9
+ <ThemeProvider enableSystem>
10
+ <ClerkProvider>{children}</ClerkProvider>
11
+ </ThemeProvider>
12
+ );
13
+ }
@@ -0,0 +1 @@
1
+ export * from "./CMSRootProviders";
@@ -0,0 +1,96 @@
1
+ type AppRoutes =
2
+ | "/"
3
+ | "/auth/requestAccount"
4
+ | "/auth/signIn/[[...signIn]]"
5
+ | "/auth/signUp/[[...signUp]]"
6
+ | "/dashboard"
7
+ | "/dashboard/content"
8
+ | "/dashboard/content/projects"
9
+ | "/dashboard/security"
10
+ | "/dashboard/security/account"
11
+ | "/dashboard/security/invitations"
12
+ | "/dashboard/security/permissions"
13
+ | "/dashboard/security/systemUsers"
14
+ | "/dashboard/security/systemUsers/[userId]"
15
+ | "/dashboard/security/systemUsers/[userId]/sessions"
16
+ | "/dashboard/settings";
17
+ type PageRoutes = never;
18
+ type LayoutRoutes = "/" | "/auth" | "/dashboard" | "/dashboard/security";
19
+ type RedirectRoutes = never;
20
+ type RewriteRoutes = never;
21
+ type Routes =
22
+ | AppRoutes
23
+ | PageRoutes
24
+ | LayoutRoutes
25
+ | RedirectRoutes
26
+ | RewriteRoutes;
27
+
28
+ interface ParamMap {
29
+ "/": {};
30
+ "/auth": {};
31
+ "/auth/requestAccount": {};
32
+ "/auth/signIn/[[...signIn]]": { signIn?: string[] };
33
+ "/auth/signUp/[[...signUp]]": { signUp?: string[] };
34
+ "/dashboard": {};
35
+ "/dashboard/content": {};
36
+ "/dashboard/content/projects": {};
37
+ "/dashboard/security": {};
38
+ "/dashboard/security/account": {};
39
+ "/dashboard/security/invitations": {};
40
+ "/dashboard/security/permissions": {};
41
+ "/dashboard/security/systemUsers": {};
42
+ "/dashboard/security/systemUsers/[userId]": { userId: string };
43
+ "/dashboard/security/systemUsers/[userId]/sessions": { userId: string };
44
+ "/dashboard/settings": {};
45
+ }
46
+
47
+ export type ParamsOf<Route extends Routes> = ParamMap[Route];
48
+
49
+ interface LayoutSlotMap {
50
+ "/": never;
51
+ "/auth": never;
52
+ "/dashboard": never;
53
+ "/dashboard/security": never;
54
+ }
55
+
56
+ export type {
57
+ AppRoutes,
58
+ PageRoutes,
59
+ LayoutRoutes,
60
+ RedirectRoutes,
61
+ RewriteRoutes,
62
+ ParamMap,
63
+ };
64
+
65
+ declare global {
66
+ /**
67
+ * Props for Next.js App Router page components
68
+ * @example
69
+ * ```tsx
70
+ * export default function Page(props: PageProps<'/blog/[slug]'>) {
71
+ * const { slug } = await props.params
72
+ * return <div>Blog post: {slug}</div>
73
+ * }
74
+ * ```
75
+ */
76
+ interface PageProps<AppRoute extends AppRoutes> {
77
+ params: Promise<ParamMap[AppRoute]>;
78
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
79
+ }
80
+
81
+ /**
82
+ * Props for Next.js App Router layout components
83
+ * @example
84
+ * ```tsx
85
+ * export default function Layout(props: LayoutProps<'/dashboard'>) {
86
+ * return <div>{props.children}</div>
87
+ * }
88
+ * ```
89
+ */
90
+ type LayoutProps<LayoutRoute extends LayoutRoutes> = {
91
+ params: Promise<ParamMap[LayoutRoute]>;
92
+ children: React.ReactNode;
93
+ } & {
94
+ [K in LayoutSlotMap[LayoutRoute]]: React.ReactNode;
95
+ };
96
+ }
@@ -0,0 +1,4 @@
1
+ .root {
2
+ border-radius: var(--shape-br-sm);
3
+ overflow: hidden;
4
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { Editor, type EditorProps } from "@monaco-editor/react";
4
+ import { useTheme } from "next-themes";
5
+ import styles from "./ThemedMonacoEditor.module.css";
6
+ export function ThemedMonacoEditor(props: EditorProps) {
7
+ const theme = useTheme();
8
+
9
+ return (
10
+ <Editor
11
+ className={styles.root}
12
+ theme={theme.resolvedTheme == "dark" ? "vs-dark" : "vs-light"}
13
+ {...props}
14
+ />
15
+ );
16
+ }
@@ -0,0 +1 @@
1
+ export * from "./ThemedMonacoEditor/ThemedMonacoEditor";
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { PageLayoutTabs, type PageLayoutTabsProps } from "@studiocubics/ui";
4
+ import Link from "next/link";
5
+ import { usePathname } from "next/navigation";
6
+
7
+ export function CMSSecurityLayout(
8
+ props: Omit<PageLayoutTabsProps, "title" | "subtitle" | "selectedIndex">,
9
+ ) {
10
+ const { menuItems } = props;
11
+ const pathname = usePathname();
12
+ const selectedItem = menuItems?.find(
13
+ (mi) => mi.href && pathname.startsWith(mi.href),
14
+ );
15
+ const selectedIndex = selectedItem
16
+ ? menuItems?.indexOf(selectedItem)
17
+ : undefined;
18
+ return (
19
+ <PageLayoutTabs
20
+ {...props}
21
+ subtitle={"Manage your account and other System security options"}
22
+ title={"Security"}
23
+ selectedIndex={selectedIndex}
24
+ LinkComponent={Link}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ Sidebar,
3
+ SidebarViewport,
4
+ SidebarDrawer,
5
+ type ListItemProps,
6
+ } from "@studiocubics/ui";
7
+ import type { ReactNode } from "react";
8
+ import { CMSSidebarHeader } from "./CMSSidebarHeader/CMSSidebarHeader";
9
+ import { CMSSidebarFooter } from "./CMSSidebarFooter/CMSSidebarFooter";
10
+ import { CMSSidebarBody } from "./CMSSidebarBody";
11
+ import { cookies } from "next/headers";
12
+
13
+ export async function CMSSidebar({
14
+ children,
15
+ sidebarLinks,
16
+ }: {
17
+ children: ReactNode;
18
+ sidebarLinks: ListItemProps[];
19
+ }) {
20
+ const cookieStore = await cookies();
21
+ const defaultOpen = cookieStore.get("sidebarOpen")?.value === "true";
22
+
23
+ return (
24
+ <div style={{ height: "100dvh" }}>
25
+ <Sidebar defaultOpen={defaultOpen}>
26
+ <SidebarDrawer>
27
+ <CMSSidebarHeader />
28
+ <CMSSidebarBody sidebarLinks={sidebarLinks} />
29
+ <CMSSidebarFooter />
30
+ </SidebarDrawer>
31
+ <SidebarViewport
32
+ style={{ backgroundColor: "var(--color-surface-alpha)" }}
33
+ >
34
+ {children}
35
+ </SidebarViewport>
36
+ </Sidebar>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import {
4
+ List,
5
+ useSidebar,
6
+ SidebarBody,
7
+ ListItem,
8
+ type ListItemProps,
9
+ } from "@studiocubics/ui";
10
+ import { usePathname } from "next/navigation";
11
+ import Link from "next/link";
12
+ import { useScreenSize } from "@studiocubics/hooks";
13
+
14
+ export function CMSSidebarBody({
15
+ sidebarLinks,
16
+ }: {
17
+ sidebarLinks: ListItemProps[];
18
+ }) {
19
+ const { sidebarOpen, toggleSidebar } = useSidebar();
20
+ const pathname = usePathname();
21
+ const screenSize = useScreenSize();
22
+
23
+ return (
24
+ <SidebarBody>
25
+ <List>
26
+ {sidebarLinks.map((sl, i) => (
27
+ <ListItem
28
+ LinkComponent={Link}
29
+ key={i}
30
+ {...sl}
31
+ onClick={
32
+ screenSize?.ltMedium && !sl.childNodes?.length && sidebarOpen
33
+ ? toggleSidebar
34
+ : undefined
35
+ }
36
+ shortened={!sidebarOpen}
37
+ selected={sl.href ? pathname.startsWith(sl.href) : undefined}
38
+ />
39
+ ))}
40
+ </List>
41
+ </SidebarBody>
42
+ );
43
+ }
@@ -0,0 +1,7 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: flex-end;
5
+ height: 100%;
6
+ gap: var(--spacing-gap-2);
7
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useMounted } from "@studiocubics/hooks";
4
+ import {
5
+ useSidebar,
6
+ SidebarFooter,
7
+ ThemeToggleListItem,
8
+ type ThemeObject,
9
+ List,
10
+ } from "@studiocubics/ui";
11
+ import { Moon, Sun, Monitor } from "lucide-react";
12
+ import { useTheme } from "next-themes";
13
+ import styles from "./CMSSidebarFooter.module.css";
14
+ import { CurrentSystemUserButton } from "../../../System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton";
15
+
16
+ export function CMSSidebarFooter() {
17
+ const { sidebarOpen } = useSidebar();
18
+ const { theme, setTheme } = useTheme();
19
+ const { mounted } = useMounted();
20
+ if (!mounted) return;
21
+ return (
22
+ <SidebarFooter>
23
+ <div className={styles.root}>
24
+ {theme && (
25
+ <List>
26
+ <ThemeToggleListItem
27
+ shortened={!sidebarOpen}
28
+ currentTheme={theme as keyof ThemeObject}
29
+ themeObject={{
30
+ dark: {
31
+ icon: <Moon />,
32
+ onClick: () => {
33
+ setTheme("dark");
34
+ },
35
+ },
36
+ light: {
37
+ icon: <Sun />,
38
+ onClick: () => {
39
+ setTheme("light");
40
+ },
41
+ },
42
+ system: {
43
+ icon: <Monitor />,
44
+ onClick: () => {
45
+ setTheme("system");
46
+ },
47
+ },
48
+ }}
49
+ />
50
+ </List>
51
+ )}
52
+ <CurrentSystemUserButton
53
+ clickable
54
+ variant={sidebarOpen ? "compact" : "image-only"}
55
+ />
56
+ </div>
57
+ </SidebarFooter>
58
+ );
59
+ }
@@ -0,0 +1,44 @@
1
+ .root {
2
+ flex: 1;
3
+ display: flex;
4
+ flex-direction: row;
5
+ align-items: center;
6
+ padding-inline: 8.33%;
7
+ gap: var(--spacing-gap);
8
+
9
+ padding-top: var(--spacing-gap-3);
10
+ padding-bottom: var(--spacing-gap-2);
11
+ transition: padding var(--transition-time) var(--transition-tf);
12
+ }
13
+ .logoContainer {
14
+ flex: 1;
15
+ display: flex;
16
+ justify-content: flex-start;
17
+ align-items: center;
18
+ }
19
+
20
+ .logo {
21
+ max-width: 6rem;
22
+ height: auto;
23
+ width: 40%;
24
+ }
25
+
26
+ @media (min-width: 600px) {
27
+ .root {
28
+ flex: 0 1 0%;
29
+ flex-direction: column;
30
+ align-items: center;
31
+ padding-inline: 0;
32
+ }
33
+ .root[data-open="true"] {
34
+ flex-direction: row;
35
+ justify-content: space-between;
36
+ padding-top: var(--spacing-gap-2);
37
+ & .logo {
38
+ width: 100%;
39
+ }
40
+ }
41
+ .logoContainer {
42
+ justify-content: center;
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { useScreenSize } from "@studiocubics/hooks";
4
+ import {
5
+ Button,
6
+ CubicsUILogo,
7
+ SidebarHeader,
8
+ useSidebar,
9
+ } from "@studiocubics/ui";
10
+ import styles from "./CMSSidebarHeader.module.css";
11
+ import { cn } from "@studiocubics/utils";
12
+ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
13
+
14
+ export function CMSSidebarHeader() {
15
+ const { sidebarOpen, toggleSidebar } = useSidebar();
16
+ const screen = useScreenSize();
17
+ return (
18
+ <SidebarHeader className={styles.root}>
19
+ <div className={styles.logoContainer}>
20
+ <CubicsUILogo
21
+ onlyFavicon={screen?.gtSmall ? !sidebarOpen : false}
22
+ className={cn(styles.logo)}
23
+ />
24
+ </div>
25
+ <Button square onClick={toggleSidebar}>
26
+ {sidebarOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
27
+ </Button>
28
+ </SidebarHeader>
29
+ );
30
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./CMSSidebar";
2
+ export * from "./CMSSidebarBody";
3
+ export * from "./CMSSidebarFooter/CMSSidebarFooter";
4
+ export * from "./CMSSidebarHeader/CMSSidebarHeader";
@@ -0,0 +1,2 @@
1
+ export * from "./CMSSecurityLayout"
2
+ export * from "./CMSSidebar/_index"
@@ -0,0 +1,50 @@
1
+ /* Yes this is seperate its meant to be separate dont make the SignUp and SignIn styles into one eventhough they are the exact same. Just dont do it ples! */
2
+ .root,
3
+ .body,
4
+ .header,
5
+ .form,
6
+ .footer {
7
+ display: flex;
8
+ flex-direction: column;
9
+ }
10
+ .root {
11
+ width: clamp(350px, 50vw, 550px);
12
+ align-items: center;
13
+ gap: var(--spacing-gap-7);
14
+ & p {
15
+ font-size: var(--fs-body2);
16
+ text-align: center;
17
+ }
18
+ }
19
+ .body {
20
+ width: 100%;
21
+ align-items: center;
22
+ gap: var(--spacing-gap-3);
23
+ }
24
+ .header {
25
+ align-items: center;
26
+ text-align: center;
27
+ gap: var(--spacing-gap);
28
+ & > h1 {
29
+ font-size: var(--fs-h3);
30
+ }
31
+ & > p {
32
+ font-size: var(--fs-body2);
33
+ color: var(--color-on-background-faint);
34
+ }
35
+ }
36
+ .form {
37
+ width: 100%;
38
+ gap: var(--spacing-gap-2);
39
+ }
40
+ .footer {
41
+ width: 100%;
42
+ padding: var(--spacing-gap-3);
43
+ align-items: center;
44
+ border-top: 1px solid var(--color-outline);
45
+ }
46
+ .error {
47
+ font-size: 2rem;
48
+ color: var(--color-error);
49
+ }
50
+