@cosmicdrift/kumiko-bundled-features 0.67.1 → 0.68.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.67.1",
3
+ "version": "0.68.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -84,11 +84,11 @@
84
84
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
85
85
  },
86
86
  "dependencies": {
87
- "@cosmicdrift/kumiko-dispatcher-live": "0.67.1",
88
- "@cosmicdrift/kumiko-framework": "0.67.1",
89
- "@cosmicdrift/kumiko-headless": "0.67.1",
90
- "@cosmicdrift/kumiko-renderer": "0.67.1",
91
- "@cosmicdrift/kumiko-renderer-web": "0.67.1",
87
+ "@cosmicdrift/kumiko-dispatcher-live": "0.68.0",
88
+ "@cosmicdrift/kumiko-framework": "0.68.0",
89
+ "@cosmicdrift/kumiko-headless": "0.68.0",
90
+ "@cosmicdrift/kumiko-renderer": "0.68.0",
91
+ "@cosmicdrift/kumiko-renderer-web": "0.68.0",
92
92
  "@mollie/api-client": "^4.5.0",
93
93
  "@node-rs/argon2": "^2.0.2",
94
94
  "@types/nodemailer": "^8.0.0",
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { SidebarProvider } from "@cosmicdrift/kumiko-renderer-web";
2
3
  import { screen } from "@testing-library/react";
3
4
  import userEvent from "@testing-library/user-event";
4
5
  import { UserMenu } from "../user-menu";
@@ -46,4 +47,21 @@ describe("UserMenu", () => {
46
47
  await user.click(screen.getByText("Abmelden"));
47
48
  expect(session.logout).toHaveBeenCalledTimes(1);
48
49
  });
50
+ test("sidebar variant: NavUser-Row zeigt Name + Email, Dropdown trägt Logout", async () => {
51
+ const user = userEvent.setup();
52
+ const session = makeSessionApi({
53
+ user: { id: "u1", email: "alice@example.com", displayName: "Alice Wonder", globalRoles: [] },
54
+ });
55
+ renderWithProviders(
56
+ <SidebarProvider>
57
+ <UserMenu variant="sidebar" />
58
+ </SidebarProvider>,
59
+ { session },
60
+ );
61
+ // Anders als die Pill: die Row zeigt Name UND Email direkt.
62
+ expect(screen.getByText("Alice Wonder")).toBeTruthy();
63
+ expect(screen.getByText("alice@example.com")).toBeTruthy();
64
+ await user.click(screen.getByRole("button", { name: /Alice Wonder/ }));
65
+ expect(screen.getByText("Abmelden")).toBeTruthy();
66
+ });
49
67
  });
@@ -1,8 +1,9 @@
1
1
  // @runtime client
2
- // UserMenu — Avatar-Dropdown in der Topbar/Sidebar. Zeigt Name/Email
3
- // des aktuellen Users + Logout-Button. Auf Radix-DropdownMenu, damit
4
- // Click-outside, Escape, Focus-Management, Keyboard-Nav (↑↓/Home/End)
5
- // und ARIA-Roles aus der Kiste funktionieren.
2
+ // UserMenu — Avatar-Dropdown für die Topbar (variant="pill", Default) oder den
3
+ // Sidebar-Footer (variant="sidebar" = die sidebar-07-NavUser-Row). Zeigt
4
+ // Name/Email + Logout. Auf Radix-DropdownMenu, damit Click-outside, Escape,
5
+ // Focus-Management, Keyboard-Nav (↑↓/Home/End) und ARIA-Roles aus der Kiste
6
+ // funktionieren.
6
7
  //
7
8
  // Rendert NICHTS wenn kein User eingeloggt ist — Hosts dürfen das
8
9
  // Component außerhalb des AuthGate einhängen ohne dass ein harter
@@ -17,15 +18,23 @@ import {
17
18
  DropdownMenuLabel,
18
19
  DropdownMenuSeparator,
19
20
  DropdownMenuTrigger,
21
+ SidebarMenu,
22
+ SidebarMenuButton,
23
+ SidebarMenuItem,
20
24
  } from "@cosmicdrift/kumiko-renderer-web";
21
- import { ChevronDown, LogOut } from "lucide-react";
25
+ import { ChevronDown, ChevronsUpDown, LogOut } from "lucide-react";
22
26
  import type { ReactNode } from "react";
23
27
  import { useSession } from "./session";
24
28
 
