@nospt/plugin-dev-ai-hub 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,6 +58,20 @@ The sidebar item and the `/dev-ai-hub` route register themselves.
58
58
 
59
59
  All three are covered in the [repository README](https://github.com/nosportugal/backstage-plugin-dev-ai-hub#readme), which is the single place kept current.
60
60
 
61
+ ## Configuration
62
+
63
+ ```yaml
64
+ devAiHub:
65
+ ui:
66
+ # Optional. Width of the resource detail drawer, in pixels. The drawer is
67
+ # an overlay, so this never reflows the page; it is capped at the viewport
68
+ # width and floored at a sane minimum. Defaults to 615 when unset.
69
+ detailPanelWidth: 615
70
+ ```
71
+
72
+ The framework allow-list (`devAiHub.frameworks.allowed`) is enforced by the
73
+ backend plugin — see its README.
74
+
61
75
  ## Troubleshooting
62
76
 
63
77
  | Symptom | Usually means |
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "devAiHub": {
6
+ "type": "object",
7
+ "properties": {
8
+ "ui": {
9
+ "type": "object",
10
+ "properties": {
11
+ "detailPanelWidth": {
12
+ "type": "number",
13
+ "description": "Width of the resource detail drawer, in pixels. The drawer is an overlay, so this never reflows the page; it is capped at the viewport width and floored at a sane minimum. Defaults to 615 when unset.",
14
+ "visibility": "frontend"
15
+ }
16
+ },
17
+ "description": "Presentation settings for the Dev AI Hub frontend."
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
@@ -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":";;;;;;;;AA+BA,MAAM,gBAAA,GAAmB,CAAA;AACzB,MAAM,iBAAA,GAAoB,CAAA;AAM1B,SAAS,cAAc,QAAA,EAA+C;AACpE,EAAA,MAAM,KAAA,GAAQ,SAAS,UAAA,IAAc,CAAA;AACrC,EAAA,IAAI,KAAA,IAAS,GAAG,OAAO,MAAA;AACvB,EAAA,IAAI,SAAS,IAAA,KAAS,aAAA;AACpB,IAAA,OAAO,GAAG,KAAK,CAAA,CAAA,EAAI,KAAA,KAAU,CAAA,GAAI,WAAW,SAAS,CAAA,CAAA;AACvD,EAAA,IAAI,SAAS,IAAA,KAAS,QAAA;AACpB,IAAA,OAAO,GAAG,KAAK,CAAA,CAAA,EAAI,KAAA,KAAU,CAAA,GAAI,SAAS,OAAO,CAAA,CAAA;AACnD,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,YAAA,CAAa;AAAA,EAC3B,QAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAAsB;AACpB,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,QAAA,CAAS,IAAI,CAAA;AACtC,EAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,QAAA,CAAS,SAAS,CAAA;AAEpD,EAAA,MAAM,WAAA,GAAc,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,GAAG,gBAAgB,CAAA;AAC3D,EAAA,MAAM,cAAA,GAAiB,QAAA,CAAS,IAAA,CAAK,MAAA,GAAS,WAAA,CAAY,MAAA;AAC1D,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,KAAA,EAAO,OAAA,CAAQ,8BAA8B,EAAE,CAAA;AAC3E,EAAA,MAAM,WAAA,GAAc,QAAA,CAAS,OAAA,EAAS,MAAA,IAAU,CAAA;AAEhD,EAAA,uBACE,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,YAAY,CAAA,EAAG,MAAA,CAAO,IAAI,CAAA,CAAA,EAAI,SAAS,KAAK,MAAA,CAAO,IAAA;AAAA,MAC9D,KAAA,EACE;AAAA,QACE,iBAAiB,IAAA,CAAK,KAAA;AAAA,QACtB,oBAAoB,IAAA,CAAK,OAAA;AAAA,QACzB,GAAG;AAAA,OACL;AAAA,MAEF,OAAA,EAAS,MAAM,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA;AAAA,MACxC,WAAW,CAAA,CAAA,KAAK;AACd,QAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,EAAS,MAAA,CAAO,SAAS,SAAS,CAAA;AAAA,MAClD,CAAA;AAAA,MACA,IAAA,EAAK,QAAA;AAAA,MACL,QAAA,EAAU,CAAA;AAAA,MACV,YAAA,EAAY,CAAA,KAAA,EAAQ,QAAA,CAAS,KAAA,IAAS,SAAS,IAAI,CAAA,CAAA;AAAA,MAEnD,QAAA,EAAA;AAAA,wBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACrB,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,KAAK,IAAA,EAAL,EAAU,IAAA,EAAM,EAAA,EAAI,CAAA,EACvB,CAAA;AAAA,0BACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,UAAA,EAKrB,QAAA,EAAA;AAAA,4BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,WAAW,MAAA,CAAO,SAAA,EAC5C,eAAK,KAAA,EACR,CAAA;AAAA,4BACA,GAAA;AAAA,cAAC,IAAA;AAAA,cAAA;AAAA,gBACC,OAAA,EAAQ,eAAA;AAAA,gBACR,MAAA,EAAO,MAAA;AAAA,gBACP,EAAA,EAAG,IAAA;AAAA,gBACH,WAAW,MAAA,CAAO,KAAA;AAAA,gBAEjB,QAAA,EAAA,QAAA,CAAS,SAAS,QAAA,CAAS;AAAA;AAAA;AAC9B,WAAA,EACF;AAAA,SAAA,EACF,CAAA;AAAA,wBAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,IAAA,EACpB,QAAA,EAAA;AAAA,UAAA,QAAA,CAAS,UAAA,CAAW,MAAA,GAAS,CAAA,oBAC5B,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EAOpB,QAAA,EAAA,QAAA,CAAS,UAAA,CAAW,GAAA,CAAI,CAAA,CAAA,qBACvB,GAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cAEC,WAAW,MAAA,CAAO,cAAA;AAAA,cAClB,KAAA,EAAO,eAAe,CAAC,CAAA;AAAA,cAEvB,QAAA,kBAAA,GAAA,CAAC,QAAA,EAAA,EAAS,IAAA,EAAM,CAAA,EAAqB,MAAM,EAAA,EAAI;AAAA,aAAA;AAAA,YAJ1C;AAAA,WAMR,CAAA,EACH,CAAA;AAAA,UAGD,QAAA,CAAS,KAAK,MAAA,GAAS,CAAA,yBACrB,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,IAAA,EACpB,QAAA,EAAA;AAAA,YAAA,WAAA,CAAY,IAAI,CAAA,CAAA,qBACf,IAAA,CAAC,MAAA,EAAA,EAAa,SAAA,EAAW,OAAO,GAAA,EAAK,QAAA,EAAA;AAAA,cAAA,GAAA;AAAA,cACjC;AAAA,aAAA,EAAA,EADO,CAEX,CACD,CAAA;AAAA,YACA,iBAAiB,CAAA,oBAChB,IAAA;AAAA,cAAC,MAAA;AAAA,cAAA;AAAA,gBACC,WAAW,MAAA,CAAO,GAAA;AAAA,gBAClB,OAAO,QAAA,CAAS,IAAA,CAAK,MAAM,gBAAgB,CAAA,CAAE,KAAK,IAAI,CAAA;AAAA,gBACvD,QAAA,EAAA;AAAA,kBAAA,GAAA;AAAA,kBACG;AAAA;AAAA;AAAA;AACJ,WAAA,EAEJ,CAAA;AAAA,UAAA,CAGA,aAAA,CAAc,QAAQ,CAAA,IAAK,WAAA,GAAc,sBACzC,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACpB,QAAA,EAAA;AAAA,YAAA,aAAA,CAAc,QAAQ,CAAA,oBACrB,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,OAAA,EAAS,KAAA,EAAM,UAAA,EACpC,QAAA,EAAA,aAAA,CAAc,QAAQ,CAAA,EACzB,CAAA;AAAA,YAED,WAAA,GAAc,qBACb,IAAA,CAAC,MAAA,EAAA,EAAK,WAAW,MAAA,CAAO,OAAA,EAAS,OAAM,wBAAA,EAAyB,QAAA,EAAA;AAAA,cAAA,UAAA;AAAA,cACrD;AAAA,aAAA,EACX;AAAA,WAAA,EAEJ,CAAA;AAAA,UAAA,CAGA,QAAA,CAAS,OAAA,IACT,UAAA,IACA,CAAC,CAAC,MAAA,EAAQ,OAAA,IACV,CAAC,CAAC,QAAQ,IAAA,qBACV,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,MAAA,EACrB,QAAA,EAAA;AAAA,4BAAA,GAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,cAAA,EAAe,KAAA,EAAM,WAAA,EAChC,QAAA,EAAA;AAAA,cACC,QAAA,CAAS,WAAW,CAAA,CAAA,EAAI,QAAA,CAAS,QAAQ,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,CAAA;AAAA,cAC1D;AAAA,cAEC,MAAA,CAAO,OAAO,CAAA,CACd,IAAA,CAAK,QAAK,CAAA,EACf,CAAA;AAAA,4BAIA,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,SAAA,EACrB,QAAA,EAAA;AAAA,cAAA,CAAC,CAAC,MAAA,EAAQ,IAAA,oBACT,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,KAAA,EAAO,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,IAAI,CAAA,MAAA,CAAA,EAClD,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,SAAA,EAAA,EAAU,MAAM,EAAA,EAAI,CAAA;AAAA,gBACpB,MAAA,CAAO;AAAA,eAAA,EACV,CAAA;AAAA,cAED,CAAC,CAAC,MAAA,EAAQ,OAAA,oBACT,IAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,WACE,MAAA,CAAO,OAAA,IAAW,iBAAA,GACd,MAAA,CAAO,eACP,MAAA,CAAO,KAAA;AAAA,kBAEb,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,OAAO,CAAA,SAAA,CAAA;AAAA,kBAEvB,QAAA,EAAA;AAAA,oBAAA,MAAA,CAAO,WAAW,iBAAA,GACjB,WAAA,mBAEA,GAAA,CAAC,cAAA,EAAA,EAAe,MAAM,EAAA,EAAI,CAAA;AAAA,oBAE3B,MAAA,CAAO;AAAA;AAAA;AAAA;AACV,aAAA,EAEJ;AAAA,WAAA,EACF;AAAA,SAAA,EAEJ;AAAA;AAAA;AAAA,GACF;AAEJ;;;;"}
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 };
@@ -1,24 +1,25 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { useState, useEffect } from 'react';
3
3
  import ReactMarkdown from 'react-markdown';
4
- import { Text, Skeleton, Flex, Button, Box, ButtonIcon, Link } from '@backstage/ui';
5
- import { RiCloseLine, RiEyeLine, RiInstallLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
6
- import { useApi } from '@backstage/core-plugin-api';
4
+ import { Skeleton, Flex, Box, Text, ButtonIcon, Button, Link } from '@backstage/ui';
5
+ import { RiPriceTag3Line, RiEyeLine, RiInstallLine, RiCloseLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
6
+ import { useApi, configApiRef } from '@backstage/core-plugin-api';
7
7
  import { EntityRefLink } from '@backstage/plugin-catalog-react';
8
- import { getBodyShape, hasCopyableBody, hasDownloadableArtifact, isParseableEntityRef } 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';
20
19
 
21
20
  const ENTITY_REF_ROWS = /* @__PURE__ */ new Set(["Owner", "Entity ref"]);
21
+ const DEFAULT_PANEL_WIDTH = 615;
22
+ const MIN_PANEL_WIDTH = 320;
22
23
  function ResourceDetailPanel({
23
24
  resource: resourceProp,
24
25
  onClose,
@@ -31,9 +32,15 @@ function ResourceDetailPanel({
31
32
  if (resourceProp) setDisplayResource(resourceProp);
32
33
  }, [resourceProp]);
33
34
  const api = useApi(devAiHubResourceApiRef);
35
+ const track = useTrackTelemetry();
36
+ const config = useApi(configApiRef);
37
+ const panelWidth = Math.max(
38
+ MIN_PANEL_WIDTH,
39
+ config.getOptionalNumber("devAiHub.ui.detailPanelWidth") ?? DEFAULT_PANEL_WIDTH
40
+ );
34
41
  const openedRef = resourceProp?.entityRef;
35
42
  useEffect(() => {
36
- if (openedRef) api.track(openedRef, "view");
43
+ if (openedRef) track(openedRef, "view");
37
44
  }, [openedRef]);
38
45
  const actionable = !!displayResource?.sourceLocation;
39
46
  const bodyState = useResourceBody(displayResource?.entityRef, actionable);
@@ -43,50 +50,39 @@ function ResourceDetailPanel({
43
50
  if (!displayResource) return null;
44
51
  const resource = displayResource;
45
52
  const meta = getTypeMeta(resource.type);
46
- const bodyShape = getBodyShape(resource.type);
47
53
  const handleCopy = async () => {
48
54
  if (!bodyState.body) return;
49
55
  await navigator.clipboard.writeText(bodyState.body.content);
50
- api.track(resource.entityRef, "copy");
56
+ track(resource.entityRef, "copy");
51
57
  setCopied(true);
52
58
  setTimeout(() => setCopied(false), 2e3);
53
59
  };
54
60
  const handleDownload = () => {
55
- api.track(resource.entityRef, "download");
61
+ track(resource.entityRef, "download");
56
62
  return api.downloadEntityBody(resource.entityRef);
57
63
  };
58
- const handleInstall = () => {
59
- api.track(resource.entityRef, "install");
60
- setInstallOpen(true);
61
- };
64
+ const handleInstall = () => setInstallOpen(true);
62
65
  const metadataRows = [
63
- ["Type", meta.label],
64
- ["Lifecycle", resource.lifecycle || void 0],
65
66
  ["Owner", resource.owner],
66
- ["Version", resource.version],
67
67
  ["Entity ref", resource.entityRef]
68
68
  ];
69
- const collapsibleContent = bodyShape === "json";
70
- const contentInner = /* @__PURE__ */ jsxs(Fragment, { children: [
69
+ const bodyProblem = /* @__PURE__ */ jsxs(Fragment, { children: [
71
70
  !actionable && /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", color: "secondary", children: "No content location published for this resource." }),
72
- actionable && bodyState.loading && /* @__PURE__ */ jsxs("div", { className: styles.bodyLoading, children: [
73
- /* @__PURE__ */ jsx(Skeleton, { width: "100%", height: 14 }),
74
- /* @__PURE__ */ jsx(Skeleton, { width: "85%", height: 14 }),
75
- /* @__PURE__ */ jsx(Skeleton, { width: "60%", height: 14 })
76
- ] }),
77
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." }),
78
72
  actionable && bodyState.error === "upstream" && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "2", children: [
79
73
  /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", color: "secondary", children: "Couldn\u2019t fetch the content from its source." }),
80
74
  /* @__PURE__ */ jsx(Button, { size: "small", variant: "tertiary", onPress: bodyState.retry, children: "Retry" })
75
+ ] })
76
+ ] });
77
+ const hasBodyProblem = !actionable || bodyState.error === "not-found" || bodyState.error === "upstream";
78
+ const contentInner = /* @__PURE__ */ jsxs(Fragment, { children: [
79
+ bodyProblem,
80
+ actionable && bodyState.loading && /* @__PURE__ */ jsxs("div", { className: styles.bodyLoading, children: [
81
+ /* @__PURE__ */ jsx(Skeleton, { width: "100%", height: 14 }),
82
+ /* @__PURE__ */ jsx(Skeleton, { width: "85%", height: 14 }),
83
+ /* @__PURE__ */ jsx(Skeleton, { width: "60%", height: 14 })
81
84
  ] }),
82
- bodyState.body && bodyShape === "json" && /* @__PURE__ */ jsx(
83
- JsonBody,
84
- {
85
- content: bodyState.body.content,
86
- className: styles.codeBlock
87
- }
88
- ),
89
- 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) }) })
90
86
  ] });
91
87
  return /* @__PURE__ */ jsxs(Fragment, { children: [
92
88
  /* @__PURE__ */ jsx(
@@ -104,7 +100,10 @@ function ResourceDetailPanel({
104
100
  {
105
101
  className: styles.drawer,
106
102
  "data-open": isOpen,
107
- style: { "--drawer-accent": meta.color },
103
+ style: {
104
+ "--drawer-accent": meta.color,
105
+ "--drawer-width": `${panelWidth}px`
106
+ },
108
107
  role: "dialog",
109
108
  "aria-label": resource.title ?? resource.name,
110
109
  "aria-hidden": !isOpen,
@@ -127,10 +126,28 @@ function ResourceDetailPanel({
127
126
  Text,
128
127
  {
129
128
  variant: "body-x-small",
130
- style: { color: meta.color, fontWeight: 600 },
129
+ className: styles.typeLabel,
130
+ style: { color: meta.color },
131
131
  children: meta.label
132
132
  }
133
- )
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
+ ] })
134
151
  ] }),
135
152
  /* @__PURE__ */ jsx(
136
153
  ButtonIcon,
@@ -143,20 +160,23 @@ function ResourceDetailPanel({
143
160
  )
144
161
  ] }),
145
162
  /* @__PURE__ */ jsxs("div", { className: styles.body, children: [
146
- resource.description && /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", children: resource.description }),
147
- (!!counts?.view || !!counts?.install) && /* @__PURE__ */ jsxs("div", { className: styles.statsRow, children: [
148
- !!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.statItem, children: [
149
- /* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
150
- " ",
151
- counts.view,
152
- " views"
153
- ] }),
154
- !!counts?.install && /* @__PURE__ */ jsxs("span", { className: styles.statItem, children: [
155
- /* @__PURE__ */ jsx(RiInstallLine, { size: 14 }),
156
- " ",
157
- counts.install,
158
- " installs"
159
- ] })
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
+ ] }),
167
+ /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
168
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Metadata:" }),
169
+ /* @__PURE__ */ jsx("dl", { className: styles.metaList, children: metadataRows.filter(([, v]) => v).map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
170
+ /* @__PURE__ */ jsx("dt", { children: k }),
171
+ /* @__PURE__ */ jsx("dd", { children: ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v) ? /* @__PURE__ */ jsx(EntityRefLink, { entityRef: v, children: v }) : v })
172
+ ] }, k)) })
173
+ ] }),
174
+ resource.frameworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
175
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Works with:" }),
176
+ /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.frameworks.map((f) => /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
177
+ /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 14 }),
178
+ frameworkLabel(f)
179
+ ] }, f)) })
160
180
  ] }),
