@igstack/app-catalog-frontend-core 0.18.6 → 0.18.7-alpha-20260911031421
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/dist/esm/__tests__/ui/components/header/Header.test.d.ts +0 -0
- package/dist/esm/ui/components/header/Header.d.ts +9 -0
- package/dist/esm/ui/components/header/Header.js +63 -24
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/ui/popover.d.ts +2 -1
- package/dist/esm/ui/popover.js +6 -0
- package/dist/esm/ui/popover.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/ui/components/header/Header.test.tsx +59 -0
- package/src/ui/components/header/Header.tsx +90 -28
- package/src/ui/popover.tsx +7 -1
|
File without changes
|
|
@@ -3,6 +3,15 @@ import { default as React } from 'react';
|
|
|
3
3
|
export declare function VersionDisplay({ versions }: {
|
|
4
4
|
versions: AppVersionInfo;
|
|
5
5
|
}): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
/**
|
|
7
|
+
* Version detail moved behind an on-demand popover (same click-to-reveal
|
|
8
|
+
* pattern as PersonBadge's approver chips) instead of always-visible text —
|
|
9
|
+
* keeps the header compact. Content is unchanged: same Pipeline/Core/FE
|
|
10
|
+
* lines, links, and SHA suffix as before, just revealed on click.
|
|
11
|
+
*/
|
|
12
|
+
export declare function VersionPopover({ versions }: {
|
|
13
|
+
versions: AppVersionInfo;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
6
15
|
export interface HeaderProps {
|
|
7
16
|
middle?: React.ReactNode;
|
|
8
17
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Link } from "@tanstack/react-router";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { LogOut } from "lucide-react";
|
|
4
|
+
import { LogOut, X } from "lucide-react";
|
|
5
5
|
import { use } from "react";
|
|
6
6
|
import SvgAppCatalog from "../../../assets/app-catalog.svg.js";
|
|
7
7
|
import { useTRPC } from "../../../api/infra/trpc.js";
|
|
@@ -12,6 +12,7 @@ import { useAuthActions } from "../../../modules/auth/useAuthActions.js";
|
|
|
12
12
|
import { useAuthModal } from "../../../modules/auth/AuthModalContext.js";
|
|
13
13
|
import { Button } from "../../button.js";
|
|
14
14
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuItem } from "../../dropdown-menu.js";
|
|
15
|
+
import { Popover, PopoverTrigger, PopoverContent, PopoverClose } from "../../popover.js";
|
|
15
16
|
import { Skeleton } from "../../skeleton.js";
|
|
16
17
|
function ShaSuffix({ version }) {
|
|
17
18
|
if (!version.sha) return null;
|
|
@@ -45,22 +46,22 @@ function VersionItem({
|
|
|
45
46
|
label,
|
|
46
47
|
version
|
|
47
48
|
}) {
|
|
48
|
-
return /* @__PURE__ */ jsxs("
|
|
49
|
-
label,
|
|
50
|
-
":",
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4 py-1", children: [
|
|
50
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: label }),
|
|
51
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-right", children: [
|
|
52
|
+
version.url ? /* @__PURE__ */ jsx(
|
|
53
|
+
"a",
|
|
54
|
+
{
|
|
55
|
+
href: version.url,
|
|
56
|
+
target: "_blank",
|
|
57
|
+
rel: "noopener noreferrer",
|
|
58
|
+
className: "text-primary hover:text-primary/80 transition-colors",
|
|
59
|
+
onClick: (e) => e.stopPropagation(),
|
|
60
|
+
children: version.displayName
|
|
61
|
+
}
|
|
62
|
+
) : version.displayName,
|
|
63
|
+
/* @__PURE__ */ jsx(ShaSuffix, { version })
|
|
64
|
+
] })
|
|
64
65
|
] });
|
|
65
66
|
}
|
|
66
67
|
function VersionDisplay({ versions }) {
|
|
@@ -69,12 +70,46 @@ function VersionDisplay({ versions }) {
|
|
|
69
70
|
if (isLocal) {
|
|
70
71
|
return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", title: "Local", children: "local" });
|
|
71
72
|
}
|
|
72
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col
|
|
73
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col divide-y", children: [
|
|
73
74
|
versions.backend && /* @__PURE__ */ jsx(VersionItem, { label: "Pipeline", version: versions.backend }),
|
|
74
75
|
versions.coreVersion && /* @__PURE__ */ jsx(VersionItem, { label: "Core", version: versions.coreVersion }),
|
|
75
76
|
versions.frontend && /* @__PURE__ */ jsx(VersionItem, { label: "FE", version: versions.frontend })
|
|
76
77
|
] });
|
|
77
78
|
}
|
|
79
|
+
function VersionPopover({ versions }) {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
const isLocal = ((_a = versions.backend) == null ? void 0 : _a.displayName) === "local" && !versions.coreVersion;
|
|
82
|
+
if (isLocal) {
|
|
83
|
+
return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", title: "Local", children: "local" });
|
|
84
|
+
}
|
|
85
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
86
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
87
|
+
"button",
|
|
88
|
+
{
|
|
89
|
+
type: "button",
|
|
90
|
+
className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-mono leading-none text-muted-foreground hover:bg-primary/15 hover:text-primary transition-colors",
|
|
91
|
+
"aria-label": "Show version details",
|
|
92
|
+
title: "Version details",
|
|
93
|
+
"data-testid": "version-info-trigger",
|
|
94
|
+
children: ((_b = versions.backend) == null ? void 0 : _b.displayName) === "local" ? "local" : `#${((_c = versions.backend) == null ? void 0 : _c.displayName) ?? "?"}`
|
|
95
|
+
}
|
|
96
|
+
) }),
|
|
97
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-72 p-0 overflow-hidden", children: [
|
|
98
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b bg-muted/40", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Version details" }),
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
PopoverClose,
|
|
102
|
+
{
|
|
103
|
+
className: "rounded p-1 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
|
|
104
|
+
"aria-label": "Close",
|
|
105
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1", children: /* @__PURE__ */ jsx(VersionDisplay, { versions }) })
|
|
110
|
+
] })
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
78
113
|
function Header({ middle }) {
|
|
79
114
|
var _a;
|
|
80
115
|
const { isLoading } = useAuth();
|
|
@@ -99,13 +134,16 @@ function Header({ middle }) {
|
|
|
99
134
|
openLoginModal(currentUrl);
|
|
100
135
|
};
|
|
101
136
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center mb-4 pb-4 gap-4 border-b", children: [
|
|
102
|
-
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */
|
|
103
|
-
/* @__PURE__ */ jsx(SvgAppCatalog, { className: "h-16 w-16" }),
|
|
137
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
138
|
+
/* @__PURE__ */ jsx(Link, { to: "/", children: /* @__PURE__ */ jsx(SvgAppCatalog, { className: "h-16 w-16" }) }),
|
|
104
139
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
105
|
-
/* @__PURE__ */ jsx("
|
|
106
|
-
|
|
140
|
+
/* @__PURE__ */ jsx(Link, { to: "/", className: "font-serif text-lg font-semibold", children: "App Catalog" }),
|
|
141
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
142
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/60", children: "by Igor Golovin" }),
|
|
143
|
+
(appCatalogContextMaybe == null ? void 0 : appCatalogContextMaybe.versions) && /* @__PURE__ */ jsx(VersionPopover, { versions: appCatalogContextMaybe.versions })
|
|
144
|
+
] })
|
|
107
145
|
] })
|
|
108
|
-
] }) })
|
|
146
|
+
] }) }),
|
|
109
147
|
middle && /* @__PURE__ */ jsx("div", { className: "flex-1", children: middle }),
|
|
110
148
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 ml-auto", children: [
|
|
111
149
|
/* @__PURE__ */ jsx(ThemeSwitcher, {}),
|
|
@@ -177,6 +215,7 @@ function Header({ middle }) {
|
|
|
177
215
|
}
|
|
178
216
|
export {
|
|
179
217
|
Header,
|
|
180
|
-
VersionDisplay
|
|
218
|
+
VersionDisplay,
|
|
219
|
+
VersionPopover
|
|
181
220
|
};
|
|
182
221
|
//# sourceMappingURL=Header.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n // Outer element is a <span> (not <a>) so the optional SHA link can be a\n // sibling anchor without producing invalid nested <a> markup.\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}:{' '}\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"font-serif text-lg font-semibold\">\n App Catalog\n </span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut, X } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport {\n Popover,\n PopoverClose,\n PopoverContent,\n PopoverTrigger,\n} from '~/ui/popover'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n return (\n <div className=\"flex items-center justify-between gap-4 py-1\">\n <span className=\"text-xs text-muted-foreground\">{label}</span>\n <span className=\"text-sm font-medium text-right\">\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n </div>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col divide-y\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\n/**\n * Version detail moved behind an on-demand popover (same click-to-reveal\n * pattern as PersonBadge's approver chips) instead of always-visible text —\n * keeps the header compact. Content is unchanged: same Pipeline/Core/FE\n * lines, links, and SHA suffix as before, just revealed on click.\n */\nexport function VersionPopover({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-mono leading-none text-muted-foreground hover:bg-primary/15 hover:text-primary transition-colors\"\n aria-label=\"Show version details\"\n title=\"Version details\"\n data-testid=\"version-info-trigger\"\n >\n {versions.backend?.displayName === 'local'\n ? 'local'\n : `#${versions.backend?.displayName ?? '?'}`}\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-0 overflow-hidden\">\n <div className=\"flex items-center justify-between px-3 py-2 border-b bg-muted/40\">\n <span className=\"text-sm font-semibold\">Version details</span>\n <PopoverClose\n className=\"rounded p-1 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\n aria-label=\"Close\"\n >\n <X className=\"size-3.5\" />\n </PopoverClose>\n </div>\n <div className=\"px-3 py-1\">\n <VersionDisplay versions={versions} />\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <Link to=\"/\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n </Link>\n <div className=\"flex flex-col\">\n <Link to=\"/\" className=\"font-serif text-lg font-semibold\">\n App Catalog\n </Link>\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-[10px] text-muted-foreground/60\">\n by Igor Golovin\n </span>\n {appCatalogContextMaybe?.versions && (\n <VersionPopover versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </div>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
|
package/dist/esm/ui/popover.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ declare function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimit
|
|
|
4
4
|
declare function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
declare function PopoverContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
declare function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>): import("react/jsx-runtime").JSX.Element;
|
|
7
|
-
|
|
7
|
+
declare function PopoverClose({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Close>): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose };
|
package/dist/esm/ui/popover.js
CHANGED
|
@@ -32,8 +32,14 @@ function PopoverContent({
|
|
|
32
32
|
}
|
|
33
33
|
) });
|
|
34
34
|
}
|
|
35
|
+
function PopoverClose({
|
|
36
|
+
...props
|
|
37
|
+
}) {
|
|
38
|
+
return /* @__PURE__ */ jsx(PopoverPrimitive.Close, { "data-slot": "popover-close", ...props });
|
|
39
|
+
}
|
|
35
40
|
export {
|
|
36
41
|
Popover,
|
|
42
|
+
PopoverClose,
|
|
37
43
|
PopoverContent,
|
|
38
44
|
PopoverTrigger
|
|
39
45
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover.js","sources":["../../../src/ui/popover.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } from '~/lib/utils'\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = 'center',\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',\n className,\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n )\n}\n\nfunction PopoverAnchor({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"],"names":[],"mappings":";;;;AAOA;AAAiB;AAEjB;AACE;AACF;AAEA;AAAwB;AAExB;AACE;AACF;AAEA;AAAwB;AACtB;AACQ;AACK;AAEf;AACE;AAEI;AAAkB;AAAjB;AACW;AACV;AACA;AACW;AACT;AACA;AAAA;AAEE;AAAA;AAIZ
|
|
1
|
+
{"version":3,"file":"popover.js","sources":["../../../src/ui/popover.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } from '~/lib/utils'\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = 'center',\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',\n className,\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n )\n}\n\nfunction PopoverAnchor({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nfunction PopoverClose({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Close>) {\n return <PopoverPrimitive.Close data-slot=\"popover-close\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose }\n"],"names":[],"mappings":";;;;AAOA;AAAiB;AAEjB;AACE;AACF;AAEA;AAAwB;AAExB;AACE;AACF;AAEA;AAAwB;AACtB;AACQ;AACK;AAEf;AACE;AAEI;AAAkB;AAAjB;AACW;AACV;AACA;AACW;AACT;AACA;AAAA;AAEE;AAAA;AAIZ;AAQA;AAAsB;AAEtB;AACE;AACF;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7-alpha-20260911031421",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -132,8 +132,8 @@
|
|
|
132
132
|
"vite-plugin-static-copy": "^3.1.4",
|
|
133
133
|
"vite-plugin-svgr": "^4.2.0",
|
|
134
134
|
"vitest": "^4.1.2",
|
|
135
|
-
"@igstack/app-catalog-backend-core": "0.18.
|
|
136
|
-
"@igstack/app-catalog-shared-core": "0.18.
|
|
135
|
+
"@igstack/app-catalog-backend-core": "0.18.7-alpha-20260911031421",
|
|
136
|
+
"@igstack/app-catalog-shared-core": "0.18.7-alpha-20260911031421"
|
|
137
137
|
},
|
|
138
138
|
"peerDependencies": {
|
|
139
139
|
"react": "19.1.2",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"vite": "^6.3.5",
|
|
143
143
|
"vite-plugin-svgr": "^4.2.0"
|
|
144
144
|
},
|
|
145
|
-
"gitHead": "
|
|
145
|
+
"gitHead": "485ab901f46a7448acb1784d706661aecfb4db1b",
|
|
146
146
|
"scripts": {
|
|
147
147
|
"build": "vite build",
|
|
148
148
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import '@testing-library/jest-dom/vitest'
|
|
4
|
+
|
|
5
|
+
import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
|
|
6
|
+
import { VersionPopover } from '~/ui/components/header/Header'
|
|
7
|
+
|
|
8
|
+
const versions: AppVersionInfo = {
|
|
9
|
+
backend: {
|
|
10
|
+
displayName: '#123',
|
|
11
|
+
url: 'https://example.test/pipeline/123',
|
|
12
|
+
sha: 'abc1234',
|
|
13
|
+
shaUrl: 'https://example.test/commit/abc1234',
|
|
14
|
+
},
|
|
15
|
+
coreVersion: { displayName: '0.18.6' },
|
|
16
|
+
frontend: { displayName: 'fe-build-42' },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('VersionPopover', () => {
|
|
20
|
+
it('hides the version detail until the trigger is clicked', () => {
|
|
21
|
+
render(<VersionPopover versions={versions} />)
|
|
22
|
+
|
|
23
|
+
expect(screen.queryByText(/0\.18\.6/)).not.toBeInTheDocument()
|
|
24
|
+
|
|
25
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
26
|
+
|
|
27
|
+
expect(screen.getByText('#123')).toBeInTheDocument()
|
|
28
|
+
expect(screen.getByText('Core')).toBeInTheDocument()
|
|
29
|
+
expect(screen.getByText('0.18.6')).toBeInTheDocument()
|
|
30
|
+
expect(screen.getByText('FE')).toBeInTheDocument()
|
|
31
|
+
expect(screen.getByText('fe-build-42')).toBeInTheDocument()
|
|
32
|
+
expect(screen.getByText('(abc1234)')).toBeInTheDocument()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('renders the pipeline link with the existing href', () => {
|
|
36
|
+
render(<VersionPopover versions={versions} />)
|
|
37
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
38
|
+
|
|
39
|
+
expect(screen.getByRole('link', { name: '#123' })).toHaveAttribute(
|
|
40
|
+
'href',
|
|
41
|
+
'https://example.test/pipeline/123',
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('closes when the close button is clicked', () => {
|
|
46
|
+
render(<VersionPopover versions={versions} />)
|
|
47
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
48
|
+
expect(screen.getByText('0.18.6')).toBeInTheDocument()
|
|
49
|
+
|
|
50
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
51
|
+
expect(screen.queryByText('0.18.6')).not.toBeInTheDocument()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders "local" without a popover when running local-only', () => {
|
|
55
|
+
render(<VersionPopover versions={{ backend: { displayName: 'local' } }} />)
|
|
56
|
+
expect(screen.getByText('local')).toBeInTheDocument()
|
|
57
|
+
expect(screen.queryByTestId('version-info-trigger')).not.toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from '@igstack/app-catalog-backend-core'
|
|
5
5
|
import { Link } from '@tanstack/react-router'
|
|
6
6
|
import { useQuery } from '@tanstack/react-query'
|
|
7
|
-
import { LogOut } from 'lucide-react'
|
|
7
|
+
import { LogOut, X } from 'lucide-react'
|
|
8
8
|
import React, { use } from 'react'
|
|
9
9
|
import AppCatalogLogo from '~/assets/app-catalog.svg?react'
|
|
10
10
|
import { useTRPC } from '~/api/infra/trpc'
|
|
@@ -25,6 +25,12 @@ import {
|
|
|
25
25
|
DropdownMenuSeparator,
|
|
26
26
|
DropdownMenuTrigger,
|
|
27
27
|
} from '~/ui/dropdown-menu'
|
|
28
|
+
import {
|
|
29
|
+
Popover,
|
|
30
|
+
PopoverClose,
|
|
31
|
+
PopoverContent,
|
|
32
|
+
PopoverTrigger,
|
|
33
|
+
} from '~/ui/popover'
|
|
28
34
|
import { Skeleton } from '~/ui/skeleton'
|
|
29
35
|
|
|
30
36
|
function ShaSuffix({ version }: { version: VersionInfo }) {
|
|
@@ -55,26 +61,26 @@ function VersionItem({
|
|
|
55
61
|
label: string
|
|
56
62
|
version: VersionInfo
|
|
57
63
|
}) {
|
|
58
|
-
// Outer element is a <span> (not <a>) so the optional SHA link can be a
|
|
59
|
-
// sibling anchor without producing invalid nested <a> markup.
|
|
60
64
|
return (
|
|
61
|
-
<
|
|
62
|
-
{label}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
<div className="flex items-center justify-between gap-4 py-1">
|
|
66
|
+
<span className="text-xs text-muted-foreground">{label}</span>
|
|
67
|
+
<span className="text-sm font-medium text-right">
|
|
68
|
+
{version.url ? (
|
|
69
|
+
<a
|
|
70
|
+
href={version.url}
|
|
71
|
+
target="_blank"
|
|
72
|
+
rel="noopener noreferrer"
|
|
73
|
+
className="text-primary hover:text-primary/80 transition-colors"
|
|
74
|
+
onClick={(e) => e.stopPropagation()}
|
|
75
|
+
>
|
|
76
|
+
{version.displayName}
|
|
77
|
+
</a>
|
|
78
|
+
) : (
|
|
79
|
+
version.displayName
|
|
80
|
+
)}
|
|
81
|
+
<ShaSuffix version={version} />
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
78
84
|
)
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -91,7 +97,7 @@ export function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
return (
|
|
94
|
-
<div className="flex flex-col
|
|
100
|
+
<div className="flex flex-col divide-y">
|
|
95
101
|
{versions.backend && (
|
|
96
102
|
<VersionItem label="Pipeline" version={versions.backend} />
|
|
97
103
|
)}
|
|
@@ -105,6 +111,57 @@ export function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
|
105
111
|
)
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Version detail moved behind an on-demand popover (same click-to-reveal
|
|
116
|
+
* pattern as PersonBadge's approver chips) instead of always-visible text —
|
|
117
|
+
* keeps the header compact. Content is unchanged: same Pipeline/Core/FE
|
|
118
|
+
* lines, links, and SHA suffix as before, just revealed on click.
|
|
119
|
+
*/
|
|
120
|
+
export function VersionPopover({ versions }: { versions: AppVersionInfo }) {
|
|
121
|
+
const isLocal =
|
|
122
|
+
versions.backend?.displayName === 'local' && !versions.coreVersion
|
|
123
|
+
|
|
124
|
+
if (isLocal) {
|
|
125
|
+
return (
|
|
126
|
+
<span className="text-xs text-muted-foreground" title="Local">
|
|
127
|
+
local
|
|
128
|
+
</span>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Popover>
|
|
134
|
+
<PopoverTrigger asChild>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-mono leading-none text-muted-foreground hover:bg-primary/15 hover:text-primary transition-colors"
|
|
138
|
+
aria-label="Show version details"
|
|
139
|
+
title="Version details"
|
|
140
|
+
data-testid="version-info-trigger"
|
|
141
|
+
>
|
|
142
|
+
{versions.backend?.displayName === 'local'
|
|
143
|
+
? 'local'
|
|
144
|
+
: `#${versions.backend?.displayName ?? '?'}`}
|
|
145
|
+
</button>
|
|
146
|
+
</PopoverTrigger>
|
|
147
|
+
<PopoverContent align="start" className="w-72 p-0 overflow-hidden">
|
|
148
|
+
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/40">
|
|
149
|
+
<span className="text-sm font-semibold">Version details</span>
|
|
150
|
+
<PopoverClose
|
|
151
|
+
className="rounded p-1 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
152
|
+
aria-label="Close"
|
|
153
|
+
>
|
|
154
|
+
<X className="size-3.5" />
|
|
155
|
+
</PopoverClose>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="px-3 py-1">
|
|
158
|
+
<VersionDisplay versions={versions} />
|
|
159
|
+
</div>
|
|
160
|
+
</PopoverContent>
|
|
161
|
+
</Popover>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
108
165
|
export interface HeaderProps {
|
|
109
166
|
middle?: React.ReactNode
|
|
110
167
|
}
|
|
@@ -138,19 +195,24 @@ export function Header({ middle }: HeaderProps) {
|
|
|
138
195
|
return (
|
|
139
196
|
<div className="flex items-center mb-4 pb-4 gap-4 border-b">
|
|
140
197
|
<div className="flex items-center gap-4">
|
|
141
|
-
<
|
|
142
|
-
<
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<Link to="/">
|
|
143
200
|
<AppCatalogLogo className="h-16 w-16" />
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
</Link>
|
|
202
|
+
<div className="flex flex-col">
|
|
203
|
+
<Link to="/" className="font-serif text-lg font-semibold">
|
|
204
|
+
App Catalog
|
|
205
|
+
</Link>
|
|
206
|
+
<div className="flex items-center gap-1.5">
|
|
207
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
208
|
+
by Igor Golovin
|
|
147
209
|
</span>
|
|
148
210
|
{appCatalogContextMaybe?.versions && (
|
|
149
|
-
<
|
|
211
|
+
<VersionPopover versions={appCatalogContextMaybe.versions} />
|
|
150
212
|
)}
|
|
151
213
|
</div>
|
|
152
214
|
</div>
|
|
153
|
-
</
|
|
215
|
+
</div>
|
|
154
216
|
</div>
|
|
155
217
|
{middle && <div className="flex-1">{middle}</div>}
|
|
156
218
|
<div className="flex items-center gap-3 ml-auto">
|
package/src/ui/popover.tsx
CHANGED
|
@@ -45,4 +45,10 @@ function PopoverAnchor({
|
|
|
45
45
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function PopoverClose({
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Close>) {
|
|
51
|
+
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose }
|