@nospt/plugin-dev-ai-hub 1.1.1 → 1.1.3

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.
@@ -0,0 +1,9 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { UNSAFE_LocationContext } from 'react-router-dom';
3
+
4
+ function AppLaunchScope({ children }) {
5
+ return /* @__PURE__ */ jsx(UNSAFE_LocationContext.Provider, { value: null, children });
6
+ }
7
+
8
+ export { AppLaunchScope };
9
+ //# sourceMappingURL=AppLaunchScope.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppLaunchScope.esm.js","sources":["../../../src/components/AiResourcesPage/AppLaunchScope.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { UNSAFE_LocationContext as LocationContext } from 'react-router-dom';\n\n/**\n * Scope for buttons that launch a local app through a custom-scheme deep link\n * (`vscode:`, `cursor://`, `claude-cli://`).\n *\n * `@backstage/ui` link components resolve every `href` through React Router\n * unless it is `http(s)://`, `//`, `mailto:` or `tel:`, and since\n * `@remix-run/router` 1.23.4 the router treats `vscode:…` as a route-relative\n * path — the button then points at `https://<backstage>/vscode:…`. BUI only\n * resolves when `useInRouterContext()` is true, so hiding the location\n * context here makes it pass the href through untouched; a custom-scheme\n * anchor is never same-origin, so React Aria leaves the click to the browser,\n * which hands it to the OS protocol handler.\n *\n * Nothing inside may rely on routing — wrap app launchers only.\n */\nexport function AppLaunchScope({ children }: { children: ReactNode }) {\n return (\n <LocationContext.Provider value={null as never}>\n {children}\n </LocationContext.Provider>\n );\n}\n"],"names":["LocationContext"],"mappings":";;;AAkBO,SAAS,cAAA,CAAe,EAAE,QAAA,EAAS,EAA4B;AACpE,EAAA,2BACGA,sBAAA,CAAgB,QAAA,EAAhB,EAAyB,KAAA,EAAO,MAC9B,QAAA,EACH,CAAA;AAEJ;;;;"}
@@ -3,20 +3,12 @@ import { Text } from '@backstage/ui';
3
3
  import { RiEyeLine, RiDownloadLine } from '@remixicon/react';
4
4
  import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
5
5
  import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
6
+ import { countLabel } from './countLabel.esm.js';
6
7
  import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
7
8
  import styles from './ResourceCard.module.css.esm.js';
8
9
 
9
10
  const MAX_VISIBLE_TAGS = 3;
10
11
  const POPULAR_THRESHOLD = 5;