161
181
  actionable && /* @__PURE__ */ jsxs("div", { className: styles.actions, children: [
162
182
  hasCopyableBody(resource.type) && /* @__PURE__ */ jsx(
@@ -199,46 +219,18 @@ function ResourceDetailPanel({
199
219
  onOpen
200
220
  }
201
221
  ),
202
- /* @__PURE__ */ jsx("div", { className: styles.section, children: collapsibleContent ? (
203
- // Keyed on entityRef: the drawer stays mounted across a
204
- // relationship navigation (only `displayResource` swaps), so
205
- // without a key React reuses this instance and carries a
206
- // collapsed choice from the previous resource onto the next
207
- // one, silently defeating "expanded by default".
208
- /* @__PURE__ */ jsx(
209
- CollapsibleSection,
210
- {
211
- label: "Content",
212
- defaultOpen: true,
213
- children: contentInner
214
- },
215
- resource.entityRef
216
- )
217
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
218
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Content" }),
219
- contentInner
220
- ] }) }),
221
- resource.frameworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
222
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Works with" }),
223
- /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.frameworks.map((f) => /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
224
- /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 14 }),
225
- frameworkLabel(f)
226
- ] }, f)) })
227
- ] }),
228
222
  resource.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
229
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Tags" }),
230
- /* @__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: [
231
225
  "#",
232
226
  t
233
227
  ] }, t)) })
234
228
  ] }),
235
- /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
236
- /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Metadata" }),
237
- /* @__PURE__ */ jsx("dl", { className: styles.metaList, children: metadataRows.filter(([, v]) => v).map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
238
- /* @__PURE__ */ jsx("dt", { children: k }),
239
- /* @__PURE__ */ jsx("dd", { children: ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v) ? /* @__PURE__ */ jsx(EntityRefLink, { entityRef: v, children: v }) : v })
240
- ] }, k)) })
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
241
232
  ] }),
