@igstack/app-catalog-frontend-core 6.1.6 → 6.2.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/dist/esm/modules/appCatalog/ui/catalog/AttributionFooter.d.ts +7 -2
- package/dist/esm/modules/appCatalog/ui/catalog/AttributionFooter.js +62 -22
- package/dist/esm/modules/appCatalog/ui/catalog/AttributionFooter.js.map +1 -1
- package/dist/esm/types/uiSettings.d.ts +9 -3
- package/dist/esm/ui/components/header/Header.js +4 -5
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/ui/components/header/Header.test.tsx +48 -0
- package/src/modules/appCatalog/ui/catalog/AttributionFooter.tsx +71 -9
- package/src/types/uiSettings.ts +9 -3
- package/src/ui/components/header/Header.tsx +5 -8
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Subtle attribution line
|
|
2
|
+
* Subtle attribution line (author, support channel, repo links). Content is
|
|
3
3
|
* supplied by the consuming app via `UiSettings.attribution` — the OSS core
|
|
4
4
|
* hard-codes nothing company-specific. Renders nothing when unset.
|
|
5
5
|
*
|
|
6
6
|
* `kind: 'oss'` links get a GitHub mark; everything else gets a generic
|
|
7
7
|
* external-link icon (proprietary repos, etc.).
|
|
8
|
+
*
|
|
9
|
+
* `variant`: `page` is the home-view footer, `popover` the compact block at the
|
|
10
|
+
* bottom of the header's version popover.
|
|
8
11
|
*/
|
|
9
|
-
export declare function AttributionFooter(
|
|
12
|
+
export declare function AttributionFooter({ variant, }: {
|
|
13
|
+
variant?: 'page' | 'popover';
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,29 +1,69 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Github, ExternalLink } from "lucide-react";
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Github, ExternalLink, Slack } from "lucide-react";
|
|
3
3
|
import { useUiSettings } from "../../../../context/UiSettingsContext.js";
|
|
4
|
-
function
|
|
5
|
-
|
|
4
|
+
function SupportChannel({ name }) {
|
|
5
|
+
const { chatChannelUrlTemplate } = useUiSettings();
|
|
6
|
+
const label = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7
|
+
/* @__PURE__ */ jsx(Slack, { className: "size-3.5" }),
|
|
8
|
+
`#${name}`
|
|
9
|
+
] });
|
|
10
|
+
if (!chatChannelUrlTemplate) {
|
|
11
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-1", children: label });
|
|
12
|
+
}
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
"a",
|
|
15
|
+
{
|
|
16
|
+
href: chatChannelUrlTemplate.replace("{name}", name),
|
|
17
|
+
target: "_blank",
|
|
18
|
+
rel: "noopener noreferrer",
|
|
19
|
+
className: "inline-flex items-center gap-1 hover:text-primary transition-colors",
|
|
20
|
+
children: label
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function AttributionFooter({
|
|
25
|
+
variant = "page"
|
|
26
|
+
}) {
|
|
27
|
+
var _a, _b, _c;
|
|
6
28
|
const { attribution } = useUiSettings();
|
|
7
|
-
if (!attribution || !attribution.madeBy && !((_a = attribution.links) == null ? void 0 : _a.length)) {
|
|
29
|
+
if (!attribution || !attribution.madeBy && !attribution.supportChannel && !((_a = attribution.links) == null ? void 0 : _a.length)) {
|
|
8
30
|
return null;
|
|
9
31
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const isPopover = variant === "popover";
|
|
33
|
+
const Wrapper = isPopover ? "div" : "footer";
|
|
34
|
+
const hasRow = attribution.supportChannel || ((_b = attribution.links) == null ? void 0 : _b.length);
|
|
35
|
+
return /* @__PURE__ */ jsxs(
|
|
36
|
+
Wrapper,
|
|
37
|
+
{
|
|
38
|
+
className: isPopover ? "border-t bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground" : "mt-16 border-t border-border/60 pt-5 text-center text-[12.5px] text-muted-foreground",
|
|
39
|
+
children: [
|
|
40
|
+
attribution.madeBy && /* @__PURE__ */ jsx("span", { children: attribution.madeBy }),
|
|
41
|
+
hasRow && /* @__PURE__ */ jsxs(
|
|
42
|
+
"div",
|
|
43
|
+
{
|
|
44
|
+
className: isPopover ? "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1" : "mt-1.5 flex flex-wrap items-center justify-center gap-x-4 gap-y-1",
|
|
45
|
+
children: [
|
|
46
|
+
attribution.supportChannel && /* @__PURE__ */ jsx(SupportChannel, { name: attribution.supportChannel }),
|
|
47
|
+
(_c = attribution.links) == null ? void 0 : _c.map((link) => /* @__PURE__ */ jsxs(
|
|
48
|
+
"a",
|
|
49
|
+
{
|
|
50
|
+
href: link.url,
|
|
51
|
+
target: "_blank",
|
|
52
|
+
rel: "noopener noreferrer",
|
|
53
|
+
className: "inline-flex items-center gap-1 hover:text-primary transition-colors",
|
|
54
|
+
children: [
|
|
55
|
+
link.kind === "oss" ? /* @__PURE__ */ jsx(Github, { className: "size-3.5" }) : /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5" }),
|
|
56
|
+
link.label
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
link.url
|
|
60
|
+
))
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
);
|
|
27
67
|
}
|
|
28
68
|
export {
|
|
29
69
|
AttributionFooter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttributionFooter.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AttributionFooter.tsx"],"sourcesContent":["import { ExternalLink, Github } from 'lucide-react'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"AttributionFooter.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AttributionFooter.tsx"],"sourcesContent":["import { ExternalLink, Github, Slack } from 'lucide-react'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\n/**\n * Support channel as an icon + `#name`, linked through\n * `UiSettings.chatChannelUrlTemplate` (the same template that turns `#name`\n * mentions in catalog text into links). Plain text when no template is set —\n * a deployment on another chat platform still gets the channel name.\n */\nfunction SupportChannel({ name }: { name: string }) {\n const { chatChannelUrlTemplate } = useUiSettings()\n const label = (\n <>\n <Slack className=\"size-3.5\" />\n {`#${name}`}\n </>\n )\n\n if (!chatChannelUrlTemplate) {\n return <span className=\"inline-flex items-center gap-1\">{label}</span>\n }\n\n return (\n <a\n href={chatChannelUrlTemplate.replace('{name}', name)}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 hover:text-primary transition-colors\"\n >\n {label}\n </a>\n )\n}\n\n/**\n * Subtle attribution line (author, support channel, repo links). Content is\n * supplied by the consuming app via `UiSettings.attribution` — the OSS core\n * hard-codes nothing company-specific. Renders nothing when unset.\n *\n * `kind: 'oss'` links get a GitHub mark; everything else gets a generic\n * external-link icon (proprietary repos, etc.).\n *\n * `variant`: `page` is the home-view footer, `popover` the compact block at the\n * bottom of the header's version popover.\n */\nexport function AttributionFooter({\n variant = 'page',\n}: {\n variant?: 'page' | 'popover'\n}) {\n const { attribution } = useUiSettings()\n if (\n !attribution ||\n (!attribution.madeBy &&\n !attribution.supportChannel &&\n !attribution.links?.length)\n ) {\n return null\n }\n\n const isPopover = variant === 'popover'\n const Wrapper = isPopover ? 'div' : 'footer'\n const hasRow = attribution.supportChannel || attribution.links?.length\n\n return (\n <Wrapper\n className={\n isPopover\n ? 'border-t bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground'\n : 'mt-16 border-t border-border/60 pt-5 text-center text-[12.5px] text-muted-foreground'\n }\n >\n {attribution.madeBy && <span>{attribution.madeBy}</span>}\n {hasRow && (\n <div\n className={\n isPopover\n ? 'mt-1 flex flex-wrap items-center gap-x-3 gap-y-1'\n : 'mt-1.5 flex flex-wrap items-center justify-center gap-x-4 gap-y-1'\n }\n >\n {attribution.supportChannel && (\n <SupportChannel name={attribution.supportChannel} />\n )}\n {attribution.links?.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 hover:text-primary transition-colors\"\n >\n {link.kind === 'oss' ? (\n <Github className=\"size-3.5\" />\n ) : (\n <ExternalLink className=\"size-3.5\" />\n )}\n {link.label}\n </a>\n ))}\n </div>\n )}\n </Wrapper>\n )\n}\n"],"names":[],"mappings":";;;AASA,SAAS,eAAe,EAAE,QAA0B;AAClD,QAAM,EAAE,uBAAA,IAA2B,cAAA;AACnC,QAAM,QACJ,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAM,WAAU,WAAA,CAAW;AAAA,IAC3B,IAAI,IAAI;AAAA,EAAA,GACX;AAGF,MAAI,CAAC,wBAAwB;AAC3B,WAAO,oBAAC,QAAA,EAAK,WAAU,kCAAkC,UAAA,OAAM;AAAA,EACjE;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,uBAAuB,QAAQ,UAAU,IAAI;AAAA,MACnD,QAAO;AAAA,MACP,KAAI;AAAA,MACJ,WAAU;AAAA,MAET,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;AAaO,SAAS,kBAAkB;AAAA,EAChC,UAAU;AACZ,GAEG;;AACD,QAAM,EAAE,YAAA,IAAgB,cAAA;AACxB,MACE,CAAC,eACA,CAAC,YAAY,UACZ,CAAC,YAAY,kBACb,GAAC,iBAAY,UAAZ,mBAAmB,SACtB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,YAAY;AAC9B,QAAM,UAAU,YAAY,QAAQ;AACpC,QAAM,SAAS,YAAY,oBAAkB,iBAAY,UAAZ,mBAAmB;AAEhE,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WACE,YACI,qEACA;AAAA,MAGL,UAAA;AAAA,QAAA,YAAY,UAAU,oBAAC,QAAA,EAAM,UAAA,YAAY,QAAO;AAAA,QAChD,UACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WACE,YACI,qDACA;AAAA,YAGL,UAAA;AAAA,cAAA,YAAY,kBACX,oBAAC,gBAAA,EAAe,MAAM,YAAY,gBAAgB;AAAA,eAEnD,iBAAY,UAAZ,mBAAmB,IAAI,CAAC,SACvB;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBAEC,MAAM,KAAK;AAAA,kBACX,QAAO;AAAA,kBACP,KAAI;AAAA,kBACJ,WAAU;AAAA,kBAET,UAAA;AAAA,oBAAA,KAAK,SAAS,QACb,oBAAC,QAAA,EAAO,WAAU,YAAW,IAE7B,oBAAC,cAAA,EAAa,WAAU,WAAA,CAAW;AAAA,oBAEpC,KAAK;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAXD,KAAK;AAAA,cAAA;AAAA,YAab;AAAA,UAAA;AAAA,QAAA;AAAA,MACH;AAAA,IAAA;AAAA,EAAA;AAIR;"}
|
|
@@ -43,13 +43,19 @@ export interface UiSettings {
|
|
|
43
43
|
/** PWA auto-update configuration (idle timeout, check interval, debug) */
|
|
44
44
|
pwaAutoUpdate?: PwaAutoUpdateOptions;
|
|
45
45
|
/**
|
|
46
|
-
* Optional attribution shown as a subtle footer line on the home view
|
|
47
|
-
* Kept generic here (no hard-coded
|
|
48
|
-
* vendor-neutral; the consuming app
|
|
46
|
+
* Optional attribution shown as a subtle footer line on the home view and in
|
|
47
|
+
* the header's version popover. Kept generic here (no hard-coded
|
|
48
|
+
* names/links) so the OSS core stays vendor-neutral; the consuming app
|
|
49
|
+
* supplies its own author, repo links and support channel.
|
|
49
50
|
*/
|
|
50
51
|
attribution?: {
|
|
51
52
|
/** e.g. "Made by <name>" — rendered verbatim. */
|
|
52
53
|
madeBy?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Chat channel where users can ask for help, without the `#`. Linked via
|
|
56
|
+
* {@link UiSettings.chatChannelUrlTemplate}; plain text when that is unset.
|
|
57
|
+
*/
|
|
58
|
+
supportChannel?: string;
|
|
53
59
|
/** Labeled links; `kind` lets the UI hint open-source vs proprietary. */
|
|
54
60
|
links?: {
|
|
55
61
|
label: string;
|
|
@@ -7,6 +7,7 @@ import SvgAppCatalog from "../../../assets/app-catalog.svg.js";
|
|
|
7
7
|
import { useTRPC } from "../../../api/infra/trpc.js";
|
|
8
8
|
import { ThemeSwitcher } from "../../../components/ThemeSwitcher.js";
|
|
9
9
|
import { AppCatalogContext } from "../../../modules/appCatalog/context/AppCatalogContext.js";
|
|
10
|
+
import { AttributionFooter } from "../../../modules/appCatalog/ui/catalog/AttributionFooter.js";
|
|
10
11
|
import { useAuth, useIsAuthenticated, useUser } from "../../../modules/auth/AuthContext.js";
|
|
11
12
|
import { Button } from "../../button.js";
|
|
12
13
|
import { useAuthActions } from "../../../modules/auth/useAuthActions.js";
|
|
@@ -105,7 +106,8 @@ function VersionPopover({ versions }) {
|
|
|
105
106
|
}
|
|
106
107
|
)
|
|
107
108
|
] }),
|
|
108
|
-
/* @__PURE__ */ jsx("div", { className: "px-3 py-1", children: /* @__PURE__ */ jsx(VersionDisplay, { versions }) })
|
|
109
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1", children: /* @__PURE__ */ jsx(VersionDisplay, { versions }) }),
|
|
110
|
+
/* @__PURE__ */ jsx(AttributionFooter, { variant: "popover" })
|
|
109
111
|
] })
|
|
110
112
|
] });
|
|
111
113
|
}
|
|
@@ -132,10 +134,7 @@ function Header({ middle }) {
|
|
|
132
134
|
/* @__PURE__ */ jsx(Link, { to: "/", children: /* @__PURE__ */ jsx(SvgAppCatalog, { className: "h-16 w-16" }) }),
|
|
133
135
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
134
136
|
/* @__PURE__ */ jsx(Link, { to: "/", className: "font-serif text-lg font-semibold", children: "App Catalog" }),
|
|
135
|
-
/* @__PURE__ */
|
|
136
|
-
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/60", children: "by Igor Golovin" }),
|
|
137
|
-
(appCatalogContextMaybe == null ? void 0 : appCatalogContextMaybe.versions) && /* @__PURE__ */ jsx(VersionPopover, { versions: appCatalogContextMaybe.versions })
|
|
138
|
-
] })
|
|
137
|
+
(appCatalogContextMaybe == null ? void 0 : appCatalogContextMaybe.versions) && /* @__PURE__ */ jsx(VersionPopover, { versions: appCatalogContextMaybe.versions })
|
|
139
138
|
] })
|
|
140
139
|
] }) }),
|
|
141
140
|
middle && /* @__PURE__ */ jsx("div", { className: "flex-1", children: middle }),
|
|
@@ -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, 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 { 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 ?? '?'}\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 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 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 // No Login button: the catalog is fully browsable without signing in,\n // so it mostly advertised a flow most visitors have no reason to\n // enter. Auth itself is untouched -- the modal, the login route, the\n // client and session handling all still work, so anyone who reaches\n // the flow by URL or by calling `useAuthModal().open()` can sign in,\n // and this branch flips to the user menu below once they do. `git\n // revert` of the commit that removed it restores the button as-is.\n // DEV Login stays: it is gated behind `devLoginEnabled`, so it never\n // reaches a real deployment.\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 ) : (\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 { AttributionFooter } from '~/modules/appCatalog/ui/catalog/AttributionFooter'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\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 ?? '?'}\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 <AttributionFooter variant=\"popover\" />\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 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 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 {appCatalogContextMaybe?.versions && (\n <VersionPopover versions={appCatalogContextMaybe.versions} />\n )}\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 // No Login button: the catalog is fully browsable without signing in,\n // so it mostly advertised a flow most visitors have no reason to\n // enter. Auth itself is untouched -- the modal, the login route, the\n // client and session handling all still work, so anyone who reaches\n // the flow by URL or by calling `useAuthModal().open()` can sign in,\n // and this branch flips to the user menu below once they do. `git\n // revert` of the commit that removed it restores the button as-is.\n // DEV Login stays: it is gated behind `devLoginEnabled`, so it never\n // reaches a real deployment.\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 ) : (\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"tailwindcss": "^4.1.7",
|
|
113
113
|
"tiptap-markdown": "^0.8.0",
|
|
114
114
|
"tslib": "^2.3.0",
|
|
115
|
-
"@igstack/app-catalog-shared-core": "6.
|
|
115
|
+
"@igstack/app-catalog-shared-core": "6.2.0"
|
|
116
116
|
},
|
|
117
117
|
"devDependencies": {
|
|
118
118
|
"@tailwindcss/vite": "^4.0.5",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"vite-plugin-static-copy": "^3.1.4",
|
|
140
140
|
"vite-plugin-svgr": "^4.2.0",
|
|
141
141
|
"vitest": "^4.1.2",
|
|
142
|
-
"@igstack/app-catalog-backend-core": "6.
|
|
142
|
+
"@igstack/app-catalog-backend-core": "6.2.0"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
145
|
"react": "19.1.2",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"vite": "^6.3.5",
|
|
149
149
|
"vite-plugin-svgr": "^4.2.0"
|
|
150
150
|
},
|
|
151
|
-
"gitHead": "
|
|
151
|
+
"gitHead": "7ef73749601b799901ab612014eebbe16cced020",
|
|
152
152
|
"scripts": {
|
|
153
153
|
"build": "vite build",
|
|
154
154
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import '@testing-library/jest-dom/vitest'
|
|
4
4
|
|
|
5
5
|
import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
|
|
6
|
+
import { UiSettingsContext } from '~/context/UiSettingsContext'
|
|
6
7
|
import { VersionPopover } from '~/ui/components/header/Header'
|
|
7
8
|
|
|
8
9
|
const versions: AppVersionInfo = {
|
|
@@ -51,6 +52,53 @@ describe('VersionPopover', () => {
|
|
|
51
52
|
expect(screen.queryByText('0.18.6')).not.toBeInTheDocument()
|
|
52
53
|
})
|
|
53
54
|
|
|
55
|
+
it('shows the configured author and support channel inside the popover', () => {
|
|
56
|
+
render(
|
|
57
|
+
<UiSettingsContext
|
|
58
|
+
value={{
|
|
59
|
+
chatChannelUrlTemplate: 'https://chat.test/channels/{name}',
|
|
60
|
+
attribution: {
|
|
61
|
+
madeBy: 'by Some Author',
|
|
62
|
+
supportChannel: 'help-desk',
|
|
63
|
+
},
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<VersionPopover versions={versions} />
|
|
67
|
+
</UiSettingsContext>,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(screen.queryByText('by Some Author')).not.toBeInTheDocument()
|
|
71
|
+
|
|
72
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText('by Some Author')).toBeInTheDocument()
|
|
75
|
+
expect(screen.getByRole('link', { name: '#help-desk' })).toHaveAttribute(
|
|
76
|
+
'href',
|
|
77
|
+
'https://chat.test/channels/help-desk',
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps the support channel as plain text without a channel URL template', () => {
|
|
82
|
+
render(
|
|
83
|
+
<UiSettingsContext
|
|
84
|
+
value={{ attribution: { supportChannel: 'help-desk' } }}
|
|
85
|
+
>
|
|
86
|
+
<VersionPopover versions={versions} />
|
|
87
|
+
</UiSettingsContext>,
|
|
88
|
+
)
|
|
89
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
90
|
+
|
|
91
|
+
expect(screen.getByText('#help-desk')).toBeInTheDocument()
|
|
92
|
+
expect(screen.queryByRole('link', { name: '#help-desk' })).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('renders no attribution block when the app configures none', () => {
|
|
96
|
+
render(<VersionPopover versions={versions} />)
|
|
97
|
+
fireEvent.click(screen.getByTestId('version-info-trigger'))
|
|
98
|
+
|
|
99
|
+
expect(screen.queryByText(/^by /)).not.toBeInTheDocument()
|
|
100
|
+
})
|
|
101
|
+
|
|
54
102
|
it('renders "local" without a popover when running local-only', () => {
|
|
55
103
|
render(<VersionPopover versions={{ backend: { displayName: 'local' } }} />)
|
|
56
104
|
expect(screen.getByText('local')).toBeInTheDocument()
|
|
@@ -1,26 +1,88 @@
|
|
|
1
|
-
import { ExternalLink, Github } from 'lucide-react'
|
|
1
|
+
import { ExternalLink, Github, Slack } from 'lucide-react'
|
|
2
2
|
import { useUiSettings } from '~/context/UiSettingsContext'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Support channel as an icon + `#name`, linked through
|
|
6
|
+
* `UiSettings.chatChannelUrlTemplate` (the same template that turns `#name`
|
|
7
|
+
* mentions in catalog text into links). Plain text when no template is set —
|
|
8
|
+
* a deployment on another chat platform still gets the channel name.
|
|
9
|
+
*/
|
|
10
|
+
function SupportChannel({ name }: { name: string }) {
|
|
11
|
+
const { chatChannelUrlTemplate } = useUiSettings()
|
|
12
|
+
const label = (
|
|
13
|
+
<>
|
|
14
|
+
<Slack className="size-3.5" />
|
|
15
|
+
{`#${name}`}
|
|
16
|
+
</>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if (!chatChannelUrlTemplate) {
|
|
20
|
+
return <span className="inline-flex items-center gap-1">{label}</span>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<a
|
|
25
|
+
href={chatChannelUrlTemplate.replace('{name}', name)}
|
|
26
|
+
target="_blank"
|
|
27
|
+
rel="noopener noreferrer"
|
|
28
|
+
className="inline-flex items-center gap-1 hover:text-primary transition-colors"
|
|
29
|
+
>
|
|
30
|
+
{label}
|
|
31
|
+
</a>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Subtle attribution line (author, support channel, repo links). Content is
|
|
6
37
|
* supplied by the consuming app via `UiSettings.attribution` — the OSS core
|
|
7
38
|
* hard-codes nothing company-specific. Renders nothing when unset.
|
|
8
39
|
*
|
|
9
40
|
* `kind: 'oss'` links get a GitHub mark; everything else gets a generic
|
|
10
41
|
* external-link icon (proprietary repos, etc.).
|
|
42
|
+
*
|
|
43
|
+
* `variant`: `page` is the home-view footer, `popover` the compact block at the
|
|
44
|
+
* bottom of the header's version popover.
|
|
11
45
|
*/
|
|
12
|
-
export function AttributionFooter(
|
|
46
|
+
export function AttributionFooter({
|
|
47
|
+
variant = 'page',
|
|
48
|
+
}: {
|
|
49
|
+
variant?: 'page' | 'popover'
|
|
50
|
+
}) {
|
|
13
51
|
const { attribution } = useUiSettings()
|
|
14
|
-
if (
|
|
52
|
+
if (
|
|
53
|
+
!attribution ||
|
|
54
|
+
(!attribution.madeBy &&
|
|
55
|
+
!attribution.supportChannel &&
|
|
56
|
+
!attribution.links?.length)
|
|
57
|
+
) {
|
|
15
58
|
return null
|
|
16
59
|
}
|
|
17
60
|
|
|
61
|
+
const isPopover = variant === 'popover'
|
|
62
|
+
const Wrapper = isPopover ? 'div' : 'footer'
|
|
63
|
+
const hasRow = attribution.supportChannel || attribution.links?.length
|
|
64
|
+
|
|
18
65
|
return (
|
|
19
|
-
<
|
|
66
|
+
<Wrapper
|
|
67
|
+
className={
|
|
68
|
+
isPopover
|
|
69
|
+
? 'border-t bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground'
|
|
70
|
+
: 'mt-16 border-t border-border/60 pt-5 text-center text-[12.5px] text-muted-foreground'
|
|
71
|
+
}
|
|
72
|
+
>
|
|
20
73
|
{attribution.madeBy && <span>{attribution.madeBy}</span>}
|
|
21
|
-
{
|
|
22
|
-
<div
|
|
23
|
-
{
|
|
74
|
+
{hasRow && (
|
|
75
|
+
<div
|
|
76
|
+
className={
|
|
77
|
+
isPopover
|
|
78
|
+
? 'mt-1 flex flex-wrap items-center gap-x-3 gap-y-1'
|
|
79
|
+
: 'mt-1.5 flex flex-wrap items-center justify-center gap-x-4 gap-y-1'
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
{attribution.supportChannel && (
|
|
83
|
+
<SupportChannel name={attribution.supportChannel} />
|
|
84
|
+
)}
|
|
85
|
+
{attribution.links?.map((link) => (
|
|
24
86
|
<a
|
|
25
87
|
key={link.url}
|
|
26
88
|
href={link.url}
|
|
@@ -38,6 +100,6 @@ export function AttributionFooter() {
|
|
|
38
100
|
))}
|
|
39
101
|
</div>
|
|
40
102
|
)}
|
|
41
|
-
</
|
|
103
|
+
</Wrapper>
|
|
42
104
|
)
|
|
43
105
|
}
|
package/src/types/uiSettings.ts
CHANGED
|
@@ -46,13 +46,19 @@ export interface UiSettings {
|
|
|
46
46
|
/** PWA auto-update configuration (idle timeout, check interval, debug) */
|
|
47
47
|
pwaAutoUpdate?: PwaAutoUpdateOptions
|
|
48
48
|
/**
|
|
49
|
-
* Optional attribution shown as a subtle footer line on the home view
|
|
50
|
-
* Kept generic here (no hard-coded
|
|
51
|
-
* vendor-neutral; the consuming app
|
|
49
|
+
* Optional attribution shown as a subtle footer line on the home view and in
|
|
50
|
+
* the header's version popover. Kept generic here (no hard-coded
|
|
51
|
+
* names/links) so the OSS core stays vendor-neutral; the consuming app
|
|
52
|
+
* supplies its own author, repo links and support channel.
|
|
52
53
|
*/
|
|
53
54
|
attribution?: {
|
|
54
55
|
/** e.g. "Made by <name>" — rendered verbatim. */
|
|
55
56
|
madeBy?: string
|
|
57
|
+
/**
|
|
58
|
+
* Chat channel where users can ask for help, without the `#`. Linked via
|
|
59
|
+
* {@link UiSettings.chatChannelUrlTemplate}; plain text when that is unset.
|
|
60
|
+
*/
|
|
61
|
+
supportChannel?: string
|
|
56
62
|
/** Labeled links; `kind` lets the UI hint open-source vs proprietary. */
|
|
57
63
|
links?: {
|
|
58
64
|
label: string
|
|
@@ -10,6 +10,7 @@ import AppCatalogLogo from '~/assets/app-catalog.svg?react'
|
|
|
10
10
|
import { useTRPC } from '~/api/infra/trpc'
|
|
11
11
|
import { ThemeSwitcher } from '~/components/ThemeSwitcher'
|
|
12
12
|
import { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
13
|
+
import { AttributionFooter } from '~/modules/appCatalog/ui/catalog/AttributionFooter'
|
|
13
14
|
import {
|
|
14
15
|
useAuth,
|
|
15
16
|
useAuthActions,
|
|
@@ -154,6 +155,7 @@ export function VersionPopover({ versions }: { versions: AppVersionInfo }) {
|
|
|
154
155
|
<div className="px-3 py-1">
|
|
155
156
|
<VersionDisplay versions={versions} />
|
|
156
157
|
</div>
|
|
158
|
+
<AttributionFooter variant="popover" />
|
|
157
159
|
</PopoverContent>
|
|
158
160
|
</Popover>
|
|
159
161
|
)
|
|
@@ -193,14 +195,9 @@ export function Header({ middle }: HeaderProps) {
|
|
|
193
195
|
<Link to="/" className="font-serif text-lg font-semibold">
|
|
194
196
|
App Catalog
|
|
195
197
|
</Link>
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
</span>
|
|
200
|
-
{appCatalogContextMaybe?.versions && (
|
|
201
|
-
<VersionPopover versions={appCatalogContextMaybe.versions} />
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
198
|
+
{appCatalogContextMaybe?.versions && (
|
|
199
|
+
<VersionPopover versions={appCatalogContextMaybe.versions} />
|
|
200
|
+
)}
|
|
204
201
|
</div>
|
|
205
202
|
</div>
|
|
206
203
|
</div>
|