11
- function containsLabel(resource) {
12
- const count = resource.childCount ?? 0;
13
- if (count <= 0) return void 0;
14
- if (resource.type === "marketplace")
15
- return `${count} ${count === 1 ? "plugin" : "plugins"}`;
16
- if (resource.type === "plugin")
17
- return `${count} ${count === 1 ? "item" : "items"}`;
18
- return void 0;
19
- }
20
12
  function ResourceCard({
21
13
  resource,
22
14
  onView,
@@ -28,7 +20,6 @@ function ResourceCard({
28
20
  const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);
29
21
  const hiddenTagCount = resource.tags.length - visibleTags.length;
30
22
  const ownerLabel = resource.owner?.replace(/^(group|user):(default\/)?/, "");
31
- const parentCount = resource.parents?.length ?? 0;
32
23
  return /* @__PURE__ */ jsxs(
33
24
  "div",
34
25
  {
@@ -89,28 +80,28 @@ function ResourceCard({
89
80
  }
90
81
  )
91
82
  ] }),
92
- (containsLabel(resource) || parentCount > 0) && /* @__PURE__ */ jsxs("div", { className: styles.relRow, children: [
93
- containsLabel(resource) && /* @__PURE__ */ jsx("span", { className: styles.relChip, title: "Contains", children: containsLabel(resource) }),
94
- parentCount > 0 && /* @__PURE__ */ jsxs("span", { className: styles.relChip, title: "Belongs to a container", children: [
95
- "part of ",
96
- parentCount
97
- ] })
98
- ] }),
99
83
  (resource.version || ownerLabel || !!counts?.install || !!counts?.view) && /* @__PURE__ */ jsxs("div", { className: styles.footer, children: [
100
84
  /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", children: [
101
85
  resource.version && `v${resource.version.replace(/^v/, "")}`,
102
86
  ownerLabel
103
87
  ].filter(Boolean).join(" \xB7 ") }),
104
88
  /* @__PURE__ */ jsxs("span", { className: styles.countsRow, children: [
105
- !!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.count, title: `${counts.view} views`, children: [
106
- /* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
107
- counts.view
108
- ] }),
89
+ !!counts?.view && /* @__PURE__ */ jsxs(
90
+ "span",
91
+ {
92
+ className: styles.count,
93
+ title: countLabel(counts.view, "view"),
94
+ children: [
95
+ /* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
96
+ counts.view
97
+ ]
98
+ }
99
+ ),
109
100
  !!counts?.install && /* @__PURE__ */ jsxs(
110
101
  "span",
111
102
  {
112
103
  className: counts.install >= POPULAR_THRESHOLD ? styles.countPopular : styles.count,
113
- title: `${counts.install} installs`,
104
+ title: countLabel(counts.install, "install"),
114
105
  children: [
115
106
  counts.install >= POPULAR_THRESHOLD ? "\u{1F525}" : /* @__PURE__ */ jsx(RiDownloadLine, { size: 14 }),
116
107
  counts.install
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiDownloadLine, RiEyeLine } from '@remixicon/react';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceCard.module.css';\n\ninterface ResourceCardProps {\n resource: ResourceSummary;\n onView: (entityRef: string) => void;\n /** Extra class (e.g. the initial-load entrance animation) merged onto the\n * card's own root — kept off a wrapper div so this stays the grid's direct\n * child and CSS Grid's row-height stretch (the footer's margin-top: auto\n * relies on it for equal card heights) keeps working. */\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * The per-type card: type identity (colour/icon/label from the registry),\n * title, framework badges, tags, and a footer of version · owner plus the\n * view and install counts.\n *\n * Deliberately no description — clamped to two lines at card width it was\n * unreadable, and the drawer shows it in full the moment a card is opened.\n * Deliberately no \"View source\" link either: it competed with the card's own\n * click target and lives in the drawer, which is where someone deciding\n * whether to install is already looking.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst POPULAR_THRESHOLD = 5;\n\n/**\n * The count chip a container card shows for its members (ADR-0015). Reads the\n * caller-visible `childCount`; leaf types and empty containers show nothing.\n */\nfunction containsLabel(resource: ResourceSummary): string | undefined {\n const count = resource.childCount ?? 0;\n if (count <= 0) return undefined;\n if (resource.type === 'marketplace')\n return `${count} ${count === 1 ? 'plugin' : 'plugins'}`;\n if (resource.type === 'plugin')\n return `${count} ${count === 1 ? 'item' : 'items'}`;\n return undefined;\n}\n\nexport function ResourceCard({\n resource,\n onView,\n className,\n style,\n}: ResourceCardProps) {\n const meta = getTypeMeta(resource.type);\n const counts = useTelemetryCounts(resource.entityRef);\n\n const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);\n const hiddenTagCount = resource.tags.length - visibleTags.length;\n const ownerLabel = resource.owner?.replace(/^(group|user):(default\\/)?/, '');\n const parentCount = resource.parents?.length ?? 0;\n\n return (\n <div\n className={className ? `${styles.card} ${className}` : styles.card}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n ...style,\n } as React.CSSProperties\n }\n onClick={() => onView(resource.entityRef)}\n onKeyDown={e => {\n if (e.key === 'Enter') onView(resource.entityRef);\n }}\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${resource.title ?? resource.name}`}\n >\n <div className={styles.header}>\n <div className={styles.iconBox}>\n <meta.Icon size={18} />\n </div>\n <div className={styles.headerText}>\n {/* Type reads as an eyebrow above the title. Below it, the reserved\n second title line would leave a gap between a one-line title and\n its own label; above it, that space falls before the description\n where nothing looks detached. */}\n <Text variant=\"body-x-small\" className={styles.typeLabel}>\n {meta.label}\n </Text>\n <Text\n variant=\"title-x-small\"\n weight=\"bold\"\n as=\"h3\"\n className={styles.title}\n >\n {resource.title ?? resource.name}\n </Text>\n </div>\n </div>\n\n <div className={styles.body}>\n {resource.frameworks.length > 0 && (\n <div className={styles.badges}>\n {/* Icon only — the tool's mark is recognisable enough on a card,\n and dropping the label keeps the row narrow. The name stays\n reachable: ToolIcon carries it as an aria-label, and `title`\n surfaces it on hover. Every framework is shown: icons are\n narrow enough that the full set fits, so there is no overflow\n pill to hide the tail behind. */}\n {resource.frameworks.map(f => (\n <span\n key={f}\n className={styles.frameworkBadge}\n title={frameworkLabel(f)}\n >\n <ToolIcon tool={f as FrameworkToken} size={16} />\n </span>\n ))}\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.tags}>\n {visibleTags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n {hiddenTagCount > 0 && (\n <span\n className={styles.tag}\n title={resource.tags.slice(MAX_VISIBLE_TAGS).join(', ')}\n >\n +{hiddenTagCount}\n </span>\n )}\n </div>\n )}\n\n {(containsLabel(resource) || parentCount > 0) && (\n <div className={styles.relRow}>\n {containsLabel(resource) && (\n <span className={styles.relChip} title=\"Contains\">\n {containsLabel(resource)}\n </span>\n )}\n {parentCount > 0 && (\n <span className={styles.relChip} title=\"Belongs to a container\">\n part of {parentCount}\n </span>\n )}\n </div>\n )}\n\n {(resource.version ||\n ownerLabel ||\n !!counts?.install ||\n !!counts?.view) && (\n <div className={styles.footer}>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {[\n resource.version && `v${resource.version.replace(/^v/, '')}`,\n ownerLabel,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Text>\n {/* Views and installs are the social proof someone browsing\n actually scans for, so they are chips at full foreground rather\n than grey footnotes beside the version. */}\n <span className={styles.countsRow}>\n {!!counts?.view && (\n <span className={styles.count} title={`${counts.view} views`}>\n <RiEyeLine size={14} />\n {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={\n counts.install >= POPULAR_THRESHOLD\n ? styles.countPopular\n : styles.count\n }\n title={`${counts.install} installs`}\n >\n {counts.install >= POPULAR_THRESHOLD ? (\n '\\u{1F525}'\n ) : (\n <RiDownloadLine size={14} />\n )}\n {counts.install}\n </span>\n )}\n </span>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiDownloadLine, RiEyeLine } from '@remixicon/react';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\nimport { countLabel } from './countLabel';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceCard.module.css';\n\ninterface ResourceCardProps {\n resource: ResourceSummary;\n onView: (entityRef: string) => void;\n /** Extra class (e.g. the initial-load entrance animation) merged onto the\n * card's own root — kept off a wrapper div so this stays the grid's direct\n * child and CSS Grid's row-height stretch (the footer's margin-top: auto\n * relies on it for equal card heights) keeps working. */\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * The per-type card: type identity (colour/icon/label from the registry),\n * title, framework badges, tags, and a footer of version · owner plus the\n * view and install counts.\n *\n * Deliberately no description — clamped to two lines at card width it was\n * unreadable, and the drawer shows it in full the moment a card is opened.\n * Deliberately no \"View source\" link either: it competed with the card's own\n * click target and lives in the drawer, which is where someone deciding\n * whether to install is already looking.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst POPULAR_THRESHOLD = 5;\n\nexport function ResourceCard({\n resource,\n onView,\n className,\n style,\n}: ResourceCardProps) {\n const meta = getTypeMeta(resource.type);\n const counts = useTelemetryCounts(resource.entityRef);\n\n const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);\n const hiddenTagCount = resource.tags.length - visibleTags.length;\n const ownerLabel = resource.owner?.replace(/^(group|user):(default\\/)?/, '');\n\n return (\n <div\n className={className ? `${styles.card} ${className}` : styles.card}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n ...style,\n } as React.CSSProperties\n }\n onClick={() => onView(resource.entityRef)}\n onKeyDown={e => {\n if (e.key === 'Enter') onView(resource.entityRef);\n }}\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${resource.title ?? resource.name}`}\n >\n <div className={styles.header}>\n <div className={styles.iconBox}>\n <meta.Icon size={18} />\n </div>\n <div className={styles.headerText}>\n {/* Type reads as an eyebrow above the title. Below it, the reserved\n second title line would leave a gap between a one-line title and\n its own label; above it, that space falls before the description\n where nothing looks detached. */}\n <Text variant=\"body-x-small\" className={styles.typeLabel}>\n {meta.label}\n </Text>\n <Text\n variant=\"title-x-small\"\n weight=\"bold\"\n as=\"h3\"\n className={styles.title}\n >\n {resource.title ?? resource.name}\n </Text>\n </div>\n </div>\n\n <div className={styles.body}>\n {resource.frameworks.length > 0 && (\n <div className={styles.badges}>\n {/* Icon only — the tool's mark is recognisable enough on a card,\n and dropping the label keeps the row narrow. The name stays\n reachable: ToolIcon carries it as an aria-label, and `title`\n surfaces it on hover. Every framework is shown: icons are\n narrow enough that the full set fits, so there is no overflow\n pill to hide the tail behind. */}\n {resource.frameworks.map(f => (\n <span\n key={f}\n className={styles.frameworkBadge}\n title={frameworkLabel(f)}\n >\n <ToolIcon tool={f as FrameworkToken} size={16} />\n </span>\n ))}\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.tags}>\n {visibleTags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n {hiddenTagCount > 0 && (\n <span\n className={styles.tag}\n title={resource.tags.slice(MAX_VISIBLE_TAGS).join(', ')}\n >\n +{hiddenTagCount}\n </span>\n )}\n </div>\n )}\n\n {(resource.version ||\n ownerLabel ||\n !!counts?.install ||\n !!counts?.view) && (\n <div className={styles.footer}>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {[\n resource.version && `v${resource.version.replace(/^v/, '')}`,\n ownerLabel,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Text>\n {/* Views and installs are the social proof someone browsing\n actually scans for, so they are chips at full foreground rather\n than grey footnotes beside the version. */}\n <span className={styles.countsRow}>\n {!!counts?.view && (\n <span\n className={styles.count}\n title={countLabel(counts.view, 'view')}\n >\n <RiEyeLine size={14} />\n {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={\n counts.install >= POPULAR_THRESHOLD\n ? styles.countPopular\n : styles.count\n }\n title={countLabel(counts.install, 'install')}\n >\n {counts.install >= POPULAR_THRESHOLD ? (\n '\\u{1F525}'\n ) : (\n <RiDownloadLine size={14} />\n )}\n {counts.install}\n </span>\n )}\n </span>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
2
 
3
- var css_248z = "@layer components {\n /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .ResourceCard_card__445ab09501 {\n background: var(--bui-bg-neutral-1);\n border: 1px solid var(--bui-border-1);\n /* The type's colour, kept as the card's identity at a glance in the grid.\n 2px rather than 3: on a 16px radius a thick stripe fights the curve. */\n border-left: 2px solid var(--card-accent);\n border-radius: var(--bui-radius-5);\n overflow: hidden;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n padding: var(--bui-space-4);\n /* Room for the tallest realistic card (2-line title, badges, tags,\n footer) so the grid reads as a grid. The footer's margin-top: auto\n absorbs the slack on lighter cards. */\n min-height: 176px;\n transition: box-shadow 160ms ease,\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1), border-color 160ms ease;\n }\n\n .ResourceCard_card__445ab09501:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent) 40%,\n var(--bui-border-1)\n );\n border-left-color: var(--card-accent);\n /* Two-part shadow: a soft ambient one and a tighter contact one. A single\n large blur reads as a glow rather than as elevation. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),\n 0 12px 28px -12px color-mix(in srgb, var(--card-accent) 40%, transparent);\n transform: translateY(-2px);\n }\n\n .ResourceCard_card__445ab09501:focus-visible {\n outline: 2px solid var(--bui-ring, var(--card-accent));\n outline-offset: 2px;\n }\n\n /* The header is a fixed-height zone with its contents centred, rather than\n a top-aligned block with a reserved second title line. Same outcome for\n alignment across the grid — everything below starts at one height — but a\n one-line title now sits centred in that zone instead of leaving an\n orphaned gap beneath it, which is what made short-titled cards read as\n detached from their own content. */\n .ResourceCard_header__445ab09501 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n flex-shrink: 0;\n min-height: 3.5rem;\n }\n\n .ResourceCard_iconBox__445ab09501 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-4);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceCard_headerText__445ab09501 {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n }\n\n .ResourceCard_title__445ab09501 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n /* Tighter leading than body copy: a two-line title should read as one\n object rather than as two stacked lines. */\n line-height: 1.25;\n }\n\n .ResourceCard_typeLabel__445ab09501 {\n display: block;\n color: var(--bui-fg-secondary);\n font-weight: 600;\n font-size: 0.6875rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n margin-bottom: 2px;\n }\n\n .ResourceCard_body__445ab09501 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n flex: 1;\n }\n\n .ResourceCard_badges__445ab09501 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Holds a single tool icon, so the padding is square and the badge reads as\n a circle rather than a pill. */\n .ResourceCard_frameworkBadge__445ab09501 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-full, 999px);\n padding: var(--bui-space-1);\n font-size: 0.8125rem;\n line-height: 1;\n color: var(--bui-fg-primary);\n }\n\n .ResourceCard_tags__445ab09501 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Quiet chips rather than bare \"#text\": a chip reads as a discrete value,\n which is what a tag is, and the surface keeps them from competing with the\n description. Not interactive, so they stay low-contrast. */\n .ResourceCard_tag__445ab09501 {\n font-size: 0.6875rem;\n line-height: 1.6;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n\n /* Relationship chips: a compact signal that a card contains members or\n belongs to a container (ADR-0015). The navigable list lives in the detail\n drawer; here it is just a count, so the chips stay quiet and outlined. */\n .ResourceCard_relRow__445ab09501 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n .ResourceCard_relChip__445ab09501 {\n font-size: 0.6875rem;\n line-height: 1.6;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n border: 1px solid var(--bui-border-1);\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n\n /* No rule above the footer — the gap does the separating, and the meta is\n already de-emphasised enough to read as a different tier. */\n .ResourceCard_footer__445ab09501 {\n margin-top: auto;\n padding-top: var(--bui-space-1);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-2);\n font-size: 0.6875rem;\n }\n\n .ResourceCard_countsRow__445ab09501 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n flex-shrink: 0;\n }\n\n /* Chips, not footnotes: views and installs are the signal a browser scans\n for, so they carry a surface and the full foreground colour while the\n version/owner meta stays secondary beside them. */\n .ResourceCard_count__445ab09501 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n padding: 2px var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--bui-fg-primary);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n /* The glyph takes the card's accent while the number stays foreground: a\n fully monochrome chip reads as detached on a card that is otherwise\n colour-coded, and colouring the number too would cost its legibility. */\n .ResourceCard_count__445ab09501 svg {\n color: var(--card-accent);\n flex-shrink: 0;\n }\n\n /* Past the popularity threshold the install chip takes the type's own\n colour — the one place on the card where the accent marks a value rather\n than an identity. */\n .ResourceCard_countPopular__445ab09501 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n }\n}\n";
4
- var styles = {"card":"ResourceCard_card__445ab09501","header":"ResourceCard_header__445ab09501","iconBox":"ResourceCard_iconBox__445ab09501","headerText":"ResourceCard_headerText__445ab09501","title":"ResourceCard_title__445ab09501","typeLabel":"ResourceCard_typeLabel__445ab09501","body":"ResourceCard_body__445ab09501","badges":"ResourceCard_badges__445ab09501","frameworkBadge":"ResourceCard_frameworkBadge__445ab09501","tags":"ResourceCard_tags__445ab09501","tag":"ResourceCard_tag__445ab09501","relRow":"ResourceCard_relRow__445ab09501","relChip":"ResourceCard_relChip__445ab09501","footer":"ResourceCard_footer__445ab09501","countsRow":"ResourceCard_countsRow__445ab09501","count":"ResourceCard_count__445ab09501","countPopular":"ResourceCard_countPopular__445ab09501 ResourceCard_count__445ab09501"};
3
+ var css_248z = "@layer components {\n /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .ResourceCard_card__82e843b6a3 {\n background: var(--bui-bg-neutral-1);\n border: 1px solid var(--bui-border-1);\n /* The type's colour, kept as the card's identity at a glance in the grid.\n 2px rather than 3: on a 16px radius a thick stripe fights the curve. */\n border-left: 2px solid var(--card-accent);\n border-radius: var(--bui-radius-5);\n overflow: hidden;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n padding: var(--bui-space-4);\n /* Room for the tallest realistic card (2-line title, badges, tags,\n footer) so the grid reads as a grid. The footer's margin-top: auto\n absorbs the slack on lighter cards. */\n min-height: 176px;\n transition: box-shadow 160ms ease,\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1), border-color 160ms ease;\n }\n\n .ResourceCard_card__82e843b6a3:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent) 40%,\n var(--bui-border-1)\n );\n border-left-color: var(--card-accent);\n /* Two-part shadow: a soft ambient one and a tighter contact one. A single\n large blur reads as a glow rather than as elevation. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),\n 0 12px 28px -12px color-mix(in srgb, var(--card-accent) 40%, transparent);\n transform: translateY(-2px);\n }\n\n .ResourceCard_card__82e843b6a3:focus-visible {\n outline: 2px solid var(--bui-ring, var(--card-accent));\n outline-offset: 2px;\n }\n\n /* The header is a fixed-height zone with its contents centred, rather than\n a top-aligned block with a reserved second title line. Same outcome for\n alignment across the grid — everything below starts at one height — but a\n one-line title now sits centred in that zone instead of leaving an\n orphaned gap beneath it, which is what made short-titled cards read as\n detached from their own content. */\n .ResourceCard_header__82e843b6a3 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n flex-shrink: 0;\n min-height: 3.5rem;\n }\n\n .ResourceCard_iconBox__82e843b6a3 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-4);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceCard_headerText__82e843b6a3 {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n }\n\n .ResourceCard_title__82e843b6a3 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n /* Tighter leading than body copy: a two-line title should read as one\n object rather than as two stacked lines. */\n line-height: 1.25;\n }\n\n .ResourceCard_typeLabel__82e843b6a3 {\n display: block;\n color: var(--bui-fg-secondary);\n font-weight: 600;\n font-size: 0.6875rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n margin-bottom: 2px;\n }\n\n .ResourceCard_body__82e843b6a3 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n flex: 1;\n }\n\n .ResourceCard_badges__82e843b6a3 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Holds a single tool icon, so the padding is square and the badge reads as\n a circle rather than a pill. */\n .ResourceCard_frameworkBadge__82e843b6a3 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-full, 999px);\n padding: var(--bui-space-1);\n font-size: 0.8125rem;\n line-height: 1;\n color: var(--bui-fg-primary);\n }\n\n .ResourceCard_tags__82e843b6a3 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Quiet chips rather than bare \"#text\": a chip reads as a discrete value,\n which is what a tag is, and the surface keeps them from competing with the\n description. Not interactive, so they stay low-contrast. */\n .ResourceCard_tag__82e843b6a3 {\n font-size: 0.6875rem;\n line-height: 1.6;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n\n /* No rule above the footer — the gap does the separating, and the meta is\n already de-emphasised enough to read as a different tier. */\n .ResourceCard_footer__82e843b6a3 {\n margin-top: auto;\n padding-top: var(--bui-space-1);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-2);\n font-size: 0.6875rem;\n }\n\n .ResourceCard_countsRow__82e843b6a3 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n flex-shrink: 0;\n }\n\n /* Chips, not footnotes: views and installs are the signal a browser scans\n for, so they carry a surface and the full foreground colour while the\n version/owner meta stays secondary beside them. */\n .ResourceCard_count__82e843b6a3 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n padding: 2px var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--bui-fg-primary);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n /* The glyph takes the card's accent while the number stays foreground: a\n fully monochrome chip reads as detached on a card that is otherwise\n colour-coded, and colouring the number too would cost its legibility. */\n .ResourceCard_count__82e843b6a3 svg {\n color: var(--card-accent);\n flex-shrink: 0;\n }\n\n /* Past the popularity threshold the install chip takes the type's own\n colour — the one place on the card where the accent marks a value rather\n than an identity. */\n .ResourceCard_countPopular__82e843b6a3 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n }\n}\n";
4
+ var styles = {"card":"ResourceCard_card__82e843b6a3","header":"ResourceCard_header__82e843b6a3","iconBox":"ResourceCard_iconBox__82e843b6a3","headerText":"ResourceCard_headerText__82e843b6a3","title":"ResourceCard_title__82e843b6a3","typeLabel":"ResourceCard_typeLabel__82e843b6a3","body":"ResourceCard_body__82e843b6a3","badges":"ResourceCard_badges__82e843b6a3","frameworkBadge":"ResourceCard_frameworkBadge__82e843b6a3","tags":"ResourceCard_tags__82e843b6a3","tag":"ResourceCard_tag__82e843b6a3","footer":"ResourceCard_footer__82e843b6a3","countsRow":"ResourceCard_countsRow__82e843b6a3","count":"ResourceCard_count__82e843b6a3","countPopular":"ResourceCard_countPopular__82e843b6a3 ResourceCard_count__82e843b6a3"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { styles as default };
@@ -2,18 +2,17 @@ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { useState, useEffect } from 'react';
3
3
  import ReactMarkdown from 'react-markdown';
4
4
  import { Skeleton, Flex, Box, Text, ButtonIcon, Button, Link } from '@backstage/ui';
5
- import { RiCloseLine, RiEyeLine, RiInstallLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
5
+ import { RiPriceTag3Line, RiEyeLine, RiInstallLine, RiCloseLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
6
6
  import { useApi, configApiRef } from '@backstage/core-plugin-api';
7
7
  import { EntityRefLink } from '@backstage/plugin-catalog-react';
8
- import { getBodyShape, isParseableEntityRef, hasCopyableBody, hasDownloadableArtifact, showsContentSection } from '@nospt/plugin-dev-ai-hub-common';
8
+ import { isParseableEntityRef, hasCopyableBody, hasDownloadableArtifact, showsContentSection } from '@nospt/plugin-dev-ai-hub-common';
9
9
  import { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient.esm.js';
10
10
  import { useResourceBody } from '../../hooks/useResourceBody.esm.js';
11
- import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
12
- import { CollapsibleSection } from './CollapsibleSection.esm.js';
13
- import { JsonBody } from './JsonBody.esm.js';
11
+ import { useTrackTelemetry, useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
14
12
  import { ResourceRelationships } from './ResourceRelationships.esm.js';
15
13
  import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
16
14
  import { ResourceInstallDialog } from './ResourceInstallDialog.esm.js';
15
+ import { countLabel } from './countLabel.esm.js';
17
16
  import { stripFrontmatter } from './stripFrontmatter.esm.js';
18
17
  import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
19
18
  import styles from './ResourceDetailPanel.module.css.esm.js';
@@ -33,6 +32,7 @@ function ResourceDetailPanel({
33
32
  if (resourceProp) setDisplayResource(resourceProp);
34
33
  }, [resourceProp]);
35
34
  const api = useApi(devAiHubResourceApiRef);
35
+ const track = useTrackTelemetry();
36
36
  const config = useApi(configApiRef);
37
37
  const panelWidth = Math.max(
38
38
  MIN_PANEL_WIDTH,
@@ -40,7 +40,7 @@ function ResourceDetailPanel({
40
40
  );
41
41
  const openedRef = resourceProp?.entityRef;
42
42
  useEffect(() => {
43
- if (openedRef) api.track(openedRef, "view");
43
+ if (openedRef) track(openedRef, "view");
44
44
  }, [openedRef]);
45
45
  const actionable = !!displayResource?.sourceLocation;
46
46
  const bodyState = useResourceBody(displayResource?.entityRef, actionable);
@@ -50,30 +50,22 @@ function ResourceDetailPanel({
50
50
  if (!displayResource) return null;
51
51
  const resource = displayResource;
52
52
  const meta = getTypeMeta(resource.type);
53
- const bodyShape = getBodyShape(resource.type);
54
53
  const handleCopy = async () => {
55
54
  if (!bodyState.body) return;
56
55
  await navigator.clipboard.writeText(bodyState.body.content);
57
- api.track(resource.entityRef, "copy");
56
+ track(resource.entityRef, "copy");
58
57
  setCopied(true);
59
58
  setTimeout(() => setCopied(false), 2e3);
60
59
  };
61
60
  const handleDownload = () => {
62
- api.track(resource.entityRef, "download");
61
+ track(resource.entityRef, "download");
63
62
  return api.downloadEntityBody(resource.entityRef);
64
63
  };
65
- const handleInstall = () => {
66
- api.track(resource.entityRef, "install");
67
- setInstallOpen(true);
68
- };
64
+ const handleInstall = () => setInstallOpen(true);
69
65
  const metadataRows = [
70
- ["Type", meta.label],
71
- ["Lifecycle", resource.lifecycle || void 0],
72
66
  ["Owner", resource.owner],
73
- ["Version", resource.version],
74
67
  ["Entity ref", resource.entityRef]
75
68
  ];
76
- const collapsibleContent = bodyShape === "json";
77
69
  const bodyProblem = /* @__PURE__ */ jsxs(Fragment, { children: [
78
70
  !actionable && /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", color: "secondary", children: "No content location published for this resource." }),
79
71
  actionable && bodyState.error === "not-found" && /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", color: "secondary", children: "Content not available \u2014 it may have been removed, or you may not have access to it." }),
@@ -90,14 +82,7 @@ function ResourceDetailPanel({
90
82
  /* @__PURE__ */ jsx(Skeleton, { width: "85%", height: 14 }),
91
83
  /* @__PURE__ */ jsx(Skeleton, { width: "60%", height: 14 })
92
84
  ] }),
93
- bodyState.body && bodyShape === "json" && /* @__PURE__ */ jsx(
94
- JsonBody,
95
- {
96
- content: bodyState.body.content,
97
- className: styles.codeBlock
98
- }
99
- ),
100
- bodyState.body && bodyShape === "markdown" && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: stripFrontmatter(bodyState.body.content) }) })
85
+ bodyState.body && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: stripFrontmatter(bodyState.body.content) }) })
101
86
  ] });
102
87
  return /* @__PURE__ */ jsxs(Fragment, { children: [
103
88
  /* @__PURE__ */ jsx(
@@ -141,10 +126,28 @@ function ResourceDetailPanel({
141
126
  Text,
142
127
  {
143
128
  variant: "body-x-small",
144
- style: { color: meta.color, fontWeight: 600 },
129
+ className: styles.typeLabel,
130
+ style: { color: meta.color },
145
131
  children: meta.label
146
132
  }
147
- )
133
+ ),
134
+ (resource.version || !!counts?.view || !!counts?.install) && /* @__PURE__ */ jsxs("div", { className: styles.headerBadges, children: [
135
+ resource.version && /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
136
+ /* @__PURE__ */ jsx(RiPriceTag3Line, { size: 14 }),
137
+ "v",
138
+ resource.version.replace(/^v/, "")
139
+ ] }),
140
+ !!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
141
+ /* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
142
+ " ",
143
+ countLabel(counts.view, "view")
144
+ ] }),
145
+ !!counts?.install && /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
146
+ /* @__PURE__ */ jsx(RiInstallLine, { size: 14 }),
147
+ " ",
148
+ countLabel(counts.install, "install")
149
+ ] })
150
+ ] })
148
151
  ] }),
149
152
  /* @__PURE__ */ jsx(
150
153
  ButtonIcon,
@@ -157,35 +160,24 @@ function ResourceDetailPanel({
157
160
  )
158
161
  ] }),
159
162
  /* @__PURE__ */ jsxs("div", { className: styles.body, children: [
160
- resource.description && /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", children: resource.description }),
163
+ resource.description && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
164
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Description:" }),
165
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", children: resource.description })
166
+ ] }),
161
167
  /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
162
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Metadata" }),
168
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Metadata:" }),
163
169
  /* @__PURE__ */ jsx("dl", { className: styles.metaList, children: metadataRows.filter(([, v]) => v).map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
164
170
  /* @__PURE__ */ jsx("dt", { children: k }),
165
171
  /* @__PURE__ */ jsx("dd", { children: ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v) ? /* @__PURE__ */ jsx(EntityRefLink, { entityRef: v, children: v }) : v })
166
172
  ] }, k)) })