233
+ !showsContentSection(resource.type) && hasBodyProblem && /* @__PURE__ */ jsx("div", { className: styles.section, children: bodyProblem }),
242
234
  resource.sourceLocation && /* @__PURE__ */ jsx("div", { className: styles.section, children: /* @__PURE__ */ jsx(
243
235
  Link,
244
236
  {
@@ -260,6 +252,7 @@ function ResourceDetailPanel({
260
252
  {
261
253
  resource,
262
254
  body: bodyState.body,
255
+ bodyLoading: bodyState.loading,
263
256
  isOpen: installOpen,
264
257
  onOpenChange: setInstallOpen
265
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 { useApi } from '@backstage/core-plugin-api';\nimport { EntityRefLink } from '@backstage/plugin-catalog-react';\nimport {\n getBodyShape,\n hasCopyableBody,\n hasDownloadableArtifact,\n isParseableEntityRef,\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.\n */\nconst ENTITY_REF_ROWS = new Set(['Owner', 'Entity ref']);\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\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 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 const contentInner = (\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.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 {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 {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={{ '--drawer-accent': meta.color } as React.CSSProperties}\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 {(!!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 <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 {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 {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 <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.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":";;;;;;;;;;;;;;;;;;;AA+DA,MAAM,kCAAkB,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,YAAY,CAAC,CAAA;AAGvD,MAAM,mBAAA,GAAsB,GAAA;AAE5B,MAAM,eAAA,GAAkB,GAAA;AAQjB,SAAS,mBAAA,CAAoB;AAAA,EAClC,QAAA,EAAU,YAAA;AAAA,EACV,OAAA;AAAA,EACA,QAAQ,EAAC;AAAA,EACT;AACF,CAAA,EAA6B;AAC3B,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,YAAA;AAOjB,EAAA,MAAM,CAAC,eAAA,EAAiB,kBAAkB,CAAA,GAAI,SAAS,YAAY,CAAA;AACnE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,YAAA,qBAAiC,YAAY,CAAA;AAAA,EACnD,CAAA,EAAG,CAAC,YAAY,CAAC,CAAA;AAEjB,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,QAAQ,iBAAA,EAAkB;AAChC,EAAA,MAAM,MAAA,GAAS,OAAO,YAAY,CAAA;AAClC,EAAA,MAAM,aAAa,IAAA,CAAK,GAAA;AAAA,IACtB,eAAA;AAAA,IACA,MAAA,CAAO,iBAAA,CAAkB,8BAA8B,CAAA,IACrD;AAAA,GACJ;AAOA,EAAA,MAAM,YAAY,YAAA,EAAc,SAAA;AAChC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAA,EAAW,KAAA,CAAM,SAAA,EAAW,MAAM,CAAA;AAAA,EAExC,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAA,MAAM,UAAA,GAAa,CAAC,CAAC,eAAA,EAAiB,cAAA;AACtC,EAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,eAAA,EAAiB,SAAA,EAAW,UAAU,CAAA;AACxE,EAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,eAAA,EAAiB,SAAS,CAAA;AAC5D,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAS,KAAK,CAAA;AACpD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAS,KAAK,CAAA;AAE1C,EAAA,IAAI,CAAC,iBAAiB,OAAO,IAAA;AAE7B,EAAA,MAAM,QAAA,GAAW,eAAA;AACjB,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,QAAA,CAAS,IAAI,CAAA;AAEtC,EAAA,MAAM,aAAa,YAAY;AAC7B,IAAA,IAAI,CAAC,UAAU,IAAA,EAAM;AACrB,IAAA,MAAM,SAAA,CAAU,SAAA,CAAU,SAAA,CAAU,SAAA,CAAU,KAAK,OAAO,CAAA;AAC1D,IAAA,KAAA,CAAM,QAAA,CAAS,WAAW,MAAM,CAAA;AAChC,IAAA,SAAA,CAAU,IAAI,CAAA;AACd,IAAA,UAAA,CAAW,MAAM,SAAA,CAAU,KAAK,CAAA,EAAG,GAAI,CAAA;AAAA,EACzC,CAAA;AAEA,EAAA,MAAM,iBAAiB,MAAM;AAC3B,IAAA,KAAA,CAAM,QAAA,CAAS,WAAW,UAAU,CAAA;AACpC,IAAA,OAAO,GAAA,CAAI,kBAAA,CAAmB,QAAA,CAAS,SAAS,CAAA;AAAA,EAClD,CAAA;AAIA,EAAA,MAAM,aAAA,GAAgB,MAAM,cAAA,CAAe,IAAI,CAAA;AAI/C,EAAA,MAAM,YAAA,GAA+C;AAAA,IACnD,CAAC,OAAA,EAAS,QAAA,CAAS,KAAK,CAAA;AAAA,IACxB,CAAC,YAAA,EAAc,QAAA,CAAS,SAAS;AAAA,GACnC;AAMA,EAAA,MAAM,8BACJ,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,CAAC,UAAA,wBACC,IAAA,EAAA,EAAK,OAAA,EAAQ,cAAa,EAAA,EAAG,GAAA,EAAI,KAAA,EAAM,WAAA,EAAY,QAAA,EAAA,kDAAA,EAEpD,CAAA;AAAA,IAED,UAAA,IAAc,SAAA,CAAU,KAAA,KAAU,WAAA,oBACjC,GAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,YAAA,EAAa,EAAA,EAAG,GAAA,EAAI,KAAA,EAAM,WAAA,EAAY,QAAA,EAAA,0FAAA,EAGpD,CAAA;AAAA,IAED,UAAA,IAAc,UAAU,KAAA,KAAU,UAAA,yBAChC,IAAA,EAAA,EAAK,KAAA,EAAM,QAAA,EAAS,GAAA,EAAI,GAAA,EACvB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,YAAA,EAAa,IAAG,GAAA,EAAI,KAAA,EAAM,aAAY,QAAA,EAAA,kDAAA,EAEpD,CAAA;AAAA,sBACA,GAAA,CAAC,UAAO,IAAA,EAAK,OAAA,EAAQ,SAAQ,UAAA,EAAW,OAAA,EAAS,SAAA,CAAU,KAAA,EAAO,QAAA,EAAA,OAAA,EAElE;AAAA,KAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAGF,EAAA,MAAM,iBACJ,CAAC,UAAA,IACD,UAAU,KAAA,KAAU,WAAA,IACpB,UAAU,KAAA,KAAU,UAAA;AAEtB,EAAA,MAAM,+BACJ,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,WAAA;AAAA,IACA,cAAc,SAAA,CAAU,OAAA,yBACtB,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,WAAA,EACrB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,MAAA,EAAO,MAAA,EAAQ,EAAA,EAAI,CAAA;AAAA,sBACnC,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,KAAA,EAAM,QAAQ,EAAA,EAAI,CAAA;AAAA,sBAClC,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,KAAA,EAAM,QAAQ,EAAA,EAAI;AAAA,KAAA,EACpC,CAAA;AAAA,IAED,SAAA,CAAU,IAAA,oBACT,GAAA,CAAC,KAAA,EAAA,EAAI,WAAW,MAAA,CAAO,QAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,aAAA,EAAA,EACE,QAAA,EAAA,gBAAA,CAAiB,SAAA,CAAU,IAAA,CAAK,OAAO,GAC1C,CAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAGF,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,WAAW,MAAA,CAAO,OAAA;AAAA,QAClB,WAAA,EAAW,MAAA;AAAA,QACX,OAAA,EAAS,OAAA;AAAA,QACT,IAAA,EAAK,cAAA;AAAA,QACL,eAAa,CAAC;AAAA;AAAA,KAChB;AAAA,oBACA,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,WAAW,MAAA,CAAO,MAAA;AAAA,QAClB,WAAA,EAAW,MAAA;AAAA,QACX,KAAA,EACE;AAAA,UACE,mBAAmB,IAAA,CAAK,KAAA;AAAA,UACxB,gBAAA,EAAkB,GAAG,UAAU,CAAA,EAAA;AAAA,SACjC;AAAA,QAEF,IAAA,EAAK,QAAA;AAAA,QACL,YAAA,EAAY,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA;AAAA,QACvC,eAAa,CAAC,MAAA;AAAA,QAEd,QAAA,EAAA;AAAA,0BAAA,IAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,MAAA,EACtB,QAAA,EAAA;AAAA,4BAAA,GAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,WAAW,MAAA,CAAO,OAAA;AAAA,gBAClB,KAAA,EACE;AAAA,kBACE,iBAAiB,IAAA,CAAK,KAAA;AAAA,kBACtB,oBAAoB,IAAA,CAAK;AAAA,iBAC3B;AAAA,gBAGF,QAAA,kBAAA,GAAA,CAAC,IAAA,CAAK,IAAA,EAAL,EAAU,MAAM,EAAA,EAAI;AAAA;AAAA,aACvB;AAAA,4BACA,IAAA,CAAC,GAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,UAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,aAAA,EAAc,MAAA,EAAO,MAAA,EAAO,IAAG,IAAA,EAC1C,QAAA,EAAA,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA,EAC9B,CAAA;AAAA,8BACA,GAAA;AAAA,gBAAC,IAAA;AAAA,gBAAA;AAAA,kBACC,OAAA,EAAQ,cAAA;AAAA,kBACR,WAAW,MAAA,CAAO,SAAA;AAAA,kBAClB,KAAA,EAAO,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM;AAAA,kBAE1B,QAAA,EAAA,IAAA,CAAK;AAAA;AAAA,eACR;AAAA,cAAA,CAKE,QAAA,CAAS,OAAA,IAAW,CAAC,CAAC,QAAQ,IAAA,IAAQ,CAAC,CAAC,MAAA,EAAQ,OAAA,qBAChD,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,YAAA,EACpB,QAAA,EAAA;AAAA,gBAAA,QAAA,CAAS,OAAA,oBACR,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,eAAA,EAAA,EAAgB,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE,GAAA;AAAA,kBAC5B,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,EAAE;AAAA,iBAAA,EACpC,CAAA;AAAA,gBAED,CAAC,CAAC,MAAA,EAAQ,IAAA,yBACR,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,SAAA,EAAA,EAAU,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE,GAAA;AAAA,kBAAE,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,MAAM;AAAA,iBAAA,EACzD,CAAA;AAAA,gBAED,CAAC,CAAC,MAAA,EAAQ,OAAA,yBACR,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,aAAA,EAAA,EAAc,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAG,GAAA;AAAA,kBAC3B,UAAA,CAAW,MAAA,CAAO,OAAA,EAAS,SAAS;AAAA,iBAAA,EACvC;AAAA,eAAA,EAEJ;AAAA,aAAA,EAEJ,CAAA;AAAA,4BACA,GAAA;AAAA,cAAC,UAAA;AAAA,cAAA;AAAA,gBACC,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,gBACnB,OAAA,EAAQ,UAAA;AAAA,gBACR,OAAA,EAAS,OAAA;AAAA,gBACT,YAAA,EAAW;AAAA;AAAA;AACb,WAAA,EACF,CAAA;AAAA,0BAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,IAAA,EACpB,QAAA,EAAA;AAAA,YAAA,QAAA,CAAS,WAAA,oBACR,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,cAAA,EAE7D,CAAA;AAAA,kCACC,IAAA,EAAA,EAAK,OAAA,EAAQ,cAAa,EAAA,EAAG,GAAA,EAC3B,mBAAS,WAAA,EACZ;AAAA,aAAA,EACF,CAAA;AAAA,4BAGF,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,WAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAW,MAAA,CAAO,QAAA,EACnB,uBACE,MAAA,CAAO,CAAC,GAAG,CAAC,CAAA,KAAM,CAAC,CAAA,CACnB,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,sBACT,IAAA,CAAC,KAAA,EAAA,EAAY,SAAA,EAAW,MAAA,CAAO,OAAA,EAC7B,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,QAAI,QAAA,EAAA,CAAA,EAAE,CAAA;AAAA,gCACP,GAAA,CAAC,IAAA,EAAA,EACE,QAAA,EAAA,eAAA,CAAgB,GAAA,CAAI,CAAC,CAAA,IAAK,oBAAA,CAAqB,CAAE,CAAA,uBAC/C,aAAA,EAAA,EAAc,SAAA,EAAW,CAAA,EAAK,QAAA,EAAA,CAAA,EAAE,IAEjC,CAAA,EAEJ;AAAA,eAAA,EAAA,EARQ,CASV,CACD,CAAA,EACL;AAAA,aAAA,EACF,CAAA;AAAA,YAEC,QAAA,CAAS,WAAW,MAAA,GAAS,CAAA,yBAC3B,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,aAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACpB,QAAA,EAAA,QAAA,CAAS,UAAA,CAAW,GAAA,CAAI,CAAA,CAAA,qBACvB,IAAA,CAAC,MAAA,EAAA,EAAa,SAAA,EAAW,OAAO,cAAA,EAC9B,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,QAAA,EAAA,EAAS,IAAA,EAAM,CAAA,EAAqB,IAAA,EAAM,EAAA,EAAI,CAAA;AAAA,gBAC9C,eAAe,CAAC;AAAA,eAAA,EAAA,EAFR,CAGX,CACD,CAAA,EACH;AAAA,aAAA,EACF,CAAA;AAAA,YAGD,UAAA,oBACC,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACpB,QAAA,EAAA;AAAA,cAAA,eAAA,CAAgB,QAAA,CAAS,IAAI,CAAA,oBAC5B,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,WAAA;AAAA,kBACR,SAAA,sBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,kBAC3B,UAAA,EAAY,CAAC,SAAA,CAAU,IAAA;AAAA,kBACvB,OAAA,EAAS,UAAA;AAAA,kBAER,mBAAS,SAAA,GAAY;AAAA;AAAA,eACxB;AAAA,cAED,uBAAA,CAAwB,QAAA,CAAS,IAAI,CAAA,oBACpC,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,WAAA;AAAA,kBACR,SAAA,sBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,kBAC3B,OAAA,EAAS,cAAA;AAAA,kBACV,QAAA,EAAA;AAAA;AAAA,eAED;AAAA,8BAEF,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,SAAA;AAAA,kBACR,SAAA,sBAAY,aAAA,EAAA,EAAc,CAAA;AAAA,kBAC1B,OAAA,EAAS,aAAA;AAAA,kBACV,QAAA,EAAA;AAAA;AAAA;AAED,aAAA,EACF,CAAA;AAAA,YAGD,MAAA,oBACC,GAAA;AAAA,cAAC,qBAAA;AAAA,cAAA;AAAA,gBACC,QAAA;AAAA,gBACA,KAAA;AAAA,gBACA;AAAA;AAAA,aACF;AAAA,YAGD,QAAA,CAAS,KAAK,MAAA,GAAS,CAAA,yBACrB,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,OAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACpB,QAAA,EAAA,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,CAAA,CAAA,qBACjB,IAAA,CAAC,MAAA,EAAA,EAAa,SAAA,EAAW,OAAO,cAAA,EAAgB,QAAA,EAAA;AAAA,gBAAA,GAAA;AAAA,gBAC5C;AAAA,eAAA,EAAA,EADO,CAEX,CACD,CAAA,EACH;AAAA,aAAA,EACF,CAAA;AAAA,YAGD,mBAAA,CAAoB,SAAS,IAAI,CAAA,yBAC/B,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,UAAA,EAE7D,CAAA;AAAA,cACC;AAAA,aAAA,EACH,CAAA;AAAA,YAMD,CAAC,mBAAA,CAAoB,QAAA,CAAS,IAAI,CAAA,IAAK,cAAA,oBACtC,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EAAU,QAAA,EAAA,WAAA,EAAY,CAAA;AAAA,YAG9C,SAAS,cAAA,oBACR,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,kBAAA,GAAA;AAAA,cAAC,IAAA;AAAA,cAAA;AAAA,gBACC,IAAA,EAAM,QAAA,CAAS,cAAA,CAAe,OAAA,CAAQ,SAAS,EAAE,CAAA;AAAA,gBACjD,MAAA,EAAO,QAAA;AAAA,gBACP,GAAA,EAAI,qBAAA;AAAA,gBAEJ,QAAA,kBAAA,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,iBAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,kBAAA,EAAA,EAAmB,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE;AAAA,iBAAA,EAClC;AAAA;AAAA,aACF,EACF;AAAA,WAAA,EAEJ;AAAA;AAAA;AAAA,KACF;AAAA,oBAEA,GAAA;AAAA,MAAC,qBAAA;AAAA,MAAA;AAAA,QACC,QAAA;AAAA,QACA,MAAM,SAAA,CAAU,IAAA;AAAA,QAChB,aAAa,SAAA,CAAU,OAAA;AAAA,QACvB,MAAA,EAAQ,WAAA;AAAA,QACR,YAAA,EAAc;AAAA;AAAA;AAChB,GAAA,EACF,CAAA;AAEJ;;;;"}
@@ -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__cf1a969bfb {\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__cf1a969bfb[data-open='true'] {\n opacity: 1;\n pointer-events: auto;\n }\n\n .ResourceDetailPanel_drawer__cf1a969bfb {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: 520px;\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__cf1a969bfb[data-open='true'] {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .ResourceDetailPanel_overlay__cf1a969bfb,\n .ResourceDetailPanel_drawer__cf1a969bfb {\n transition: none;\n }\n }\n\n .ResourceDetailPanel_header__cf1a969bfb {\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__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_bodyLoading__cf1a969bfb {\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__cf1a969bfb {\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__cf1a969bfb {\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__cf1a969bfb 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__cf1a969bfb h1,\n .ResourceDetailPanel_markdown__cf1a969bfb h2,\n .ResourceDetailPanel_markdown__cf1a969bfb h3 {\n font-size: 1rem;\n margin: var(--bui-space-3) 0 var(--bui-space-1);\n }\n\n .ResourceDetailPanel_iconBox__cf1a969bfb {\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__cf1a969bfb {\n flex: 1;\n min-width: 0;\n }\n\n .ResourceDetailPanel_body__cf1a969bfb {\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__cf1a969bfb {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_badges__cf1a969bfb {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_statsRow__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-3);\n }\n\n .ResourceDetailPanel_statItem__cf1a969bfb {\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__cf1a969bfb {\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__cf1a969bfb {\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__cf1a969bfb {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-2);\n font-size: 0.8rem;\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb dt {\n color: var(--bui-fg-secondary);\n min-width: 90px;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb dd {\n margin: 0;\n word-break: break-all;\n }\n\n .ResourceDetailPanel_sourceLinkContent__cf1a969bfb {\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__cf1a969bfb","drawer":"ResourceDetailPanel_drawer__cf1a969bfb","header":"ResourceDetailPanel_header__cf1a969bfb","actions":"ResourceDetailPanel_actions__cf1a969bfb","bodyLoading":"ResourceDetailPanel_bodyLoading__cf1a969bfb","codeBlock":"ResourceDetailPanel_codeBlock__cf1a969bfb","markdown":"ResourceDetailPanel_markdown__cf1a969bfb","iconBox":"ResourceDetailPanel_iconBox__cf1a969bfb","headerText":"ResourceDetailPanel_headerText__cf1a969bfb","body":"ResourceDetailPanel_body__cf1a969bfb","section":"ResourceDetailPanel_section__cf1a969bfb","badges":"ResourceDetailPanel_badges__cf1a969bfb","statsRow":"ResourceDetailPanel_statsRow__cf1a969bfb","statItem":"ResourceDetailPanel_statItem__cf1a969bfb","frameworkBadge":"ResourceDetailPanel_frameworkBadge__cf1a969bfb","tag":"ResourceDetailPanel_tag__cf1a969bfb","metaList":"ResourceDetailPanel_metaList__cf1a969bfb","metaRow":"ResourceDetailPanel_metaRow__cf1a969bfb","sourceLinkContent":"ResourceDetailPanel_sourceLinkContent__cf1a969bfb"};
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,6 +7,7 @@ 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';
11
12
  import { CollapsibleSection } from './CollapsibleSection.esm.js';
12
13
  import { JsonBody } from './JsonBody.esm.js';
@@ -56,20 +57,16 @@ function ClaudeIcon() {
56
57
  }
57
58
  );
58
59
  }
59
- function getInstallLinks(resource, body) {
60
+ function getInstallLinks(resource, frameworks, body) {
60
61
  if (resource.type === "agent") {
61
62
  return getAgentInstallLinks(
62
63
  resource.sourceLocation,
63
64
  resource.name,
64
- resource.frameworks
65
+ frameworks
65
66
  );
66
67
  }
67
68
  if (resource.type === "mcp-config") {
68
- return getMcpInstallLinks(
69
- resource.frameworks,
70
- resource.name,
71
- body?.content
72
- );
69
+ return getMcpInstallLinks(frameworks, resource.name, body?.content);
73
70
  }
74
71
  return [];
75
72
  }
@@ -127,7 +124,8 @@ function CopyPromptButton({ text, label }) {
127
124
  function MarketplaceJourney({
128
125
  resource,
129
126
  repoSlug,
130
- addCommands
127
+ addCommands,
128
+ onLaunch
131
129
  }) {
132
130
  return /* @__PURE__ */ jsxs("div", { className: styles.journey, children: [
133
131
  /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", className: styles.stepTitle, children: "1. Add the marketplace to your AI tool" }),
@@ -150,6 +148,7 @@ function MarketplaceJourney({
150
148
  className: brand.className,
151
149
  iconStart: brand.icon,
152
150
  href: link.href,
151
+ onPress: () => onLaunch(link.framework),
153
152
  children: [
154
153
  "Add in ",
155
154
  link.label
@@ -182,36 +181,55 @@ function MarketplaceJourney({
182
181
  function ResourceInstallDialog({
183
182
  resource,
184
183
  body,
184
+ bodyLoading = false,
185
185
  isOpen,
186
186
  onOpenChange
187
187
  }) {
188
188
  const api = useApi(devAiHubResourceApiRef);
189
+ const track = useTrackTelemetry();
189
190
  const [copied, setCopied] = useState(false);
190
191
  const [downloadError, setDownloadError] = useState(false);
191
192
  const shape = getBodyShape(resource.type);
192
- const installLinks = getInstallLinks(resource, body);
193
+ const frameworksRestricted = !!resource.frameworksRestricted;
194
+ const installFrameworks = resource.installFrameworks ?? resource.frameworks;
195
+ const installLinks = frameworksRestricted ? [] : getInstallLinks(resource, installFrameworks, body);
193
196
  const marketplaceRepoSlug = resource.type === "marketplace" ? getMarketplaceRepoSlug(resource.sourceLocation) : void 0;
194
- const marketplaceAddCommands = marketplaceRepoSlug ? getMarketplaceAddCommands(resource.frameworks, marketplaceRepoSlug) : [];
197
+ const marketplaceAddCommands = marketplaceRepoSlug && !frameworksRestricted ? getMarketplaceAddCommands(installFrameworks, marketplaceRepoSlug) : [];
195
198
  const hasMarketplaceJourney = marketplaceAddCommands.length > 0;
196
199
  const showBodyDoc = shape === "markdown" && (resource.type === "plugin" || resource.type === "marketplace" && !hasMarketplaceJourney);
197
200
  const usesPromptInstall = resource.type === "skill" || resource.type === "hook";
198
- const installSteps = usesPromptInstall ? getInstallSteps(
201
+ const installSteps = usesPromptInstall && !frameworksRestricted ? getInstallSteps(
199
202
  resource.type,
200
203
  resource.sourceLocation,
201
204
  resource.name,
202
- resource.frameworks
205
+ installFrameworks
203
206
  ) : [];
204
- const pathRows = expandInstallFrameworks(resource.frameworks).map((f) => ({
207
+ const pathRows = frameworksRestricted ? [] : expandInstallFrameworks(installFrameworks).map((f) => ({
205
208
  framework: f,
206
209
  target: getResourceInstallTarget(resource.type, f, resource.name)
207
210
  })).filter(
208
211
  (row) => !!row.target
209
212
  );
210
213
  const hasMergeTarget = (installSteps.length > 0 ? installSteps.map((s) => s.target) : pathRows.map((r) => r.target)).some((t) => t.mode === "merge");
214
+ const hasLauncher = installLinks.length > 0 || installSteps.some((step) => step.link) || marketplaceAddCommands.some((command) => command.deepLinks.length > 0);
215
+ const awaitingMcpBody = resource.type === "mcp-config" && bodyLoading;
216
+ useEffect(() => {
217
+ if (!isOpen || frameworksRestricted || hasLauncher || awaitingMcpBody) {
218
+ return;
219
+ }
220
+ track(resource.entityRef, "install");
221
+ }, [
222
+ isOpen,
223
+ awaitingMcpBody,
224
+ frameworksRestricted,
225
+ hasLauncher,
226
+ resource.entityRef,
227
+ track
228
+ ]);
211
229
  const handleCopy = async () => {
212
230
  if (!body) return;
213
231
  await navigator.clipboard.writeText(body.content);
214
- api.track(resource.entityRef, "copy");
232
+ track(resource.entityRef, "copy");
215
233
  setCopied(true);
216
234
  setTimeout(() => setCopied(false), 2e3);
217
235
  };
@@ -219,7 +237,7 @@ function ResourceInstallDialog({
219
237
  setDownloadError(false);
220
238
  try {
221
239
  await api.downloadEntityBody(resource.entityRef);
222
- api.track(resource.entityRef, "download");
240
+ track(resource.entityRef, "download");
223
241
  } catch {
224
242
  setDownloadError(true);
225
243
  }
@@ -241,14 +259,15 @@ function ResourceInstallDialog({
241
259
  resource.title ?? resource.name
242
260
  ] }),
243
261
  /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs("div", { className: styles.content, children: [
244
- /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", children: TYPE_HINTS[resource.type] }),
262
+ /* @__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] }),
245
263
  body && shape === "json" && /* @__PURE__ */ jsx(CollapsibleSection, { label: "Content", children: /* @__PURE__ */ jsx(JsonBody, { content: body.content, className: styles.codeBlock }) }),
246
264
  hasMarketplaceJourney && marketplaceRepoSlug && /* @__PURE__ */ jsx(
247
265
  MarketplaceJourney,
248
266
  {
249
267
  resource,
250
268
  repoSlug: marketplaceRepoSlug,
251
- addCommands: marketplaceAddCommands
269
+ addCommands: marketplaceAddCommands,
270
+ onLaunch: (framework) => track(resource.entityRef, "install", framework)
252
271
  }
253
272
  ),
254
273
  body && showBodyDoc && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: body.content }) }),
@@ -271,11 +290,7 @@ function ResourceInstallDialog({
271
290
  className: brand.className,
272
291
  iconStart: brand.icon,
273
292
  href: link.href,
274
- onPress: () => api.track(
275
- resource.entityRef,
276
- "install",
277
- framework
278
- ),
293
+ onPress: () => track(resource.entityRef, "install", framework),
279
294
  children: [
280
295
  "Install in ",
281
296
  link.label
@@ -327,6 +342,7 @@ function ResourceInstallDialog({
327
342
  className: brand.className,
328
343
  iconStart: brand.icon,
329
344
  href: link.href,
345
+ onPress: () => track(resource.entityRef, "install", link.framework),
330
346
  children: [
331
347
  "Install in ",
332
348
  link.label
@@ -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(resource: ResourceSummary, body?: ResourceBody) {\n if (resource.type === 'agent') {\n return getAgentInstallLinks(\n resource.sourceLocation,\n resource.name,\n resource.frameworks,\n );\n }\n if (resource.type === 'mcp-config') {\n return getMcpInstallLinks(\n resource.frameworks,\n resource.name,\n body?.content,\n );\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 const installLinks = getInstallLinks(resource, 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 = marketplaceRepoSlug\n ? getMarketplaceAddCommands(resource.frameworks, 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 = usesPromptInstall\n ? getInstallSteps(\n resource.type,\n resource.sourceLocation,\n resource.name,\n resource.frameworks,\n )\n : [];\n const pathRows = expandInstallFrameworks(resource.frameworks)\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 {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 { 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 <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 use\n “View source”.\n </Text>\n )}\n </div>\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.0",
3
+ "version": "1.1.2",
4
4
  "description": "Frontend plugin for Dev AI Hub",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.esm.js",
@@ -39,8 +39,10 @@
39
39
  "sideEffects": false,
40
40
  "files": [
41
41
  "dist",
42
+ "config.schema.json",
42
43
  "README.md"
43
44
  ],
45
+ "configSchema": "config.schema.json",
44
46
  "scripts": {
45
47
  "start": "backstage-cli package start --config ../../app-config.dev.yaml",
46
48
  "build": "backstage-cli package build",
@@ -60,8 +62,8 @@
60
62
  "@backstage/frontend-plugin-api": "^0.18.0",
61
63
  "@backstage/plugin-catalog-react": "^3.2.2",
62
64
  "@backstage/ui": "^0.17.1",
63
- "@nospt/plugin-dev-ai-hub-common": "^1.1.0",
64
- "@remixicon/react": "^4.6.0",
65
+ "@nospt/plugin-dev-ai-hub-common": "^1.1.2",
66
+ "@remixicon/react": "^4.9.0",
65
67
  "react-markdown": "^9.0.0",
66
68
  "react-syntax-highlighter": "^16.1.1",
67
69
  "remark-gfm": "^4.0.1",