@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.
@@ -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("span", { className: "text-xs text-muted-foreground", title: label, children: [
49
- label,
50
- ":",
51
- " ",
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 font-medium",
59
- onClick: (e) => e.stopPropagation(),
60
- children: version.displayName
61
- }
62
- ) : version.displayName,
63
- /* @__PURE__ */ jsx(ShaSuffix, { version })
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 gap-0.5", children: [
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__ */ jsx(Link, { to: "/", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
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("span", { className: "font-serif text-lg font-semibold", children: "App Catalog" }),
106
- (appCatalogContextMaybe == null ? void 0 : appCatalogContextMaybe.versions) && /* @__PURE__ */ jsx(VersionDisplay, { versions: appCatalogContextMaybe.versions })
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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"}
@@ -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
- export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
7
+ declare function PopoverClose({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Close>): import("react/jsx-runtime").JSX.Element;
8
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose };
@@ -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.6",
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.6",
136
- "@igstack/app-catalog-shared-core": "0.18.6"
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": "cd85a15e6266f4a2e88f574b6a14ae6427559eab",
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
- <span className="text-xs text-muted-foreground" title={label}>
62
- {label}:{' '}
63
- {version.url ? (
64
- <a
65
- href={version.url}
66
- target="_blank"
67
- rel="noopener noreferrer"
68
- className="text-primary hover:text-primary/80 transition-colors font-medium"
69
- onClick={(e) => e.stopPropagation()}
70
- >
71
- {version.displayName}
72
- </a>
73
- ) : (
74
- version.displayName
75
- )}
76
- <ShaSuffix version={version} />
77
- </span>
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 gap-0.5">
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
- <Link to="/">
142
- <div className="flex items-center gap-2">
198
+ <div className="flex items-center gap-2">
199
+ <Link to="/">
143
200
  <AppCatalogLogo className="h-16 w-16" />
144
- <div className="flex flex-col">
145
- <span className="font-serif text-lg font-semibold">
146
- App Catalog
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
- <VersionDisplay versions={appCatalogContextMaybe.versions} />
211
+ <VersionPopover versions={appCatalogContextMaybe.versions} />
150
212
  )}
151
213
  </div>
152
214
  </div>
153
- </Link>
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">
@@ -45,4 +45,10 @@ function PopoverAnchor({
45
45
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46
46
  }
47
47
 
48
- export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
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 }