167
173
  ] }),
168
174
  resource.frameworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
169
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Works with" }),
175
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Works with:" }),
170
176
  /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.frameworks.map((f) => /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
171
177
  /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 14 }),
172
178
  frameworkLabel(f)
173
179
  ] }, f)) })
174
180
  ] }),
175
- (!!counts?.view || !!counts?.install) && /* @__PURE__ */ jsxs("div", { className: styles.statsRow, children: [
176
- !!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.statItem, children: [
177
- /* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
178
- " ",
179
- counts.view,
180
- " views"
181
- ] }),
182
- !!counts?.install && /* @__PURE__ */ jsxs("span", { className: styles.statItem, children: [
183
- /* @__PURE__ */ jsx(RiInstallLine, { size: 14 }),
184
- " ",
185
- counts.install,
186
- " installs"
187
- ] })
188
- ] }),
189
181
  actionable && /* @__PURE__ */ jsxs("div", { className: styles.actions, children: [
190
182
  hasCopyableBody(resource.type) && /* @__PURE__ */ jsx(
191
183
  Button,
@@ -227,33 +219,18 @@ function ResourceDetailPanel({
227
219
  onOpen
228
220
  }
229
221
  ),
230
- showsContentSection(resource.type) && /* @__PURE__ */ jsx("div", { className: styles.section, children: collapsibleContent ? (
231
- // Keyed on entityRef: the drawer stays mounted across a
232
- // relationship navigation (only `displayResource` swaps), so
233
- // without a key React reuses this instance and carries a
234
- // collapsed choice from the previous resource onto the next
235
- // one, silently defeating "expanded by default".
236
- /* @__PURE__ */ jsx(
237
- CollapsibleSection,
238
- {
239
- label: "Content",
240
- defaultOpen: true,
241
- children: contentInner
242
- },
243
- resource.entityRef
244
- )
245
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
246
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Content" }),
247
- contentInner
248
- ] }) }),
249
- !showsContentSection(resource.type) && hasBodyProblem && /* @__PURE__ */ jsx("div", { className: styles.section, children: bodyProblem }),
250
222
  resource.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
251
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Tags" }),
252
- /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.tags.map((t) => /* @__PURE__ */ jsxs("span", { className: styles.tag, children: [
223
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Tags:" }),
224
+ /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.tags.map((t) => /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
253
225
  "#",
254
226
  t
255
227
  ] }, t)) })
256
228
  ] }),