29
+ export type UserMenuVariant = "pill" | "sidebar";
30
+
25
31
  export type UserMenuProps = {
26
32
  /** Zusätzliche Menu-Items über dem Logout. Per-item class/behaviour
27
33
  * controlliert der Caller — wir packen nur den Frame drumrum. */
28
34
  readonly children?: ReactNode;
35
+ /** "pill" (Default) = kompakter Topbar-Trigger; "sidebar" = volle NavUser-
36
+ * Row (Avatar + Name + Email) für den `sidebarFooter`-Slot der App-Shell. */
37
+ readonly variant?: UserMenuVariant;
29
38
  };
30
39
 
31
40
  function initials(value: string): string {
@@ -41,15 +50,63 @@ function initials(value: string): string {
41
50
  return trimmed.slice(0, 2).toUpperCase();
42
51
  }
43
52
 
44
- export function UserMenu({ children }: UserMenuProps): ReactNode {
53
+ export function UserMenu({ children, variant = "pill" }: UserMenuProps): ReactNode {
45
54
  const t = useTranslation();
46
55
  const { user, logout } = useSession();
47
56
 
48
57
  if (user === null) return null;
49
58
 
50
- const displayName = user.displayName.length > 0 ? user.displayName : user.email;
59
+ const hasName = user.displayName.length > 0;
60
+ const displayName = hasName ? user.displayName : user.email;
51
61
  const avatarText = initials(displayName);
52
62
 
63
+ const content = (
64
+ <DropdownMenuContent align="end" aria-label={t("auth.user.menu.label")}>
65
+ <DropdownMenuLabel className="text-xs">
66
+ <div className="font-medium text-foreground truncate">{displayName}</div>
67
+ <div className="truncate">{user.email}</div>
68
+ </DropdownMenuLabel>
69
+ <DropdownMenuSeparator />
70
+ {children}
71
+ <DropdownMenuItem onSelect={() => void logout()}>
72
+ <LogOut className="h-4 w-4" />
73
+ <span>{t("auth.user.menu.logout")}</span>
74
+ </DropdownMenuItem>
75
+ </DropdownMenuContent>
76
+ );
77
+
78
+ // Sidebar-Footer: volle NavUser-Row (sidebar-07) als Dropdown-Trigger —
79
+ // gleiche Optik wie SidebarUser, aber klickbar mit Logout/Profil.
80
+ if (variant === "sidebar") {
81
+ return (
82
+ <SidebarMenu>
83
+ <SidebarMenuItem>
84
+ <DropdownMenu>
85
+ <DropdownMenuTrigger asChild>
86
+ <SidebarMenuButton
87
+ size="lg"
88
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
89
+ >
90
+ <span
91
+ aria-hidden="true"
92
+ className="flex aspect-square size-8 items-center justify-center rounded-lg bg-muted text-xs font-medium text-muted-foreground"
93
+ >
94
+ {avatarText}
95
+ </span>
96
+ <div className="grid flex-1 text-left text-sm leading-tight">
97
+ <span className="truncate font-semibold">{displayName}</span>
98
+ {hasName && <span className="truncate text-xs">{user.email}</span>}
99
+ </div>
100
+ <ChevronsUpDown className="ml-auto size-4" />
101
+ </SidebarMenuButton>
102
+ </DropdownMenuTrigger>
103
+ {content}
104
+ </DropdownMenu>
105
+ </SidebarMenuItem>
106
+ </SidebarMenu>
107
+ );
108
+ }
109
+
53
110
  return (
54
111
  <DropdownMenu>
55
112
  <DropdownMenuTrigger asChild>
@@ -72,18 +129,7 @@ export function UserMenu({ children }: UserMenuProps): ReactNode {
72
129
  <ChevronDown className="h-3 w-3 text-muted-foreground" />
73
130
  </button>
74
131
  </DropdownMenuTrigger>
75
- <DropdownMenuContent align="end" aria-label={t("auth.user.menu.label")}>
76
- <DropdownMenuLabel className="text-xs">
77
- <div className="font-medium text-foreground truncate">{displayName}</div>
78
- <div className="truncate">{user.email}</div>
79
- </DropdownMenuLabel>
80
- <DropdownMenuSeparator />
81
- {children}
82
- <DropdownMenuItem onSelect={() => void logout()}>
83
- <LogOut className="h-4 w-4" />
84
- <span>{t("auth.user.menu.logout")}</span>
85
- </DropdownMenuItem>
86
- </DropdownMenuContent>
132
+ {content}
87
133
  </DropdownMenu>
88
134
  );
89
135
  }