diff --git a/src/app/admin/studio/layout.tsx b/src/app/admin/studio/layout.tsx
index 09b6bd74..ad69d286 100644
--- a/src/app/admin/studio/layout.tsx
+++ b/src/app/admin/studio/layout.tsx
@@ -1,158 +1,21 @@
-"use client";
+import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
+import { StudioLayoutClient } from "./studio-layout-client";
-import {
- BadgeCheck,
- Cat,
- ChevronDown,
- ClipboardCheck,
- Copy,
- Dumbbell,
- Image as ImageIcon,
- type LucideIcon,
- Puzzle,
- RefreshCw,
- Shirt,
- Upload,
- Wrench,
-} from "lucide-react";
-import { usePathname } from "next/navigation";
-import { useState } from "react";
-import Link from "@/components/link";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { cn } from "@/lib/utils";
-
-type StudioLink = { href: string; label: string; icon: LucideIcon };
-const assets: StudioLink[] = [
- { href: "/admin/studio/furni", label: "Furniture", icon: Puzzle },
- { href: "/admin/studio/badges", label: "Badges", icon: BadgeCheck },
- { href: "/admin/studio/clothing", label: "Clothing", icon: Shirt },
- { href: "/admin/studio/effects", label: "Effects", icon: Dumbbell },
- { href: "/admin/studio/pets", label: "Pets", icon: Cat },
-];
-const imports: StudioLink[] = [
- { href: "/admin/studio/upload", label: "Upload files", icon: Upload },
- { href: "/admin/studio/clone", label: "Import from a source", icon: Copy },
- { href: "/admin/studio/sync", label: "Sync & repository", icon: RefreshCw },
-];
-const maintenance: StudioLink[] = [
- { href: "/admin/studio/audit", label: "Catalog audit", icon: ClipboardCheck },
- {
- href: "/admin/studio/repair-icons",
- label: "Repair icons",
- icon: ImageIcon,
- },
- {
- href: "/admin/studio/maintenance",
- label: "Catalog maintenance",
- icon: Wrench,
- },
-];
-const activePath = (path: string, href: string) =>
- path === href || path.startsWith(`${href}/`);
-
-function StudioMenu({
- label,
- links,
- pathname,
-}: {
- label: string;
- links: StudioLink[];
- pathname: string;
-}) {
- const [open, setOpen] = useState(false);
- const active = links.find((link) => activePath(pathname, link.href));
- return (
-
-
- {label}
-
-
-
- {links.map(({ href, label: title, icon: Icon }) => (
- setOpen(false)}
- aria-current={activePath(pathname, href) ? "page" : undefined}
- className="flex min-h-11 items-center gap-3 rounded-md px-3 text-sm hover:bg-[var(--admin-accent)]/10 aria-[current=page]:bg-[var(--admin-accent)]/15"
- >
-
- {title}
-
- ))}
-
-
- );
-}
-
-export default function StudioLayout({
+export default async function StudioLayout({
children,
}: {
children: React.ReactNode;
}) {
- const pathname = usePathname() ?? "";
+ const { session, permissions } = await getAdminContext();
+ const canEditCatalog = canAccess(
+ permissions,
+ PERMS.CATALOG_EDIT,
+ session.user.rank,
+ );
+
return (
-
-
-
{children}
-
+
+ {children}
+
);
}
diff --git a/src/app/admin/studio/studio-layout-client.tsx b/src/app/admin/studio/studio-layout-client.tsx
new file mode 100644
index 00000000..9a65d237
--- /dev/null
+++ b/src/app/admin/studio/studio-layout-client.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import {
+ BadgeCheck,
+ Cat,
+ ChevronDown,
+ ClipboardCheck,
+ Copy,
+ Dumbbell,
+ Image as ImageIcon,
+ type LucideIcon,
+ Puzzle,
+ RefreshCw,
+ Shirt,
+ Upload,
+ Wrench,
+} from "lucide-react";
+import { usePathname } from "next/navigation";
+import { useState } from "react";
+import { CatalogManagerDialog } from "@/app/admin/catalog/catalog-manager-dialog-wrapper";
+import Link from "@/components/link";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+
+type StudioLink = { href: string; label: string; icon: LucideIcon };
+const assets: StudioLink[] = [
+ { href: "/admin/studio/furni", label: "Furniture", icon: Puzzle },
+ { href: "/admin/studio/badges", label: "Badges", icon: BadgeCheck },
+ { href: "/admin/studio/clothing", label: "Clothing", icon: Shirt },
+ { href: "/admin/studio/effects", label: "Effects", icon: Dumbbell },
+ { href: "/admin/studio/pets", label: "Pets", icon: Cat },
+];
+const imports: StudioLink[] = [
+ { href: "/admin/studio/upload", label: "Upload files", icon: Upload },
+ { href: "/admin/studio/clone", label: "Import from a source", icon: Copy },
+ { href: "/admin/studio/sync", label: "Sync & repository", icon: RefreshCw },
+];
+const maintenance: StudioLink[] = [
+ { href: "/admin/studio/audit", label: "Catalog audit", icon: ClipboardCheck },
+ {
+ href: "/admin/studio/repair-icons",
+ label: "Repair icons",
+ icon: ImageIcon,
+ },
+ {
+ href: "/admin/studio/maintenance",
+ label: "Catalog maintenance",
+ icon: Wrench,
+ },
+];
+const activePath = (path: string, href: string) =>
+ path === href || path.startsWith(`${href}/`);
+
+function StudioMenu({
+ label,
+ links,
+ pathname,
+}: {
+ label: string;
+ links: StudioLink[];
+ pathname: string;
+}) {
+ const [open, setOpen] = useState(false);
+ const active = links.find((link) => activePath(pathname, link.href));
+ return (
+
+
+ {label}
+
+
+
+ {links.map(({ href, label: title, icon: Icon }) => (
+ setOpen(false)}
+ aria-current={activePath(pathname, href) ? "page" : undefined}
+ className="flex min-h-11 items-center gap-3 rounded-md px-3 text-sm hover:bg-[var(--admin-accent)]/10 aria-[current=page]:bg-[var(--admin-accent)]/15"
+ >
+
+ {title}
+
+ ))}
+
+
+ );
+}
+
+export function StudioLayoutClient({
+ canEditCatalog,
+ children,
+}: {
+ canEditCatalog: boolean;
+ children: React.ReactNode;
+}) {
+ const pathname = usePathname() ?? "";
+ return (
+
+
+
{children}
+
+ );
+}
diff --git a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
index 5b2d3e7f..6ba82e13 100644
--- a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
+++ b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
@@ -74,11 +74,13 @@ export function CatalogManagerDialog({
catalogType = "normal",
canEdit = false,
totals = null,
+ triggerLabel,
}: {
initialRoots?: RootTab[];
catalogType?: CatalogManagerMode;
canEdit?: boolean;
totals?: CatalogStatsTotals | null;
+ triggerLabel?: string;
}) {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
@@ -115,7 +117,7 @@ export function CatalogManagerDialog({
onClick={() => setOpen(true)}
>
- {title}
+ {triggerLabel ?? title}
{mounted && open
? createPortal(