229
+ showsContentSection(resource.type) && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
230
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Content:" }),
231
+ contentInner
232
+ ] }),
233
+ !showsContentSection(resource.type) && hasBodyProblem && /* @__PURE__ */ jsx("div", { className: styles.section, children: bodyProblem }),
257
234
  resource.sourceLocation && /* @__PURE__ */ jsx("div", { className: styles.section, children: /* @__PURE__ */ jsx(
258
235
  Link,
259
236
  {
@@ -275,6 +252,7 @@ function ResourceDetailPanel({
275
252
  {
276
253
  resource,
277
254
  body: bodyState.body,
255
+ bodyLoading: bodyState.loading,
278
256
  isOpen: installOpen,
279
257
  onOpenChange: setInstallOpen
280
258
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceDetailPanel.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceDetailPanel.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Box,\n Button,\n ButtonIcon,\n Flex,\n Link,\n Skeleton,\n Text,\n} from '@backstage/ui';\nimport {\n RiCloseLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiEyeLine,\n RiFileCopyLine,\n RiInstallLine,\n} from '@remixicon/react';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { EntityRefLink } from '@backstage/plugin-catalog-react';\nimport {\n getBodyShape,\n hasCopyableBody,\n hasDownloadableArtifact,\n isParseableEntityRef,\n showsContentSection,\n type FrameworkToken,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { CollapsibleSection } from './CollapsibleSection';\nimport { JsonBody } from './JsonBody';\nimport { ResourceRelationships } from './ResourceRelationships';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { stripFrontmatter } from './stripFrontmatter';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n /** Every caller-visible resource, so relationship refs can be resolved. */\n items?: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen?: (entityRef: string) => void;\n}\n\n/**\n * Metadata rows that name a catalog entity and should link there. Rendering\n * still falls back to plain text per-row when the value isn't a parseable\n * ref (`isParseableEntityRef`) — Owner isn't guaranteed parseable, since\n * `toResourceSummary` deliberately passes a malformed one through rather\n * than dropping the resource, and `EntityRefLink` throws on a ref it can't\n * parse. Entity ref is always valid (built with `stringifyEntityRef`), so it\n * always renders as a link. Owner is also surfaced near the top of the\n * drawer; it stays in this list too.\n */\nconst ENTITY_REF_ROWS = new Set(['Owner', 'Entity ref']);\n\n/** Drawer width when `devAiHub.ui.detailPanelWidth` is unset (px). */\nconst DEFAULT_PANEL_WIDTH = 615;\n/** Floor for a configured width, so a fat-fingered value can't collapse it. */\nconst MIN_PANEL_WIDTH = 320;\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n items = [],\n onOpen,\n}: ResourceDetailPanelProps) {\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n const config = useApi(configApiRef);\n const panelWidth = Math.max(\n MIN_PANEL_WIDTH,\n config.getOptionalNumber('devAiHub.ui.detailPanelWidth') ??\n DEFAULT_PANEL_WIDTH,\n );\n\n // A `view` is a deliberate look at one resource, so it is recorded here —\n // when the drawer opens — and not on card render, where merely loading the\n // page counted a view for every card in the grid. Keyed on the *live* prop,\n // not `displayResource`: that one stays set through the exit animation, and\n // closing must not count a second view.\n const openedRef = resourceProp?.entityRef;\n useEffect(() => {\n if (openedRef) api.track(openedRef, 'view');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [openedRef]);\n\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n const bodyShape = getBodyShape(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n await navigator.clipboard.writeText(bodyState.body.content);\n api.track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = () => {\n api.track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n const handleInstall = () => {\n api.track(resource.entityRef, 'install');\n setInstallOpen(true);\n };\n\n // Owner is also surfaced near the top of the drawer; it stays here too.\n const metadataRows: [string, string | undefined][] = [\n ['Type', meta.label],\n ['Lifecycle', resource.lifecycle || undefined],\n ['Owner', resource.owner],\n ['Version', resource.version],\n ['Entity ref', resource.entityRef],\n ];\n\n // `plugin`/`marketplace` bodies are the full manifest JSON (ADR-0014) —\n // long enough to bury everything below them, so their Content collapses.\n const collapsibleContent = bodyShape === 'json';\n\n // Body-fetch problems (missing location, 404, upstream failure + retry).\n // Pulled out because Copy/Install depend on the body even when the \"Content\"\n // section itself is suppressed (plugin/marketplace) — the user still needs\n // to see the failure and be able to retry.\n const bodyProblem = (\n <>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not have\n access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button size=\"small\" variant=\"tertiary\" onPress={bodyState.retry}>\n Retry\n </Button>\n </Flex>\n )}\n </>\n );\n\n const hasBodyProblem =\n !actionable ||\n bodyState.error === 'not-found' ||\n bodyState.error === 'upstream';\n\n const contentInner = (\n <>\n {bodyProblem}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {bodyState.body && bodyShape === 'json' && (\n <JsonBody\n content={bodyState.body.content}\n className={styles.codeBlock}\n />\n )}\n {bodyState.body && bodyShape === 'markdown' && (\n <div className={styles.markdown}>\n <ReactMarkdown>\n {stripFrontmatter(bodyState.body.content)}\n </ReactMarkdown>\n </div>\n )}\n </>\n );\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={\n {\n '--drawer-accent': meta.color,\n '--drawer-width': `${panelWidth}px`,\n } as React.CSSProperties\n }\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n style={{ color: meta.color, fontWeight: 600 }}\n >\n {meta.label}\n </Text>\n </Box>\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onClick={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>\n {ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v!) ? (\n <EntityRefLink entityRef={v!}>{v}</EntityRefLink>\n ) : (\n v\n )}\n </dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {(!!counts?.view || !!counts?.install) && (\n <div className={styles.statsRow}>\n {!!counts?.view && (\n <span className={styles.statItem}>\n <RiEyeLine size={14} /> {counts.view} views\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.statItem}>\n <RiInstallLine size={14} /> {counts.install} installs\n </span>\n )}\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n {onOpen && (\n <ResourceRelationships\n resource={resource}\n items={items}\n onOpen={onOpen}\n />\n )}\n\n {showsContentSection(resource.type) && (\n <div className={styles.section}>\n {collapsibleContent ? (\n // Keyed on entityRef: the drawer stays mounted across a\n // relationship navigation (only `displayResource` swaps), so\n // without a key React reuses this instance and carries a\n // collapsed choice from the previous resource onto the next\n // one, silently defeating \"expanded by default\".\n <CollapsibleSection\n key={resource.entityRef}\n label=\"Content\"\n defaultOpen\n >\n {contentInner}\n </CollapsibleSection>\n ) : (\n <>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content\n </Text>\n {contentInner}\n </>\n )}\n </div>\n )}\n\n {/* Content section suppressed (plugin/marketplace) but the body\n still failed — surface the error and retry so Copy/Install\n aren't silently broken. */}\n {!showsContentSection(resource.type) && hasBodyProblem && (\n <div className={styles.section}>{bodyProblem}</div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResourceDetailPanel.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceDetailPanel.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Box,\n Button,\n ButtonIcon,\n Flex,\n Link,\n Skeleton,\n Text,\n} from '@backstage/ui';\nimport {\n RiCloseLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiEyeLine,\n RiFileCopyLine,\n RiInstallLine,\n RiPriceTag3Line,\n} from '@remixicon/react';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { EntityRefLink } from '@backstage/plugin-catalog-react';\nimport {\n hasCopyableBody,\n hasDownloadableArtifact,\n isParseableEntityRef,\n showsContentSection,\n type FrameworkToken,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport {\n useTelemetryCounts,\n useTrackTelemetry,\n} from '../../hooks/useTelemetryCounts';\nimport { ResourceRelationships } from './ResourceRelationships';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { countLabel } from './countLabel';\nimport { stripFrontmatter } from './stripFrontmatter';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n /** Every caller-visible resource, so relationship refs can be resolved. */\n items?: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen?: (entityRef: string) => void;\n}\n\n/**\n * Metadata rows that name a catalog entity and should link there. Rendering\n * still falls back to plain text per-row when the value isn't a parseable\n * ref (`isParseableEntityRef`) — Owner isn't guaranteed parseable, since\n * `toResourceSummary` deliberately passes a malformed one through rather\n * than dropping the resource, and `EntityRefLink` throws on a ref it can't\n * parse. Entity ref is always valid (built with `stringifyEntityRef`), so it\n * always renders as a link. Owner is also surfaced near the top of the\n * drawer; it stays in this list too.\n */\nconst ENTITY_REF_ROWS = new Set(['Owner', 'Entity ref']);\n\n/** Drawer width when `devAiHub.ui.detailPanelWidth` is unset (px). */\nconst DEFAULT_PANEL_WIDTH = 615;\n/** Floor for a configured width, so a fat-fingered value can't collapse it. */\nconst MIN_PANEL_WIDTH = 320;\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n items = [],\n onOpen,\n}: ResourceDetailPanelProps) {\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const config = useApi(configApiRef);\n const panelWidth = Math.max(\n MIN_PANEL_WIDTH,\n config.getOptionalNumber('devAiHub.ui.detailPanelWidth') ??\n DEFAULT_PANEL_WIDTH,\n );\n\n // A `view` is a deliberate look at one resource, so it is recorded here —\n // when the drawer opens — and not on card render, where merely loading the\n // page counted a view for every card in the grid. Keyed on the *live* prop,\n // not `displayResource`: that one stays set through the exit animation, and\n // closing must not count a second view.\n const openedRef = resourceProp?.entityRef;\n useEffect(() => {\n if (openedRef) track(openedRef, 'view');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [openedRef]);\n\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n await navigator.clipboard.writeText(bodyState.body.content);\n track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = () => {\n track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n // The `install` event is recorded by the dialog, which alone knows whether\n // it produced a launch button to wait for (ADR-0011).\n const handleInstall = () => setInstallOpen(true);\n\n // Only the two entity references live here. Type is already the coloured\n // eyebrow under the title, and version/views/installs are header chips.\n const metadataRows: [string, string | undefined][] = [\n ['Owner', resource.owner],\n ['Entity ref', resource.entityRef],\n ];\n\n // Body-fetch problems (missing location, 404, upstream failure + retry).\n // Pulled out because Copy/Install depend on the body even when the \"Content\"\n // section itself is suppressed (every JSON-bodied type) — the user still\n // needs to see the failure and be able to retry.\n const bodyProblem = (\n <>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not have\n access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button size=\"small\" variant=\"tertiary\" onPress={bodyState.retry}>\n Retry\n </Button>\n </Flex>\n )}\n </>\n );\n\n const hasBodyProblem =\n !actionable ||\n bodyState.error === 'not-found' ||\n bodyState.error === 'upstream';\n\n const contentInner = (\n <>\n {bodyProblem}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {bodyState.body && (\n <div className={styles.markdown}>\n <ReactMarkdown>\n {stripFrontmatter(bodyState.body.content)}\n </ReactMarkdown>\n </div>\n )}\n </>\n );\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={\n {\n '--drawer-accent': meta.color,\n '--drawer-width': `${panelWidth}px`,\n } as React.CSSProperties\n }\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n className={styles.typeLabel}\n style={{ color: meta.color }}\n >\n {meta.label}\n </Text>\n {/* Version and the telemetry counts ride in the header rather than\n the body: the header sits outside the scrolling `.body`, so they\n stay in view, and they read as facts about the resource rather\n than another body section. Same pill as the framework badges. */}\n {(resource.version || !!counts?.view || !!counts?.install) && (\n <div className={styles.headerBadges}>\n {resource.version && (\n <span className={styles.frameworkBadge}>\n <RiPriceTag3Line size={14} />v\n {resource.version.replace(/^v/, '')}\n </span>\n )}\n {!!counts?.view && (\n <span className={styles.frameworkBadge}>\n <RiEyeLine size={14} /> {countLabel(counts.view, 'view')}\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.frameworkBadge}>\n <RiInstallLine size={14} />{' '}\n {countLabel(counts.install, 'install')}\n </span>\n )}\n </div>\n )}\n </Box>\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onClick={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Description:\n </Text>\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata:\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>\n {ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v!) ? (\n <EntityRefLink entityRef={v!}>{v}</EntityRefLink>\n ) : (\n v\n )}\n </dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with:\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n {onOpen && (\n <ResourceRelationships\n resource={resource}\n items={items}\n onOpen={onOpen}\n />\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags:\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.frameworkBadge}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {showsContentSection(resource.type) && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content:\n </Text>\n {contentInner}\n </div>\n )}\n\n {/* Content section suppressed for JSON-bodied resources, but the body\n failure is still surfaced with Retry so Copy/Install aren't\n silently broken. */}\n {!showsContentSection(resource.type) && hasBodyProblem && (\n <div className={styles.section}>{bodyProblem}</div>\n )}\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n bodyLoading={bodyState.loading}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
2
 
3
- var css_248z = "@layer components {\n .ResourceDetailPanel_overlay__6231dbf253 {\n position: fixed;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.3);\n z-index: 1299;\n opacity: 0;\n pointer-events: none;\n transition: opacity 220ms ease;\n }\n\n .ResourceDetailPanel_overlay__6231dbf253[data-open='true'] {\n opacity: 1;\n pointer-events: auto;\n }\n\n .ResourceDetailPanel_drawer__6231dbf253 {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--drawer-width, 615px);\n max-width: 100vw;\n /* An opaque surface token, not `--bui-bg-neutral-1`: that token is a\n translucent 10% white in dark mode, meant to layer over a solid\n background. The drawer floats over the page, so it must bring its own. */\n background-color: var(--bui-bg-app);\n /* The accent mirrors the open resource's type colour (set inline by the\n component). The fallback is BUI's own accent, which the host app points\n at its brand colour — a plugin-owned brand hex here would be a second,\n competing accent on the same screen (ADR-0008, 2026-08-03 amendment). */\n border-top: 3px solid var(--drawer-accent, var(--bui-fg-info));\n box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);\n z-index: 1300;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n pointer-events: none;\n transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n .ResourceDetailPanel_drawer__6231dbf253[data-open='true'] {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .ResourceDetailPanel_overlay__6231dbf253,\n .ResourceDetailPanel_drawer__6231dbf253 {\n transition: none;\n }\n }\n\n .ResourceDetailPanel_header__6231dbf253 {\n display: flex;\n align-items: flex-start;\n gap: var(--bui-space-3);\n padding: var(--bui-space-4);\n border-bottom: 1px solid var(--bui-border-1);\n }\n\n .ResourceDetailPanel_actions__6231dbf253 {\n display: flex;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_bodyLoading__6231dbf253 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n margin-top: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_codeBlock__6231dbf253 {\n margin: var(--bui-space-2) 0 0;\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-2);\n font-size: 0.8125rem;\n overflow-x: auto;\n white-space: pre;\n }\n\n .ResourceDetailPanel_markdown__6231dbf253 {\n margin-top: var(--bui-space-2);\n font-size: 0.875rem;\n line-height: 1.55;\n overflow-wrap: anywhere;\n }\n\n .ResourceDetailPanel_markdown__6231dbf253 pre {\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-2);\n overflow-x: auto;\n }\n\n .ResourceDetailPanel_markdown__6231dbf253 h1,\n .ResourceDetailPanel_markdown__6231dbf253 h2,\n .ResourceDetailPanel_markdown__6231dbf253 h3 {\n font-size: 1rem;\n margin: var(--bui-space-3) 0 var(--bui-space-1);\n }\n\n .ResourceDetailPanel_iconBox__6231dbf253 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-2);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_headerText__6231dbf253 {\n flex: 1;\n min-width: 0;\n }\n\n .ResourceDetailPanel_body__6231dbf253 {\n padding: var(--bui-space-4);\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-4);\n }\n\n .ResourceDetailPanel_section__6231dbf253 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_badges__6231dbf253 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_statsRow__6231dbf253 {\n display: flex;\n gap: var(--bui-space-3);\n }\n\n .ResourceDetailPanel_statItem__6231dbf253 {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.8125rem;\n color: var(--bui-fg-secondary);\n }\n\n .ResourceDetailPanel_frameworkBadge__6231dbf253 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-full, 999px);\n padding: 0 var(--bui-space-2);\n font-size: 0.75rem;\n line-height: 1.8;\n }\n\n .ResourceDetailPanel_tag__6231dbf253 {\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-1);\n padding: 0 var(--bui-space-2);\n font-size: 0.75rem;\n line-height: 1.7;\n color: var(--bui-fg-secondary);\n }\n\n .ResourceDetailPanel_metaList__6231dbf253 {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_metaRow__6231dbf253 {\n display: flex;\n gap: var(--bui-space-2);\n font-size: 0.8rem;\n }\n\n .ResourceDetailPanel_metaRow__6231dbf253 dt {\n color: var(--bui-fg-secondary);\n min-width: 90px;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_metaRow__6231dbf253 dd {\n margin: 0;\n word-break: break-all;\n }\n\n .ResourceDetailPanel_sourceLinkContent__6231dbf253 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n color: var(--bui-fg-info);\n font-weight: 600;\n }\n}\n";
4
- var styles = {"overlay":"ResourceDetailPanel_overlay__6231dbf253","drawer":"ResourceDetailPanel_drawer__6231dbf253","header":"ResourceDetailPanel_header__6231dbf253","actions":"ResourceDetailPanel_actions__6231dbf253","bodyLoading":"ResourceDetailPanel_bodyLoading__6231dbf253","codeBlock":"ResourceDetailPanel_codeBlock__6231dbf253","markdown":"ResourceDetailPanel_markdown__6231dbf253","iconBox":"ResourceDetailPanel_iconBox__6231dbf253","headerText":"ResourceDetailPanel_headerText__6231dbf253","body":"ResourceDetailPanel_body__6231dbf253","section":"ResourceDetailPanel_section__6231dbf253","badges":"ResourceDetailPanel_badges__6231dbf253","statsRow":"ResourceDetailPanel_statsRow__6231dbf253","statItem":"ResourceDetailPanel_statItem__6231dbf253","frameworkBadge":"ResourceDetailPanel_frameworkBadge__6231dbf253","tag":"ResourceDetailPanel_tag__6231dbf253","metaList":"ResourceDetailPanel_metaList__6231dbf253","metaRow":"ResourceDetailPanel_metaRow__6231dbf253","sourceLinkContent":"ResourceDetailPanel_sourceLinkContent__6231dbf253"};
3
+ var css_248z = "@layer components {\n .ResourceDetailPanel_overlay__e36165df4c {\n position: fixed;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.3);\n z-index: 1299;\n opacity: 0;\n pointer-events: none;\n transition: opacity 220ms ease;\n }\n\n .ResourceDetailPanel_overlay__e36165df4c[data-open='true'] {\n opacity: 1;\n pointer-events: auto;\n }\n\n .ResourceDetailPanel_drawer__e36165df4c {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--drawer-width, 615px);\n max-width: 100vw;\n /* An opaque surface token, not `--bui-bg-neutral-1`: that token is a\n translucent 10% white in dark mode, meant to layer over a solid\n background. The drawer floats over the page, so it must bring its own. */\n background-color: var(--bui-bg-app);\n /* The accent mirrors the open resource's type colour (set inline by the\n component). The fallback is BUI's own accent, which the host app points\n at its brand colour — a plugin-owned brand hex here would be a second,\n competing accent on the same screen (ADR-0008, 2026-08-03 amendment). */\n border-top: 3px solid var(--drawer-accent, var(--bui-fg-info));\n box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);\n z-index: 1300;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n pointer-events: none;\n transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n .ResourceDetailPanel_drawer__e36165df4c[data-open='true'] {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .ResourceDetailPanel_overlay__e36165df4c,\n .ResourceDetailPanel_drawer__e36165df4c {\n transition: none;\n }\n }\n\n .ResourceDetailPanel_header__e36165df4c {\n display: flex;\n align-items: flex-start;\n gap: var(--bui-space-3);\n padding: var(--bui-space-4);\n border-bottom: 1px solid var(--bui-border-1);\n }\n\n .ResourceDetailPanel_actions__e36165df4c {\n display: flex;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_bodyLoading__e36165df4c {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n margin-top: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_codeBlock__e36165df4c {\n margin: var(--bui-space-2) 0 0;\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-2);\n font-size: 0.8125rem;\n overflow-x: auto;\n white-space: pre;\n }\n\n .ResourceDetailPanel_markdown__e36165df4c {\n margin-top: var(--bui-space-2);\n font-size: 0.875rem;\n line-height: 1.55;\n overflow-wrap: anywhere;\n }\n\n .ResourceDetailPanel_markdown__e36165df4c pre {\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-2);\n overflow-x: auto;\n }\n\n .ResourceDetailPanel_markdown__e36165df4c h1,\n .ResourceDetailPanel_markdown__e36165df4c h2,\n .ResourceDetailPanel_markdown__e36165df4c h3 {\n font-size: 1rem;\n margin: var(--bui-space-3) 0 var(--bui-space-1);\n }\n\n .ResourceDetailPanel_iconBox__e36165df4c {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-2);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_headerText__e36165df4c {\n flex: 1;\n min-width: 0;\n }\n\n /* The type eyebrow, matching the grid card's treatment\n (ResourceCard.module.css `.typeLabel`) so a resource reads the same in the\n grid and in the drawer. The colour stays inline: the drawer paints the\n eyebrow in the type's own accent, where the card uses secondary grey. */\n .ResourceDetailPanel_typeLabel__e36165df4c {\n display: block;\n font-weight: 600;\n font-size: 0.6875rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n\n .ResourceDetailPanel_body__e36165df4c {\n padding: var(--bui-space-4);\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-4);\n }\n\n .ResourceDetailPanel_section__e36165df4c {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_badges__e36165df4c {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Version / views / installs, under the title in the header. Wraps rather\n than overflowing, since `.headerText` is the flexible header column. */\n .ResourceDetailPanel_headerBadges__e36165df4c {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n margin-top: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_frameworkBadge__e36165df4c {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-full, 999px);\n padding: 0 var(--bui-space-2);\n font-size: 0.75rem;\n line-height: 1.8;\n }\n\n .ResourceDetailPanel_metaList__e36165df4c {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_metaRow__e36165df4c {\n display: flex;\n gap: var(--bui-space-2);\n font-size: 0.8rem;\n }\n\n .ResourceDetailPanel_metaRow__e36165df4c dt {\n color: var(--bui-fg-secondary);\n min-width: 90px;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_metaRow__e36165df4c dd {\n margin: 0;\n word-break: break-all;\n }\n\n .ResourceDetailPanel_sourceLinkContent__e36165df4c {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n color: var(--bui-fg-info);\n font-weight: 600;\n }\n}\n";
4
+ var styles = {"overlay":"ResourceDetailPanel_overlay__e36165df4c","drawer":"ResourceDetailPanel_drawer__e36165df4c","header":"ResourceDetailPanel_header__e36165df4c","actions":"ResourceDetailPanel_actions__e36165df4c","bodyLoading":"ResourceDetailPanel_bodyLoading__e36165df4c","markdown":"ResourceDetailPanel_markdown__e36165df4c","iconBox":"ResourceDetailPanel_iconBox__e36165df4c","headerText":"ResourceDetailPanel_headerText__e36165df4c","typeLabel":"ResourceDetailPanel_typeLabel__e36165df4c","body":"ResourceDetailPanel_body__e36165df4c","section":"ResourceDetailPanel_section__e36165df4c","badges":"ResourceDetailPanel_badges__e36165df4c","headerBadges":"ResourceDetailPanel_headerBadges__e36165df4c","frameworkBadge":"ResourceDetailPanel_frameworkBadge__e36165df4c","metaList":"ResourceDetailPanel_metaList__e36165df4c","metaRow":"ResourceDetailPanel_metaRow__e36165df4c","sourceLinkContent":"ResourceDetailPanel_sourceLinkContent__e36165df4c"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { styles as default };
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { useState } from 'react';
2
+ import { useState, useEffect } from 'react';
3
3
  import ReactMarkdown from 'react-markdown';
4
4
  import { Dialog, DialogHeader, DialogBody, Text, ButtonLink, Button, ButtonIcon } from '@backstage/ui';
5
5
  import { RiFileCopyLine, RiDownloadLine, RiExternalLinkLine, RiCheckLine } from '@remixicon/react';
@@ -7,7 +7,9 @@ import { siClaude } from 'simple-icons';
7
7
  import { useApi } from '@backstage/core-plugin-api';
8
8
  import { getBodyShape, getMarketplaceRepoSlug, getMarketplaceAddCommands, getInstallSteps, expandInstallFrameworks, getResourceInstallTarget, hasCopyableBody, hasDownloadableArtifact, getAgentInstallLinks, getMcpInstallLinks, getMarketplaceInstallTemplate, getMarketplaceTeamSnippet } from '@nospt/plugin-dev-ai-hub-common';
9
9
  import { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient.esm.js';
10
+ import { useTrackTelemetry } from '../../hooks/useTelemetryCounts.esm.js';
10
11
  import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
12
+ import { AppLaunchScope } from './AppLaunchScope.esm.js';
11
13
  import { CollapsibleSection } from './CollapsibleSection.esm.js';
12
14
  import { JsonBody } from './JsonBody.esm.js';
13
15
  import { frameworkLabel } from './typeMeta.esm.js';
@@ -123,7 +125,8 @@ function CopyPromptButton({ text, label }) {
123
125
  function MarketplaceJourney({
124
126
  resource,
125
127
  repoSlug,
126
- addCommands
128
+ addCommands,
129
+ onLaunch
127
130
  }) {
128
131
  return /* @__PURE__ */ jsxs("div", { className: styles.journey, children: [
129
132
  /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", className: styles.stepTitle, children: "1. Add the marketplace to your AI tool" }),
@@ -146,6 +149,7 @@ function MarketplaceJourney({
146
149
  className: brand.className,
147
150
  iconStart: brand.icon,
148
151
  href: link.href,
152
+ onPress: () => onLaunch(link.framework),
149
153
  children: [
150
154
  "Add in ",
151
155
  link.label
@@ -178,10 +182,12 @@ function MarketplaceJourney({
178
182
  function ResourceInstallDialog({
179
183
  resource,
180
184
  body,
185
+ bodyLoading = false,
181
186
  isOpen,
182
187
  onOpenChange
183
188
  }) {
184
189
  const api = useApi(devAiHubResourceApiRef);
190
+ const track = useTrackTelemetry();
185
191
  const [copied, setCopied] = useState(false);
186
192
  const [downloadError, setDownloadError] = useState(false);
187
193
  const shape = getBodyShape(resource.type);
@@ -206,10 +212,25 @@ function ResourceInstallDialog({
206
212
  (row) => !!row.target
207
213
  );
208
214
  const hasMergeTarget = (installSteps.length > 0 ? installSteps.map((s) => s.target) : pathRows.map((r) => r.target)).some((t) => t.mode === "merge");
215
+ const hasLauncher = installLinks.length > 0 || installSteps.some((step) => step.link) || marketplaceAddCommands.some((command) => command.deepLinks.length > 0);
216
+ const awaitingMcpBody = resource.type === "mcp-config" && bodyLoading;
217
+ useEffect(() => {
218
+ if (!isOpen || frameworksRestricted || hasLauncher || awaitingMcpBody) {
219
+ return;
220
+ }
221
+ track(resource.entityRef, "install");
222
+ }, [
223
+ isOpen,
224
+ awaitingMcpBody,
225
+ frameworksRestricted,
226
+ hasLauncher,
227
+ resource.entityRef,
228
+ track
229
+ ]);
209
230
  const handleCopy = async () => {
210
231
  if (!body) return;
211
232
  await navigator.clipboard.writeText(body.content);
212
- api.track(resource.entityRef, "copy");
233
+ track(resource.entityRef, "copy");
213
234
  setCopied(true);
214
235
  setTimeout(() => setCopied(false), 2e3);
215
236
  };
@@ -217,7 +238,7 @@ function ResourceInstallDialog({
217
238
  setDownloadError(false);
218
239
  try {
219
240
  await api.downloadEntityBody(resource.entityRef);
220
- api.track(resource.entityRef, "download");
241
+ track(resource.entityRef, "download");
221
242
  } catch {
222
243
  setDownloadError(true);
223
244
  }
@@ -238,7 +259,7 @@ function ResourceInstallDialog({
238
259
  "Install ",
239
260
  resource.title ?? resource.name
240
261
  ] }),
241
- /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs("div", { className: styles.content, children: [
262
+ /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsx(AppLaunchScope, { children: /* @__PURE__ */ jsxs("div", { className: styles.content, children: [
242
263
  /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", children: frameworksRestricted ? "No install path is available \u2014 none of the tools this resource supports are enabled for this Backstage. You can still copy or download the content below." : TYPE_HINTS[resource.type] }),
243
264
  body && shape === "json" && /* @__PURE__ */ jsx(CollapsibleSection, { label: "Content", children: /* @__PURE__ */ jsx(JsonBody, { content: body.content, className: styles.codeBlock }) }),
244
265
  hasMarketplaceJourney && marketplaceRepoSlug && /* @__PURE__ */ jsx(
@@ -246,7 +267,8 @@ function ResourceInstallDialog({
246
267
  {
247
268
  resource,
248
269
  repoSlug: marketplaceRepoSlug,
249
- addCommands: marketplaceAddCommands
270
+ addCommands: marketplaceAddCommands,
271
+ onLaunch: (framework) => track(resource.entityRef, "install", framework)
250
272
  }
251
273
  ),
252
274
  body && showBodyDoc && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: body.content }) }),
@@ -269,11 +291,7 @@ function ResourceInstallDialog({
269
291
  className: brand.className,
270
292
  iconStart: brand.icon,
271
293
  href: link.href,
272
- onPress: () => api.track(
273
- resource.entityRef,
274
- "install",
275
- framework
276
- ),
294
+ onPress: () => track(resource.entityRef, "install", framework),
277
295
  children: [
278
296
  "Install in ",
279
297
  link.label
@@ -282,7 +300,7 @@ function ResourceInstallDialog({
282
300
  );
283
301
  })() : (
284
302
  /* No prompt URI for this host — the same instruction,
285
- delivered by clipboard so the row is still actionable. */
303
+ delivered by clipboard so the row is still actionable. */
286
304
  /* @__PURE__ */ jsx(
287
305
  CopyPromptButton,
288
306
  {
@@ -325,6 +343,7 @@ function ResourceInstallDialog({
325
343
  className: brand.className,
326
344
  iconStart: brand.icon,
327
345
  href: link.href,
346
+ onPress: () => track(resource.entityRef, "install", link.framework),
328
347
  children: [
329
348
  "Install in ",
330
349
  link.label
@@ -356,7 +375,7 @@ function ResourceInstallDialog({
356
375
  )
357
376
  ] }),
358
377
  downloadError && /* @__PURE__ */ jsx(Text, { variant: "body-x-small", as: "p", children: "Download failed \u2014 the source may be unreachable. Try again or use \u201CView source\u201D." })
359
- ] }) })
378
+ ] }) }) })
360
379
  ]
361
380
  }
362
381
  );
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceInstallDialog.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceInstallDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Button,\n ButtonIcon,\n ButtonLink,\n Dialog,\n DialogBody,\n DialogHeader,\n Text,\n} from '@backstage/ui';\nimport {\n RiCheckLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiFileCopyLine,\n} from '@remixicon/react';\nimport { siClaude } from 'simple-icons';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n expandInstallFrameworks,\n getAgentInstallLinks,\n getBodyShape,\n getMarketplaceAddCommands,\n getMcpInstallLinks,\n getMarketplaceInstallTemplate,\n getMarketplaceRepoSlug,\n getMarketplaceTeamSnippet,\n getInstallSteps,\n getResourceInstallTarget,\n hasCopyableBody,\n hasDownloadableArtifact,\n type FrameworkToken,\n type MarketplaceAddCommand,\n type ResourceInstallTarget,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport {\n devAiHubResourceApiRef,\n type ResourceBody,\n} from '../../api/DevAiHubResourceClient';\nimport { ToolIcon } from '../ToolIcon';\nimport { CollapsibleSection } from './CollapsibleSection';\nimport { JsonBody } from './JsonBody';\nimport { frameworkLabel } from './typeMeta';\nimport styles from './ResourceInstallDialog.module.css';\n\ninterface ResourceInstallDialogProps {\n resource: ResourceSummary;\n body?: ResourceBody;\n isOpen: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst TYPE_HINTS: Record<ResourceSummary['type'], string> = {\n skill:\n 'Download the skill (multi-file skills arrive as one zip) and extract it into the path for your framework.',\n agent:\n 'Download or copy the agent definition into the path for your framework.',\n hook: 'Register the hook definition at the path for your framework — most hosts merge it into a shared settings file.',\n 'mcp-config': 'Add this server entry to your MCP configuration file.',\n plugin:\n 'This plugin installs through its framework — follow the instructions below.',\n marketplace:\n 'Register this marketplace with your AI tool once, then install any of its plugins from it.',\n};\n\n/** The VS Code chevron mark (devicon's mono path), for a button's iconStart slot. */\nfunction VsCodeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 128 128\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64 1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68 50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z\"\n />\n </svg>\n );\n}\n\n/** The Claude starburst, sized for a button's iconStart slot. */\nfunction ClaudeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path d={siClaude.path} />\n </svg>\n );\n}\n\n/**\n * The one-click install links for the types with a *native* install route,\n * rendered once in the actions row. `skill` and `hook` are absent on purpose:\n * their launchers are per-host and live in their own rows, so returning them\n * here too would render every button twice.\n */\nfunction getInstallLinks(\n resource: ResourceSummary,\n frameworks: string[],\n body?: ResourceBody,\n) {\n if (resource.type === 'agent') {\n return getAgentInstallLinks(\n resource.sourceLocation,\n resource.name,\n frameworks,\n );\n }\n if (resource.type === 'mcp-config') {\n return getMcpInstallLinks(frameworks, resource.name, body?.content);\n }\n return [];\n}\n\n/** Brand styling for the known deep-link hosts, keyed by link label. */\nfunction launchBrand(label: string): {\n className?: string;\n icon: React.ReactElement;\n} {\n switch (label) {\n case 'Claude':\n return { className: styles.claudeLaunch, icon: <ClaudeIcon /> };\n case 'VS Code':\n return { className: styles.vscodeLaunch, icon: <VsCodeIcon /> };\n case 'VS Code Insiders':\n return { className: styles.insidersLaunch, icon: <VsCodeIcon /> };\n case 'Cursor':\n return {\n className: styles.cursorLaunch,\n icon: <ToolIcon tool=\"cursor\" branded={false} size={16} />,\n };\n default:\n return { icon: <RiExternalLinkLine /> };\n }\n}\n\n/**\n * Icon-only copy button for a single generated command (marketplace journey\n * rows); flashes a check mark as the copied feedback.\n */\nfunction CopyCommandButton({ text }: { text: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <ButtonIcon\n size=\"small\"\n variant=\"tertiary\"\n icon={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n aria-label={copied ? 'Copied' : 'Copy command'}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n />\n );\n}\n\n/**\n * Clipboard fallback for a host with no prompt URI (Copilot, Gemini). It\n * carries the identical instruction a launcher would pre-fill, so the only\n * difference is the delivery — the user pastes it into their own agent.\n */\nfunction CopyPromptButton({ text, label }: { text: string; label: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n >\n {copied ? 'Copied!' : `Copy prompt for ${label}`}\n </Button>\n );\n}\n\n/**\n * The two-step marketplace journey (ADR-0010): ① register the catalog with\n * the AI tool, ② install plugins from it — plus a team-distribution snippet.\n * Whether this can render at all is decided by the caller, which needs the\n * same answer to know if the body doc is still required as a fallback.\n */\nfunction MarketplaceJourney({\n resource,\n repoSlug,\n addCommands,\n}: {\n resource: ResourceSummary;\n repoSlug: string;\n addCommands: MarketplaceAddCommand[];\n}) {\n return (\n <div className={styles.journey}>\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 1. Add the marketplace to your AI tool\n </Text>\n <dl className={styles.commandList}>\n {addCommands.map(({ framework, command, deepLinks }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.commandCell}>\n <code>{command}</code>\n <CopyCommandButton text={command} />\n </dd>\n {deepLinks.length > 0 && (\n <dd className={styles.deepLinks}>\n {deepLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n >\n Add in {link.label}\n </ButtonLink>\n );\n })}\n </dd>\n )}\n </div>\n ))}\n </dl>\n\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 2. Install plugins from it\n </Text>\n <div className={styles.commandCell}>\n <code>{getMarketplaceInstallTemplate(resource.name)}</code>\n </div>\n\n <details className={styles.teamSection}>\n <summary>For teams: auto-install via project settings</summary>\n <Text variant=\"body-x-small\" as=\"p\">\n Commit this to your repository’s <code>.claude/settings.json</code>{' '}\n and collaborators are prompted to install the marketplace when they\n trust the folder.\n </Text>\n <pre className={styles.codeBlock}>\n <code>{getMarketplaceTeamSnippet(resource.name, repoSlug)}</code>\n </pre>\n <CopyCommandButton\n text={getMarketplaceTeamSnippet(resource.name, repoSlug)}\n />\n </details>\n </div>\n );\n}\n\n/**\n * Install guidance wired to the body resolver (issue #30): copy the body,\n * download the artifact (file or zip, ADR-0009), and per-framework install\n * paths. The body is canonical — nothing here is reconstructed from\n * annotations (CONTEXT.md: body).\n */\nexport function ResourceInstallDialog({\n resource,\n body,\n isOpen,\n onOpenChange,\n}: ResourceInstallDialogProps) {\n const api = useApi(devAiHubResourceApiRef);\n const [copied, setCopied] = useState(false);\n const [downloadError, setDownloadError] = useState(false);\n\n const shape = getBodyShape(resource.type);\n // The deployment allow-list removed every framework this resource declared.\n // An empty `frameworks` would otherwise fall back to the neutral `default`\n // path and every native launcher, re-exposing what the allow-list suppressed\n // — so skip all generated install content and say why. Copy/Download stay.\n const frameworksRestricted = !!resource.frameworksRestricted;\n // What install-path / launcher generation is scoped to: `installFrameworks`\n // when the backend inferred a scoped set (a bare resource under an active\n // allow-list), otherwise the declared-and-filtered `frameworks`. Badges\n // still read `frameworks` only.\n const installFrameworks = resource.installFrameworks ?? resource.frameworks;\n const installLinks = frameworksRestricted\n ? []\n : getInstallLinks(resource, installFrameworks, body);\n\n // A marketplace body doc repeats the generated journey almost verbatim —\n // same two steps, same commands — so rendering both doubles the dialog's\n // height to say one thing twice. The journey wins when it can be derived;\n // the body stays as ADR-0010's fallback for when the slug cannot be.\n const marketplaceRepoSlug =\n resource.type === 'marketplace'\n ? getMarketplaceRepoSlug(resource.sourceLocation)\n : undefined;\n const marketplaceAddCommands =\n marketplaceRepoSlug && !frameworksRestricted\n ? getMarketplaceAddCommands(installFrameworks, marketplaceRepoSlug)\n : [];\n const hasMarketplaceJourney = marketplaceAddCommands.length > 0;\n // plugin/marketplace bodies are the real manifest JSON (ADR-0014), so this\n // markdown fallback can only fire if shape is still 'markdown' — kept for\n // the (currently unreachable) case bodyShape ever becomes content-driven.\n const showBodyDoc =\n shape === 'markdown' &&\n (resource.type === 'plugin' ||\n (resource.type === 'marketplace' && !hasMarketplaceJourney));\n\n // skill/hook install by instructing the agent, so each host gets a row that\n // is actionable on its own: a launcher where the host has a prompt URI, a\n // copyable prompt where it does not. Other types keep the plain path list —\n // their launchers are native routes, rendered once in the actions row.\n const usesPromptInstall =\n resource.type === 'skill' || resource.type === 'hook';\n const installSteps =\n usesPromptInstall && !frameworksRestricted\n ? getInstallSteps(\n resource.type,\n resource.sourceLocation,\n resource.name,\n installFrameworks,\n )\n : [];\n const pathRows = frameworksRestricted\n ? []\n : expandInstallFrameworks(installFrameworks)\n .map(f => ({\n framework: f,\n target: getResourceInstallTarget(resource.type, f, resource.name),\n }))\n .filter(\n (row): row is { framework: string; target: ResourceInstallTarget } =>\n !!row.target,\n );\n const hasMergeTarget = (\n installSteps.length > 0\n ? installSteps.map(s => s.target)\n : pathRows.map(r => r.target)\n ).some(t => t.mode === 'merge');\n\n const handleCopy = async () => {\n if (!body) return;\n await navigator.clipboard.writeText(body.content);\n api.track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = async () => {\n setDownloadError(false);\n try {\n await api.downloadEntityBody(resource.entityRef);\n api.track(resource.entityRef, 'download');\n } catch {\n setDownloadError(true);\n }\n };\n\n return (\n <Dialog\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n /* Size to the widest command line so generated commands stay unwrapped,\n clamped so prose-heavy bodies can't balloon the dialog. The height cap\n keeps a long fallback body scrolling inside the dialog rather than\n running off-screen — the install actions must always stay reachable. */\n style={{\n width: 'fit-content',\n minWidth: 'min(480px, calc(100vw - 3rem))',\n maxWidth: 'min(760px, calc(100vw - 3rem))',\n maxHeight: 'min(80vh, calc(100vh - 4rem))',\n }}\n >\n <DialogHeader>Install {resource.title ?? resource.name}</DialogHeader>\n <DialogBody>\n <div className={styles.content}>\n <Text variant=\"body-small\" as=\"p\">\n {frameworksRestricted\n ? 'No install path is available — none of the tools this resource ' +\n 'supports are enabled for this Backstage. You can still copy or ' +\n 'download the content below.'\n : TYPE_HINTS[resource.type]}\n </Text>\n\n {body && shape === 'json' && (\n <CollapsibleSection label=\"Content\">\n <JsonBody content={body.content} className={styles.codeBlock} />\n </CollapsibleSection>\n )}\n\n {hasMarketplaceJourney && marketplaceRepoSlug && (\n <MarketplaceJourney\n resource={resource}\n repoSlug={marketplaceRepoSlug}\n addCommands={marketplaceAddCommands}\n />\n )}\n\n {body && showBodyDoc && (\n <div className={styles.markdown}>\n <ReactMarkdown>{body.content}</ReactMarkdown>\n </div>\n )}\n\n {installSteps.length > 0 && (\n <dl className={styles.commandList}>\n {installSteps.map(({ framework, target, prompt, link }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.pathRow}>\n <code>{target.path}</code>\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n <dd className={styles.deepLinks}>\n {link ? (\n (() => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n api.track(\n resource.entityRef,\n 'install',\n framework,\n )\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })()\n ) : (\n /* No prompt URI for this host — the same instruction,\n delivered by clipboard so the row is still actionable. */\n <CopyPromptButton\n text={prompt}\n label={frameworkLabel(framework)}\n />\n )}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {installSteps.length === 0 && pathRows.length > 0 && (\n <>\n <dl className={styles.pathList}>\n {pathRows.map(({ framework, target }) => (\n <div key={framework} className={styles.pathRow}>\n <dt>\n {framework === 'default'\n ? 'Any framework'\n : frameworkLabel(framework)}\n </dt>\n <dd>\n <code>{target.path}</code>\n {/* A merge target is a file the user already owns —\n flagged inline so the path is never mistaken for a\n drop-in destination to overwrite. */}\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n </div>\n ))}\n </dl>\n {hasMergeTarget && (\n <Text\n variant=\"body-x-small\"\n as=\"p\"\n className={styles.mergeNote}\n >\n Paths marked <strong>merge into</strong> are shared\n configuration files. Add this entry to the existing file —\n replacing it discards your other settings.\n </Text>\n )}\n </>\n )}\n\n <div className={styles.actions}>\n {installLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n >\n Install in {link.label}\n </ButtonLink>\n );\n })}\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy content'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n </div>\n {downloadError && (\n <Text variant=\"body-x-small\" as=\"p\">\n Download failed — the source may be unreachable. Try again or use\n “View source”.\n </Text>\n )}\n </div>\n </DialogBody>\n </Dialog>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResourceInstallDialog.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceInstallDialog.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Button,\n ButtonIcon,\n ButtonLink,\n Dialog,\n DialogBody,\n DialogHeader,\n Text,\n} from '@backstage/ui';\nimport {\n RiCheckLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiFileCopyLine,\n} from '@remixicon/react';\nimport { siClaude } from 'simple-icons';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n expandInstallFrameworks,\n getAgentInstallLinks,\n getBodyShape,\n getMarketplaceAddCommands,\n getMcpInstallLinks,\n getMarketplaceInstallTemplate,\n getMarketplaceRepoSlug,\n getMarketplaceTeamSnippet,\n getInstallSteps,\n getResourceInstallTarget,\n hasCopyableBody,\n hasDownloadableArtifact,\n type FrameworkToken,\n type MarketplaceAddCommand,\n type ResourceInstallTarget,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport {\n devAiHubResourceApiRef,\n type ResourceBody,\n} from '../../api/DevAiHubResourceClient';\nimport { useTrackTelemetry } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { AppLaunchScope } from './AppLaunchScope';\nimport { CollapsibleSection } from './CollapsibleSection';\nimport { JsonBody } from './JsonBody';\nimport { frameworkLabel } from './typeMeta';\nimport styles from './ResourceInstallDialog.module.css';\n\ninterface ResourceInstallDialogProps {\n resource: ResourceSummary;\n body?: ResourceBody;\n /**\n * Whether the body is still being fetched. The mcp-config launchers are\n * derived from it, so the dialog-open telemetry fallback below must not\n * conclude \"no launcher\" while it is still in flight.\n */\n bodyLoading?: boolean;\n isOpen: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst TYPE_HINTS: Record<ResourceSummary['type'], string> = {\n skill:\n 'Download the skill (multi-file skills arrive as one zip) and extract it into the path for your framework.',\n agent:\n 'Download or copy the agent definition into the path for your framework.',\n hook: 'Register the hook definition at the path for your framework — most hosts merge it into a shared settings file.',\n 'mcp-config': 'Add this server entry to your MCP configuration file.',\n plugin:\n 'This plugin installs through its framework — follow the instructions below.',\n marketplace:\n 'Register this marketplace with your AI tool once, then install any of its plugins from it.',\n};\n\n/** The VS Code chevron mark (devicon's mono path), for a button's iconStart slot. */\nfunction VsCodeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 128 128\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64 1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68 50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z\"\n />\n </svg>\n );\n}\n\n/** The Claude starburst, sized for a button's iconStart slot. */\nfunction ClaudeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path d={siClaude.path} />\n </svg>\n );\n}\n\n/**\n * The one-click install links for the types with a *native* install route,\n * rendered once in the actions row. `skill` and `hook` are absent on purpose:\n * their launchers are per-host and live in their own rows, so returning them\n * here too would render every button twice.\n */\nfunction getInstallLinks(\n resource: ResourceSummary,\n frameworks: string[],\n body?: ResourceBody,\n) {\n if (resource.type === 'agent') {\n return getAgentInstallLinks(\n resource.sourceLocation,\n resource.name,\n frameworks,\n );\n }\n if (resource.type === 'mcp-config') {\n return getMcpInstallLinks(frameworks, resource.name, body?.content);\n }\n return [];\n}\n\n/** Brand styling for the known deep-link hosts, keyed by link label. */\nfunction launchBrand(label: string): {\n className?: string;\n icon: React.ReactElement;\n} {\n switch (label) {\n case 'Claude':\n return { className: styles.claudeLaunch, icon: <ClaudeIcon /> };\n case 'VS Code':\n return { className: styles.vscodeLaunch, icon: <VsCodeIcon /> };\n case 'VS Code Insiders':\n return { className: styles.insidersLaunch, icon: <VsCodeIcon /> };\n case 'Cursor':\n return {\n className: styles.cursorLaunch,\n icon: <ToolIcon tool=\"cursor\" branded={false} size={16} />,\n };\n default:\n return { icon: <RiExternalLinkLine /> };\n }\n}\n\n/**\n * Icon-only copy button for a single generated command (marketplace journey\n * rows); flashes a check mark as the copied feedback.\n */\nfunction CopyCommandButton({ text }: { text: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <ButtonIcon\n size=\"small\"\n variant=\"tertiary\"\n icon={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n aria-label={copied ? 'Copied' : 'Copy command'}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n />\n );\n}\n\n/**\n * Clipboard fallback for a host with no prompt URI (Copilot, Gemini). It\n * carries the identical instruction a launcher would pre-fill, so the only\n * difference is the delivery — the user pastes it into their own agent.\n */\nfunction CopyPromptButton({ text, label }: { text: string; label: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n >\n {copied ? 'Copied!' : `Copy prompt for ${label}`}\n </Button>\n );\n}\n\n/**\n * The two-step marketplace journey (ADR-0010): ① register the catalog with\n * the AI tool, ② install plugins from it — plus a team-distribution snippet.\n * Whether this can render at all is decided by the caller, which needs the\n * same answer to know if the body doc is still required as a fallback.\n */\nfunction MarketplaceJourney({\n resource,\n repoSlug,\n addCommands,\n onLaunch,\n}: {\n resource: ResourceSummary;\n repoSlug: string;\n addCommands: MarketplaceAddCommand[];\n /**\n * Records the `install`, with the launched host as `tool` (ADR-0011).\n * `framework` is optional on the published `ResourceDeepLink`; a generated\n * link always carries one, and an event without a `tool` is still a\n * truthful install.\n */\n onLaunch: (framework: string | undefined) => void;\n}) {\n return (\n <div className={styles.journey}>\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 1. Add the marketplace to your AI tool\n </Text>\n <dl className={styles.commandList}>\n {addCommands.map(({ framework, command, deepLinks }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.commandCell}>\n <code>{command}</code>\n <CopyCommandButton text={command} />\n </dd>\n {deepLinks.length > 0 && (\n <dd className={styles.deepLinks}>\n {deepLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() => onLaunch(link.framework)}\n >\n Add in {link.label}\n </ButtonLink>\n );\n })}\n </dd>\n )}\n </div>\n ))}\n </dl>\n\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 2. Install plugins from it\n </Text>\n <div className={styles.commandCell}>\n <code>{getMarketplaceInstallTemplate(resource.name)}</code>\n </div>\n\n <details className={styles.teamSection}>\n <summary>For teams: auto-install via project settings</summary>\n <Text variant=\"body-x-small\" as=\"p\">\n Commit this to your repository’s <code>.claude/settings.json</code>{' '}\n and collaborators are prompted to install the marketplace when they\n trust the folder.\n </Text>\n <pre className={styles.codeBlock}>\n <code>{getMarketplaceTeamSnippet(resource.name, repoSlug)}</code>\n </pre>\n <CopyCommandButton\n text={getMarketplaceTeamSnippet(resource.name, repoSlug)}\n />\n </details>\n </div>\n );\n}\n\n/**\n * Install guidance wired to the body resolver (issue #30): copy the body,\n * download the artifact (file or zip, ADR-0009), and per-framework install\n * paths. The body is canonical — nothing here is reconstructed from\n * annotations (CONTEXT.md: body).\n */\nexport function ResourceInstallDialog({\n resource,\n body,\n bodyLoading = false,\n isOpen,\n onOpenChange,\n}: ResourceInstallDialogProps) {\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const [copied, setCopied] = useState(false);\n const [downloadError, setDownloadError] = useState(false);\n\n const shape = getBodyShape(resource.type);\n // The deployment allow-list removed every framework this resource declared.\n // An empty `frameworks` would otherwise fall back to the neutral `default`\n // path and every native launcher, re-exposing what the allow-list suppressed\n // — so skip all generated install content and say why. Copy/Download stay.\n const frameworksRestricted = !!resource.frameworksRestricted;\n // What install-path / launcher generation is scoped to: `installFrameworks`\n // when the backend inferred a scoped set (a bare resource under an active\n // allow-list), otherwise the declared-and-filtered `frameworks`. Badges\n // still read `frameworks` only.\n const installFrameworks = resource.installFrameworks ?? resource.frameworks;\n const installLinks = frameworksRestricted\n ? []\n : getInstallLinks(resource, installFrameworks, body);\n\n // A marketplace body doc repeats the generated journey almost verbatim —\n // same two steps, same commands — so rendering both doubles the dialog's\n // height to say one thing twice. The journey wins when it can be derived;\n // the body stays as ADR-0010's fallback for when the slug cannot be.\n const marketplaceRepoSlug =\n resource.type === 'marketplace'\n ? getMarketplaceRepoSlug(resource.sourceLocation)\n : undefined;\n const marketplaceAddCommands =\n marketplaceRepoSlug && !frameworksRestricted\n ? getMarketplaceAddCommands(installFrameworks, marketplaceRepoSlug)\n : [];\n const hasMarketplaceJourney = marketplaceAddCommands.length > 0;\n // plugin/marketplace bodies are the real manifest JSON (ADR-0014), so this\n // markdown fallback can only fire if shape is still 'markdown' — kept for\n // the (currently unreachable) case bodyShape ever becomes content-driven.\n const showBodyDoc =\n shape === 'markdown' &&\n (resource.type === 'plugin' ||\n (resource.type === 'marketplace' && !hasMarketplaceJourney));\n\n // skill/hook install by instructing the agent, so each host gets a row that\n // is actionable on its own: a launcher where the host has a prompt URI, a\n // copyable prompt where it does not. Other types keep the plain path list —\n // their launchers are native routes, rendered once in the actions row.\n const usesPromptInstall =\n resource.type === 'skill' || resource.type === 'hook';\n const installSteps =\n usesPromptInstall && !frameworksRestricted\n ? getInstallSteps(\n resource.type,\n resource.sourceLocation,\n resource.name,\n installFrameworks,\n )\n : [];\n const pathRows = frameworksRestricted\n ? []\n : expandInstallFrameworks(installFrameworks)\n .map(f => ({\n framework: f,\n target: getResourceInstallTarget(resource.type, f, resource.name),\n }))\n .filter(\n (row): row is { framework: string; target: ResourceInstallTarget } =>\n !!row.target,\n );\n const hasMergeTarget = (\n installSteps.length > 0\n ? installSteps.map(s => s.target)\n : pathRows.map(r => r.target)\n ).some(t => t.mode === 'merge');\n\n /**\n * Whether *this* dialog actually produced a launch button. Deliberately\n * per-resource, not per-type: a skill declaring only Copilot/Gemini, an\n * agent with no resolvable source URL, an unparseable or multi-server MCP\n * body, and a marketplace with no derivable repo slug all render a path and\n * no button, and ADR-0011 requires those to fall back to the dialog-open\n * signal rather than record nothing at all.\n */\n const hasLauncher =\n installLinks.length > 0 ||\n installSteps.some(step => step.link) ||\n marketplaceAddCommands.some(command => command.deepLinks.length > 0);\n\n /**\n * `mcp-config` alone derives its launchers from the body\n * (`getMcpInstallLinks`); every other type builds them from\n * `sourceLocation` and the declared frameworks. So only it may wait for the\n * fetch — for the rest the body is only ever Copy/Download material, and\n * waiting on it would drop the event entirely when the dialog is closed\n * before an unrelated request settles.\n */\n const awaitingMcpBody = resource.type === 'mcp-config' && bodyLoading;\n\n /**\n * The fallback arm of ADR-0011's \"record `install` at the most specific\n * point available\": with no launcher to wait for, opening the dialog is\n * that point.\n *\n * Skipped when the deployment allow-list left the resource with no install\n * path at all (post-dates this ADR): nothing is installable, so an\n * `install` event would claim something that cannot have happened.\n */\n useEffect(() => {\n if (!isOpen || frameworksRestricted || hasLauncher || awaitingMcpBody) {\n return;\n }\n track(resource.entityRef, 'install');\n // `awaitingMcpBody`, not the raw `bodyLoading`: for every other type the\n // body settling is unrelated to launcher availability, and depending on\n // it would re-run this effect after the event had already been recorded\n // — one install, counted twice.\n }, [\n isOpen,\n awaitingMcpBody,\n frameworksRestricted,\n hasLauncher,\n resource.entityRef,\n track,\n ]);\n\n const handleCopy = async () => {\n if (!body) return;\n await navigator.clipboard.writeText(body.content);\n track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = async () => {\n setDownloadError(false);\n try {\n await api.downloadEntityBody(resource.entityRef);\n track(resource.entityRef, 'download');\n } catch {\n setDownloadError(true);\n }\n };\n\n return (\n <Dialog\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n /* Size to the widest command line so generated commands stay unwrapped,\n clamped so prose-heavy bodies can't balloon the dialog. The height cap\n keeps a long fallback body scrolling inside the dialog rather than\n running off-screen — the install actions must always stay reachable. */\n style={{\n width: 'fit-content',\n minWidth: 'min(480px, calc(100vw - 3rem))',\n maxWidth: 'min(760px, calc(100vw - 3rem))',\n maxHeight: 'min(80vh, calc(100vh - 4rem))',\n }}\n >\n <DialogHeader>Install {resource.title ?? resource.name}</DialogHeader>\n <DialogBody>\n {/* Every launcher here is a custom-scheme deep link — keep the app's\n router from rewriting it (see AppLaunchScope). */}\n <AppLaunchScope>\n <div className={styles.content}>\n <Text variant=\"body-small\" as=\"p\">\n {frameworksRestricted\n ? 'No install path is available — none of the tools this resource ' +\n 'supports are enabled for this Backstage. You can still copy or ' +\n 'download the content below.'\n : TYPE_HINTS[resource.type]}\n </Text>\n\n {body && shape === 'json' && (\n <CollapsibleSection label=\"Content\">\n <JsonBody content={body.content} className={styles.codeBlock} />\n </CollapsibleSection>\n )}\n\n {hasMarketplaceJourney && marketplaceRepoSlug && (\n <MarketplaceJourney\n resource={resource}\n repoSlug={marketplaceRepoSlug}\n addCommands={marketplaceAddCommands}\n onLaunch={framework =>\n track(resource.entityRef, 'install', framework)\n }\n />\n )}\n\n {body && showBodyDoc && (\n <div className={styles.markdown}>\n <ReactMarkdown>{body.content}</ReactMarkdown>\n </div>\n )}\n\n {installSteps.length > 0 && (\n <dl className={styles.commandList}>\n {installSteps.map(({ framework, target, prompt, link }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.pathRow}>\n <code>{target.path}</code>\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n <dd className={styles.deepLinks}>\n {link ? (\n (() => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })()\n ) : (\n /* No prompt URI for this host — the same instruction,\n delivered by clipboard so the row is still actionable. */\n <CopyPromptButton\n text={prompt}\n label={frameworkLabel(framework)}\n />\n )}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {installSteps.length === 0 && pathRows.length > 0 && (\n <>\n <dl className={styles.pathList}>\n {pathRows.map(({ framework, target }) => (\n <div key={framework} className={styles.pathRow}>\n <dt>\n {framework === 'default'\n ? 'Any framework'\n : frameworkLabel(framework)}\n </dt>\n <dd>\n <code>{target.path}</code>\n {/* A merge target is a file the user already owns —\n flagged inline so the path is never mistaken for a\n drop-in destination to overwrite. */}\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n </div>\n ))}\n </dl>\n {hasMergeTarget && (\n <Text\n variant=\"body-x-small\"\n as=\"p\"\n className={styles.mergeNote}\n >\n Paths marked <strong>merge into</strong> are shared\n configuration files. Add this entry to the existing file —\n replacing it discards your other settings.\n </Text>\n )}\n </>\n )}\n\n <div className={styles.actions}>\n {installLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', link.framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })}\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy content'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n </div>\n {downloadError && (\n <Text variant=\"body-x-small\" as=\"p\">\n Download failed — the source may be unreachable. Try again or\n use “View source”.\n </Text>\n )}\n </div>\n </AppLaunchScope>\n </DialogBody>\n </Dialog>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -45,7 +45,10 @@ function Section({
45
45
  onOpen
46
46
  }) {
47
47
  return /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
48
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: label }),
48
+ /* @__PURE__ */ jsxs(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: [
49
+ label,
50
+ ":"
51
+ ] }),
49
52
  /* @__PURE__ */ jsx("div", { className: styles.rows, children: related.map((r) => /* @__PURE__ */ jsx(RelationshipRow, { related: r, onOpen }, r.entityRef)) })
50
53
  ] });
51
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceRelationships.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceRelationships.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiArrowRightSLine } from '@remixicon/react';\nimport {\n isRenderableContainment,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { getTypeMeta } from './typeMeta';\nimport styles from './ResourceRelationships.module.css';\n\ninterface ResourceRelationshipsProps {\n resource: ResourceSummary;\n /** Every resource the caller can see — used to resolve related refs. */\n items: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen: (entityRef: string) => void;\n}\n\n/**\n * Resolve declared refs against the caller-visible set and keep only pairings\n * the rendering convention allows (ADR-0015). A ref absent from `items`\n * (hidden or an unsupported type) or of the wrong type is silently dropped —\n * a section never leaks that an entity exists.\n */\nfunction resolve(\n refs: string[],\n byRef: Map<string, ResourceSummary>,\n keep: (other: ResourceSummary) => boolean,\n): ResourceSummary[] {\n const out: ResourceSummary[] = [];\n for (const ref of refs) {\n const found = byRef.get(ref);\n if (found && keep(found)) out.push(found);\n }\n return out;\n}\n\n/** One navigable relationship row: type icon, title, and the type label. */\nfunction RelationshipRow({\n related,\n onOpen,\n}: {\n related: ResourceSummary;\n onOpen: (entityRef: string) => void;\n}) {\n const meta = getTypeMeta(related.type);\n return (\n <button\n type=\"button\"\n className={styles.row}\n onClick={() => onOpen(related.entityRef)}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <span className={styles.rowIcon}>\n <meta.Icon size={16} />\n </span>\n <span className={styles.rowText}>\n <Text variant=\"body-small\" weight=\"bold\" className={styles.rowTitle}>\n {related.title ?? related.name}\n </Text>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {meta.label}\n </Text>\n </span>\n <RiArrowRightSLine size={16} className={styles.rowChevron} />\n </button>\n );\n}\n\nfunction Section({\n label,\n related,\n onOpen,\n}: {\n label: string;\n related: ResourceSummary[];\n onOpen: (entityRef: string) => void;\n}) {\n return (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n {label}\n </Text>\n <div className={styles.rows}>\n {related.map(r => (\n <RelationshipRow key={r.entityRef} related={r} onOpen={onOpen} />\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * The relationship sections of the detail panel (ADR-0015): \"Part of\" (the\n * containers this resource belongs to, upward) and, for containers, the\n * members it declares (downward). Renders nothing when neither resolves to a\n * visible, correctly-typed relation, so the panel never shows an empty\n * heading.\n */\nexport function ResourceRelationships({\n resource,\n items,\n onOpen,\n}: ResourceRelationshipsProps) {\n const byRef = new Map(items.map(i => [i.entityRef, i]));\n\n // A parent is kept only if it legitimately contains this resource's type.\n const parents = resolve(resource.parents, byRef, parent =>\n isRenderableContainment(parent.type, resource.type),\n );\n // A child is kept only if this resource's type legitimately contains it.\n const children = resolve(resource.children, byRef, child =>\n isRenderableContainment(resource.type, child.type),\n );\n\n const childrenLabel =\n resource.type === 'marketplace'\n ? 'Plugins in this marketplace'\n : 'Includes';\n\n if (parents.length === 0 && children.length === 0) return null;\n\n return (\n <>\n {parents.length > 0 && (\n <Section label=\"Part of\" related={parents} onOpen={onOpen} />\n )}\n {children.length > 0 && (\n <Section label={childrenLabel} related={children} onOpen={onOpen} />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResourceRelationships.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceRelationships.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiArrowRightSLine } from '@remixicon/react';\nimport {\n isRenderableContainment,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { getTypeMeta } from './typeMeta';\nimport styles from './ResourceRelationships.module.css';\n\ninterface ResourceRelationshipsProps {\n resource: ResourceSummary;\n /** Every resource the caller can see — used to resolve related refs. */\n items: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen: (entityRef: string) => void;\n}\n\n/**\n * Resolve declared refs against the caller-visible set and keep only pairings\n * the rendering convention allows (ADR-0015). A ref absent from `items`\n * (hidden or an unsupported type) or of the wrong type is silently dropped —\n * a section never leaks that an entity exists.\n */\nfunction resolve(\n refs: string[],\n byRef: Map<string, ResourceSummary>,\n keep: (other: ResourceSummary) => boolean,\n): ResourceSummary[] {\n const out: ResourceSummary[] = [];\n for (const ref of refs) {\n const found = byRef.get(ref);\n if (found && keep(found)) out.push(found);\n }\n return out;\n}\n\n/** One navigable relationship row: type icon, title, and the type label. */\nfunction RelationshipRow({\n related,\n onOpen,\n}: {\n related: ResourceSummary;\n onOpen: (entityRef: string) => void;\n}) {\n const meta = getTypeMeta(related.type);\n return (\n <button\n type=\"button\"\n className={styles.row}\n onClick={() => onOpen(related.entityRef)}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <span className={styles.rowIcon}>\n <meta.Icon size={16} />\n </span>\n <span className={styles.rowText}>\n <Text variant=\"body-small\" weight=\"bold\" className={styles.rowTitle}>\n {related.title ?? related.name}\n </Text>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {meta.label}\n </Text>\n </span>\n <RiArrowRightSLine size={16} className={styles.rowChevron} />\n </button>\n );\n}\n\nfunction Section({\n label,\n related,\n onOpen,\n}: {\n label: string;\n related: ResourceSummary[];\n onOpen: (entityRef: string) => void;\n}) {\n return (\n <div className={styles.section}>\n {/* Trailing colon matches the detail panel's other section captions\n (Description:, Metadata:, Works with:, Tags:). */}\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n {label}:\n </Text>\n <div className={styles.rows}>\n {related.map(r => (\n <RelationshipRow key={r.entityRef} related={r} onOpen={onOpen} />\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * The relationship sections of the detail panel (ADR-0015): \"Part of\" (the\n * containers this resource belongs to, upward) and, for containers, the\n * members it declares (downward). Renders nothing when neither resolves to a\n * visible, correctly-typed relation, so the panel never shows an empty\n * heading.\n */\nexport function ResourceRelationships({\n resource,\n items,\n onOpen,\n}: ResourceRelationshipsProps) {\n const byRef = new Map(items.map(i => [i.entityRef, i]));\n\n // A parent is kept only if it legitimately contains this resource's type.\n const parents = resolve(resource.parents, byRef, parent =>\n isRenderableContainment(parent.type, resource.type),\n );\n // A child is kept only if this resource's type legitimately contains it.\n const children = resolve(resource.children, byRef, child =>\n isRenderableContainment(resource.type, child.type),\n );\n\n const childrenLabel =\n resource.type === 'marketplace'\n ? 'Plugins in this marketplace'\n : 'Includes';\n\n if (parents.length === 0 && children.length === 0) return null;\n\n return (\n <>\n {parents.length > 0 && (\n <Section label=\"Part of\" related={parents} onOpen={onOpen} />\n )}\n {children.length > 0 && (\n <Section label={childrenLabel} related={children} onOpen={onOpen} />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ function countLabel(count, noun) {
2
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
3
+ }
4
+
5
+ export { countLabel };
6
+ //# sourceMappingURL=countLabel.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"countLabel.esm.js","sources":["../../../src/components/AiResourcesPage/countLabel.ts"],"sourcesContent":["/**\n * A telemetry count with its noun, singularised at one — \"1 view\", not the\n * \"1 views\" a template literal gives you. Shared by the drawer's header chips\n * and the card's count tooltips so the two cannot word it differently.\n *\n * Deliberately naive: every noun it is used with pluralises with a bare `s`.\n * A noun that pluralises any other way needs its own helper, not a special\n * case bolted on here.\n */\nexport function countLabel(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n"],"names":[],"mappings":"AASO,SAAS,UAAA,CAAW,OAAe,IAAA,EAAsB;AAC9D,EAAA,OAAO,CAAA,EAAG,KAAK,CAAA,CAAA,EAAI,IAAI,GAAG,KAAA,KAAU,CAAA,GAAI,KAAK,GAAG,CAAA,CAAA;AAClD;;;;"}
@@ -1,12 +1,37 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import { useApi } from '@backstage/core-plugin-api';
3
3
  import { devAiHubResourceApiRef } from '../api/DevAiHubResourceClient.esm.js';
4
4
 
5
+ const listeners = /* @__PURE__ */ new Map();
6
+ function notifyTelemetryChanged(entityRef) {
7
+ listeners.get(entityRef)?.forEach((listener) => listener());
8
+ }
9
+ function useTrackTelemetry() {
10
+ const api = useApi(devAiHubResourceApiRef);
11
+ return useCallback(
12
+ (entityRef, ...rest) => api.track(entityRef, ...rest).then(() => notifyTelemetryChanged(entityRef)),
13
+ [api]
14
+ );
15
+ }
5
16
  function useTelemetryCounts(entityRef) {
6
17
  const api = useApi(devAiHubResourceApiRef);
7
18
  const [counts, setCounts] = useState();
19
+ const [revision, setRevision] = useState(0);
8
20
  useEffect(() => {
9
21
  setCounts(void 0);
22
+ }, [entityRef]);
23
+ useEffect(() => {
24
+ if (!entityRef) return void 0;
25
+ const bump = () => setRevision((r) => r + 1);
26
+ const subscribers = listeners.get(entityRef) ?? /* @__PURE__ */ new Set();
27
+ subscribers.add(bump);
28
+ listeners.set(entityRef, subscribers);
29
+ return () => {
30
+ subscribers.delete(bump);
31
+ if (subscribers.size === 0) listeners.delete(entityRef);
32
+ };
33
+ }, [entityRef]);
34
+ useEffect(() => {
10
35
  if (!entityRef) return void 0;
11
36
  let cancelled = false;
12
37
  api.getInstallCount(entityRef).then((result) => {
@@ -16,9 +41,9 @@ function useTelemetryCounts(entityRef) {
16
41
  return () => {
17
42
  cancelled = true;
18
43
  };
19
- }, [api, entityRef]);
44
+ }, [api, entityRef, revision]);
20
45
  return counts;
21
46
  }
22
47
 
23
- export { useTelemetryCounts };
48
+ export { notifyTelemetryChanged, useTelemetryCounts, useTrackTelemetry };
24
49
  //# sourceMappingURL=useTelemetryCounts.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useTelemetryCounts.esm.js","sources":["../../src/hooks/useTelemetryCounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport type { TelemetryCounts } from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../api/DevAiHubResourceClient';\n\n/**\n * Raw per-action telemetry counts for a resource (ADR-0007). Fetch failures\n * are swallowed — a missing count is not worth surfacing as a UI error, the\n * card/detail just renders without it.\n */\nexport function useTelemetryCounts(\n entityRef: string | undefined,\n): TelemetryCounts | undefined {\n const api = useApi(devAiHubResourceApiRef);\n const [counts, setCounts] = useState<TelemetryCounts>();\n\n useEffect(() => {\n setCounts(undefined);\n if (!entityRef) return undefined;\n\n let cancelled = false;\n api\n .getInstallCount(entityRef)\n .then(result => {\n if (!cancelled) setCounts(result);\n })\n .catch(() => {\n // ignored — see above\n });\n return () => {\n cancelled = true;\n };\n }, [api, entityRef]);\n\n return counts;\n}\n"],"names":[],"mappings":";;;;AAUO,SAAS,mBACd,SAAA,EAC6B;AAC7B,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,QAAA,EAA0B;AAEtD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,SAAA,CAAU,MAAS,CAAA;AACnB,IAAA,IAAI,CAAC,WAAW,OAAO,MAAA;AAEvB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,GAAA,CACG,eAAA,CAAgB,SAAS,CAAA,CACzB,IAAA,CAAK,CAAA,MAAA,KAAU;AACd,MAAA,IAAI,CAAC,SAAA,EAAW,SAAA,CAAU,MAAM,CAAA;AAAA,IAClC,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAAA,IAEb,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,SAAS,CAAC,CAAA;AAEnB,EAAA,OAAO,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"useTelemetryCounts.esm.js","sources":["../../src/hooks/useTelemetryCounts.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport type {\n TelemetryAction,\n TelemetryCounts,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../api/DevAiHubResourceClient';\n\n/**\n * Subscribers per entityRef. Module-scoped: a resource's counts are shown by\n * more than one component at once — the grid card and, once it is open, the\n * detail drawer — and a write from either has to move both.\n */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Tell every mounted `useTelemetryCounts` for this resource to refetch.\n *\n * Call it once a telemetry write has landed, not before: counts are read back\n * from the server rather than incremented locally, so the number shown is\n * always the server's and a failed write simply leaves it where it was.\n */\nexport function notifyTelemetryChanged(entityRef: string): void {\n listeners.get(entityRef)?.forEach(listener => listener());\n}\n\n/**\n * Record a telemetry event and then move every count on screen for that\n * resource. Use this rather than `api.track` directly: it is the one place\n * the write is paired with the refetch, so a new call site cannot forget it\n * and leave a stale number behind.\n *\n * Still fire-and-forget from the caller's point of view — `track` swallows\n * its own failures (ADR-0007), and a write that never landed just leaves the\n * refetched count unchanged.\n */\nexport function useTrackTelemetry() {\n const api = useApi(devAiHubResourceApiRef);\n // Rest-spread rather than named params: forwarding `tool` explicitly would\n // pass an extra `undefined` on the calls that omit it, changing `track`'s\n // observed arity for no behavioural gain.\n return useCallback(\n (entityRef: string, ...rest: [action: TelemetryAction, tool?: string]) =>\n api\n .track(entityRef, ...rest)\n .then(() => notifyTelemetryChanged(entityRef)),\n [api],\n );\n}\n\n/**\n * Raw per-action telemetry counts for a resource (ADR-0007). Fetch failures\n * are swallowed — a missing count is not worth surfacing as a UI error, the\n * card/detail just renders without it.\n *\n * Refetches when `notifyTelemetryChanged` fires for the same resource, so\n * opening or installing a resource moves its count in place instead of\n * waiting for the next page load.\n */\nexport function useTelemetryCounts(\n entityRef: string | undefined,\n): TelemetryCounts | undefined {\n const api = useApi(devAiHubResourceApiRef);\n const [counts, setCounts] = useState<TelemetryCounts>();\n const [revision, setRevision] = useState(0);\n\n // Clear on resource change only. Deliberately not folded into the fetch\n // effect below: that one also runs per revision, and blanking there would\n // flash the count to nothing on every write.\n useEffect(() => {\n setCounts(undefined);\n }, [entityRef]);\n\n useEffect(() => {\n if (!entityRef) return undefined;\n\n const bump = () => setRevision(r => r + 1);\n const subscribers = listeners.get(entityRef) ?? new Set<() => void>();\n subscribers.add(bump);\n listeners.set(entityRef, subscribers);\n\n return () => {\n subscribers.delete(bump);\n if (subscribers.size === 0) listeners.delete(entityRef);\n };\n }, [entityRef]);\n\n useEffect(() => {\n if (!entityRef) return undefined;\n\n let cancelled = false;\n api\n .getInstallCount(entityRef)\n .then(result => {\n if (!cancelled) setCounts(result);\n })\n .catch(() => {\n // ignored — see above\n });\n return () => {\n cancelled = true;\n };\n }, [api, entityRef, revision]);\n\n return counts;\n}\n"],"names":[],"mappings":";;;;AAaA,MAAM,SAAA,uBAAgB,GAAA,EAA6B;AAS5C,SAAS,uBAAuB,SAAA,EAAyB;AAC9D,EAAA,SAAA,CAAU,IAAI,SAAS,CAAA,EAAG,OAAA,CAAQ,CAAA,QAAA,KAAY,UAAU,CAAA;AAC1D;AAYO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AAIzC,EAAA,OAAO,WAAA;AAAA,IACL,CAAC,SAAA,EAAA,GAAsB,IAAA,KACrB,GAAA,CACG,KAAA,CAAM,SAAA,EAAW,GAAG,IAAI,CAAA,CACxB,IAAA,CAAK,MAAM,sBAAA,CAAuB,SAAS,CAAC,CAAA;AAAA,IACjD,CAAC,GAAG;AAAA,GACN;AACF;AAWO,SAAS,mBACd,SAAA,EAC6B;AAC7B,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,QAAA,EAA0B;AACtD,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,SAAS,CAAC,CAAA;AAK1C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,SAAA,CAAU,MAAS,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,WAAW,OAAO,MAAA;AAEvB,IAAA,MAAM,IAAA,GAAO,MAAM,WAAA,CAAY,CAAA,CAAA,KAAK,IAAI,CAAC,CAAA;AACzC,IAAA,MAAM,cAAc,SAAA,CAAU,GAAA,CAAI,SAAS,CAAA,wBAAS,GAAA,EAAgB;AACpE,IAAA,WAAA,CAAY,IAAI,IAAI,CAAA;AACpB,IAAA,SAAA,CAAU,GAAA,CAAI,WAAW,WAAW,CAAA;AAEpC,IAAA,OAAO,MAAM;AACX,MAAA,WAAA,CAAY,OAAO,IAAI,CAAA;AACvB,MAAA,IAAI,WAAA,CAAY,IAAA,KAAS,CAAA,EAAG,SAAA,CAAU,OAAO,SAAS,CAAA;AAAA,IACxD,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,WAAW,OAAO,MAAA;AAEvB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,GAAA,CACG,eAAA,CAAgB,SAAS,CAAA,CACzB,IAAA,CAAK,CAAA,MAAA,KAAU;AACd,MAAA,IAAI,CAAC,SAAA,EAAW,SAAA,CAAU,MAAM,CAAA;AAAA,IAClC,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAAA,IAEb,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,SAAA,EAAW,QAAQ,CAAC,CAAA;AAE7B,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -13,7 +13,10 @@ const devAiHubPlugin = createFrontendPlugin({
13
13
  PageBlueprint.make({
14
14
  params: {
15
15
  path: "/dev-ai-hub",
16
- title: "Dev AI Hub",
16
+ // Display name only. The route, the `pluginId` and the backend
17
+ // discovery key stay `dev-ai-hub` — renaming those would break the
18
+ // mount path and the API the frontend resolves against.
19
+ title: "AI Hub",
17
20
  icon: /* @__PURE__ */ jsx(RiRobot3Fill, {}),
18
21
  routeRef: rootRouteRef
19
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import {\n createFrontendPlugin,\n PageBlueprint,\n SubPageBlueprint,\n ApiBlueprint,\n createApiFactory,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { RiRobot3Fill } from '@remixicon/react';\nimport {\n devAiHubResourceApiRef,\n DevAiHubResourceClient,\n} from './api/DevAiHubResourceClient';\nimport { rootRouteRef } from './routes';\n\nexport const devAiHubPlugin = createFrontendPlugin({\n pluginId: 'dev-ai-hub',\n routes: {\n root: rootRouteRef,\n },\n extensions: [\n PageBlueprint.make({\n params: {\n path: '/dev-ai-hub',\n title: 'Dev AI Hub',\n icon: <RiRobot3Fill />,\n routeRef: rootRouteRef,\n },\n }),\n ApiBlueprint.make({\n name: 'resources',\n params: defineParams =>\n defineParams(\n createApiFactory({\n api: devAiHubResourceApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new DevAiHubResourceClient(discoveryApi, fetchApi),\n }),\n ),\n }),\n SubPageBlueprint.make({\n name: 'browse',\n params: {\n path: 'browse',\n title: 'Browse',\n loader: () =>\n import('./components/AiResourcesPage').then(m => (\n <m.AiResourcesPage />\n )),\n },\n }),\n ],\n});\n"],"names":[],"mappings":";;;;;;AAgBO,MAAM,iBAAiB,oBAAA,CAAqB;AAAA,EACjD,QAAA,EAAU,YAAA;AAAA,EACV,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA,GACR;AAAA,EACA,UAAA,EAAY;AAAA,IACV,cAAc,IAAA,CAAK;AAAA,MACjB,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,aAAA;AAAA,QACN,KAAA,EAAO,YAAA;AAAA,QACP,IAAA,sBAAO,YAAA,EAAA,EAAa,CAAA;AAAA,QACpB,QAAA,EAAU;AAAA;AACZ,KACD,CAAA;AAAA,IACD,aAAa,IAAA,CAAK;AAAA,MAChB,IAAA,EAAM,WAAA;AAAA,MACN,QAAQ,CAAA,YAAA,KACN,YAAA;AAAA,QACE,gBAAA,CAAiB;AAAA,UACf,GAAA,EAAK,sBAAA;AAAA,UACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,UAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,UAAS,KACjC,IAAI,sBAAA,CAAuB,YAAA,EAAc,QAAQ;AAAA,SACpD;AAAA;AACH,KACH,CAAA;AAAA,IACD,iBAAiB,IAAA,CAAK;AAAA,MACpB,IAAA,EAAM,QAAA;AAAA,MACN,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO,QAAA;AAAA,QACP,MAAA,EAAQ,MACN,OAAO,2CAA8B,CAAA,CAAE,IAAA,CAAK,CAAA,CAAA,qBAC1C,GAAA,CAAC,CAAA,CAAE,eAAA,EAAF,EAAkB,CACpB;AAAA;AACL,KACD;AAAA;AAEL,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import {\n createFrontendPlugin,\n PageBlueprint,\n SubPageBlueprint,\n ApiBlueprint,\n createApiFactory,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { RiRobot3Fill } from '@remixicon/react';\nimport {\n devAiHubResourceApiRef,\n DevAiHubResourceClient,\n} from './api/DevAiHubResourceClient';\nimport { rootRouteRef } from './routes';\n\nexport const devAiHubPlugin = createFrontendPlugin({\n pluginId: 'dev-ai-hub',\n routes: {\n root: rootRouteRef,\n },\n extensions: [\n PageBlueprint.make({\n params: {\n path: '/dev-ai-hub',\n // Display name only. The route, the `pluginId` and the backend\n // discovery key stay `dev-ai-hub` — renaming those would break the\n // mount path and the API the frontend resolves against.\n title: 'AI Hub',\n icon: <RiRobot3Fill />,\n routeRef: rootRouteRef,\n },\n }),\n ApiBlueprint.make({\n name: 'resources',\n params: defineParams =>\n defineParams(\n createApiFactory({\n api: devAiHubResourceApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new DevAiHubResourceClient(discoveryApi, fetchApi),\n }),\n ),\n }),\n SubPageBlueprint.make({\n name: 'browse',\n params: {\n path: 'browse',\n title: 'Browse',\n loader: () =>\n import('./components/AiResourcesPage').then(m => (\n <m.AiResourcesPage />\n )),\n },\n }),\n ],\n});\n"],"names":[],"mappings":";;;;;;AAgBO,MAAM,iBAAiB,oBAAA,CAAqB;AAAA,EACjD,QAAA,EAAU,YAAA;AAAA,EACV,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA,GACR;AAAA,EACA,UAAA,EAAY;AAAA,IACV,cAAc,IAAA,CAAK;AAAA,MACjB,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,aAAA;AAAA;AAAA;AAAA;AAAA,QAIN,KAAA,EAAO,QAAA;AAAA,QACP,IAAA,sBAAO,YAAA,EAAA,EAAa,CAAA;AAAA,QACpB,QAAA,EAAU;AAAA;AACZ,KACD,CAAA;AAAA,IACD,aAAa,IAAA,CAAK;AAAA,MAChB,IAAA,EAAM,WAAA;AAAA,MACN,QAAQ,CAAA,YAAA,KACN,YAAA;AAAA,QACE,gBAAA,CAAiB;AAAA,UACf,GAAA,EAAK,sBAAA;AAAA,UACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,UAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,UAAS,KACjC,IAAI,sBAAA,CAAuB,YAAA,EAAc,QAAQ;AAAA,SACpD;AAAA;AACH,KACH,CAAA;AAAA,IACD,iBAAiB,IAAA,CAAK;AAAA,MACpB,IAAA,EAAM,QAAA;AAAA,MACN,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO,QAAA;AAAA,QACP,MAAA,EAAQ,MACN,OAAO,2CAA8B,CAAA,CAAE,IAAA,CAAK,CAAA,CAAA,qBAC1C,GAAA,CAAC,CAAA,CAAE,eAAA,EAAF,EAAkB,CACpB;AAAA;AACL,KACD;AAAA;AAEL,CAAC;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/plugin-dev-ai-hub",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Frontend plugin for Dev AI Hub",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.esm.js",
@@ -62,8 +62,8 @@
62
62
  "@backstage/frontend-plugin-api": "^0.18.0",
63
63
  "@backstage/plugin-catalog-react": "^3.2.2",
64
64
  "@backstage/ui": "^0.17.1",
65
- "@nospt/plugin-dev-ai-hub-common": "^1.1.1",
66
- "@remixicon/react": "^4.6.0",
65
+ "@nospt/plugin-dev-ai-hub-common": "^1.1.3",
66
+ "@remixicon/react": "^4.9.0",
67
67
  "react-markdown": "^9.0.0",
68
68
  "react-syntax-highlighter": "^16.1.1",
69
69
  "remark-gfm": "^4.0.1",