@nospt/plugin-dev-ai-hub 1.0.0-rc1 → 1.0.1

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.
@@ -4,15 +4,18 @@ import ReactMarkdown from 'react-markdown';
4
4
  import { Flex, Box, Text, ButtonIcon, Button, Skeleton, Link } from '@backstage/ui';
5
5
  import { RiCloseLine, RiEyeLine, RiInstallLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
6
6
  import { useApi } from '@backstage/core-plugin-api';
7
- import { getBodyShape, hasCopyableBody, hasDownloadableArtifact } from '@nospt/plugin-dev-ai-hub-common';
7
+ import { EntityRefLink } from '@backstage/plugin-catalog-react';
8
+ import { getBodyShape, hasCopyableBody, hasDownloadableArtifact, isParseableEntityRef } from '@nospt/plugin-dev-ai-hub-common';
8
9
  import { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient.esm.js';
9
10
  import { useResourceBody } from '../../hooks/useResourceBody.esm.js';
10
11
  import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
11
12
  import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
12
13
  import { ResourceInstallDialog } from './ResourceInstallDialog.esm.js';
14
+ import { stripFrontmatter } from './stripFrontmatter.esm.js';
13
15
  import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
14
16
  import styles from './ResourceDetailPanel.module.css.esm.js';
15
17
 
18
+ const ENTITY_REF_ROWS = /* @__PURE__ */ new Set(["Owner", "Entity ref"]);
16
19
  function ResourceDetailPanel({
17
20
  resource: resourceProp,
18
21
  onClose
@@ -183,7 +186,7 @@ function ResourceDetailPanel({
183
186
  )
184
187
  ] }),
185
188
  bodyState.body && bodyShape === "json" && /* @__PURE__ */ jsx("pre", { className: styles.codeBlock, children: /* @__PURE__ */ jsx("code", { children: bodyState.body.content }) }),
186
- bodyState.body && bodyShape === "markdown" && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: bodyState.body.content }) })
189
+ bodyState.body && bodyShape === "markdown" && /* @__PURE__ */ jsx("div", { className: styles.markdown, children: /* @__PURE__ */ jsx(ReactMarkdown, { children: stripFrontmatter(bodyState.body.content) }) })
187
190
  ] }),
188
191
  resource.frameworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.section, children: [
189
192
  /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Works with" }),
@@ -203,7 +206,7 @@ function ResourceDetailPanel({
203
206
  /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", weight: "bold", children: "Metadata" }),
204
207
  /* @__PURE__ */ jsx("dl", { className: styles.metaList, children: metadataRows.filter(([, v]) => v).map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
205
208
  /* @__PURE__ */ jsx("dt", { children: k }),
206
- /* @__PURE__ */ jsx("dd", { children: v })
209
+ /* @__PURE__ */ jsx("dd", { children: ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v) ? /* @__PURE__ */ jsx(EntityRefLink, { entityRef: v, children: v }) : v })
207
210
  ] }, k)) })
208
211
  ] }),
209
212
  resource.sourceLocation && /* @__PURE__ */ jsx("div", { className: styles.section, children: /* @__PURE__ */ jsx(
@@ -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 {\n getBodyShape,\n hasCopyableBody,\n hasDownloadableArtifact,\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 { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n}\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}: 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 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 <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content\n </Text>\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\n have 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\n size=\"small\"\n variant=\"tertiary\"\n onPress={bodyState.retry}\n >\n Retry\n </Button>\n </Flex>\n )}\n {bodyState.body && bodyShape === 'json' && (\n <pre className={styles.codeBlock}>\n <code>{bodyState.body.content}</code>\n </pre>\n )}\n {bodyState.body && bodyShape === 'markdown' && (\n <div className={styles.markdown}>\n <ReactMarkdown>{bodyState.body.content}</ReactMarkdown>\n </div>\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>{v}</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} 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 { 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}\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}: 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 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 <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content\n </Text>\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\n have 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\n size=\"small\"\n variant=\"tertiary\"\n onPress={bodyState.retry}\n >\n Retry\n </Button>\n </Flex>\n )}\n {bodyState.body && bodyShape === 'json' && (\n <pre className={styles.codeBlock}>\n <code>{bodyState.body.content}</code>\n </pre>\n )}\n {bodyState.body && bodyShape === 'markdown' && (\n <div className={styles.markdown}>\n <ReactMarkdown>\n {stripFrontmatter(bodyState.body.content)}\n </ReactMarkdown>\n </div>\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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"}
@@ -0,0 +1,7 @@
1
+ const FRONTMATTER = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n)*/;
2
+ function stripFrontmatter(content) {
3
+ return content.replace(FRONTMATTER, "");
4
+ }
5
+
6
+ export { stripFrontmatter };
7
+ //# sourceMappingURL=stripFrontmatter.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripFrontmatter.esm.js","sources":["../../../src/components/AiResourcesPage/stripFrontmatter.ts"],"sourcesContent":["const FRONTMATTER = /^---\\r?\\n[\\s\\S]*?\\r?\\n---(?:\\r?\\n)*/;\n\n/**\n * Drop a leading YAML frontmatter block (`---\\n...\\n---`) before a body is\n * rendered as markdown. CommonMark has no concept of frontmatter: the two\n * unblanked lines merge into one paragraph, and the closing `---` is then\n * read as a Setext-heading underline for it, rendering \"name: x description:\n * y\" as a heading. Display-only — Copy/Download/Install keep the body\n * verbatim (CONTEXT.md: body).\n */\nexport function stripFrontmatter(content: string): string {\n return content.replace(FRONTMATTER, '');\n}\n"],"names":[],"mappings":"AAAA,MAAM,WAAA,GAAc,qCAAA;AAUb,SAAS,iBAAiB,OAAA,EAAyB;AACxD,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,EAAE,CAAA;AACxC;;;;"}
@@ -24,6 +24,7 @@ const FRAMEWORK_LABELS = {
24
24
  "github-copilot": "GitHub Copilot",
25
25
  "google-gemini": "Google Gemini",
26
26
  cursor: "Cursor",
27
+ opencode: "OpenCode",
27
28
  all: "All tools"
28
29
  };
29
30
  function frameworkLabel(token) {
@@ -1 +1 @@
1
- {"version":3,"file":"typeMeta.esm.js","sources":["../../../src/components/AiResourcesPage/typeMeta.tsx"],"sourcesContent":["import type { ElementType } from 'react';\nimport {\n RiFlashlightLine,\n RiPlugLine,\n RiPuzzleLine,\n RiRobot2Line,\n RiStore2Line,\n RiToolsLine,\n} from '@remixicon/react';\nimport {\n RESOURCE_TYPE_REGISTRY,\n type ResourceType,\n type ResourceTypeIcon,\n type ResourceTypeInfo,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Maps the registry's React-free icon identifiers to components. */\nconst ICONS: Record<ResourceTypeIcon, ElementType> = {\n tools: RiToolsLine,\n robot: RiRobot2Line,\n flash: RiFlashlightLine,\n plug: RiPlugLine,\n puzzle: RiPuzzleLine,\n store: RiStore2Line,\n};\n\nexport interface TypeMeta extends ResourceTypeInfo {\n Icon: ElementType;\n /** The plugin-owned colour tokens for this role (ADR-0008). */\n color: string;\n colorBg: string;\n /**\n * The stat tile's solid fill: the brand hex as published, which is NOT\n * `color` — that one is darkened in light mode to read against a card.\n */\n tileFill: string;\n}\n\nexport function getTypeMeta(type: ResourceType): TypeMeta {\n const info = RESOURCE_TYPE_REGISTRY[type];\n return {\n ...info,\n Icon: ICONS[info.icon],\n color: `var(--devaihub-type-${info.colorRole})`,\n colorBg: `var(--devaihub-type-${info.colorRole}-bg)`,\n tileFill: `var(--devaihub-tile-${info.colorRole})`,\n };\n}\n\n/** Display labels for the known framework tokens; unknown pass through. */\nconst FRAMEWORK_LABELS: Record<string, string> = {\n 'claude-code': 'Claude Code',\n 'github-copilot': 'GitHub Copilot',\n 'google-gemini': 'Google Gemini',\n cursor: 'Cursor',\n all: 'All tools',\n};\n\nexport function frameworkLabel(token: string): string {\n return FRAMEWORK_LABELS[token] ?? token;\n}\n"],"names":[],"mappings":";;;AAiBA,MAAM,KAAA,GAA+C;AAAA,EACnD,KAAA,EAAO,WAAA;AAAA,EACP,KAAA,EAAO,YAAA;AAAA,EACP,KAAA,EAAO,gBAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,MAAA,EAAQ,YAAA;AAAA,EACR,KAAA,EAAO;AACT,CAAA;AAcO,SAAS,YAAY,IAAA,EAA8B;AACxD,EAAA,MAAM,IAAA,GAAO,uBAAuB,IAAI,CAAA;AACxC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAAA,IACrB,KAAA,EAAO,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA,CAAA;AAAA,IAC5C,OAAA,EAAS,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,IAAA,CAAA;AAAA,IAC9C,QAAA,EAAU,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA;AAAA,GACjD;AACF;AAGA,MAAM,gBAAA,GAA2C;AAAA,EAC/C,aAAA,EAAe,aAAA;AAAA,EACf,gBAAA,EAAkB,gBAAA;AAAA,EAClB,eAAA,EAAiB,eAAA;AAAA,EACjB,MAAA,EAAQ,QAAA;AAAA,EACR,GAAA,EAAK;AACP,CAAA;AAEO,SAAS,eAAe,KAAA,EAAuB;AACpD,EAAA,OAAO,gBAAA,CAAiB,KAAK,CAAA,IAAK,KAAA;AACpC;;;;"}
1
+ {"version":3,"file":"typeMeta.esm.js","sources":["../../../src/components/AiResourcesPage/typeMeta.tsx"],"sourcesContent":["import type { ElementType } from 'react';\nimport {\n RiFlashlightLine,\n RiPlugLine,\n RiPuzzleLine,\n RiRobot2Line,\n RiStore2Line,\n RiToolsLine,\n} from '@remixicon/react';\nimport {\n RESOURCE_TYPE_REGISTRY,\n type ResourceType,\n type ResourceTypeIcon,\n type ResourceTypeInfo,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Maps the registry's React-free icon identifiers to components. */\nconst ICONS: Record<ResourceTypeIcon, ElementType> = {\n tools: RiToolsLine,\n robot: RiRobot2Line,\n flash: RiFlashlightLine,\n plug: RiPlugLine,\n puzzle: RiPuzzleLine,\n store: RiStore2Line,\n};\n\nexport interface TypeMeta extends ResourceTypeInfo {\n Icon: ElementType;\n /** The plugin-owned colour tokens for this role (ADR-0008). */\n color: string;\n colorBg: string;\n /**\n * The stat tile's solid fill: the brand hex as published, which is NOT\n * `color` — that one is darkened in light mode to read against a card.\n */\n tileFill: string;\n}\n\nexport function getTypeMeta(type: ResourceType): TypeMeta {\n const info = RESOURCE_TYPE_REGISTRY[type];\n return {\n ...info,\n Icon: ICONS[info.icon],\n color: `var(--devaihub-type-${info.colorRole})`,\n colorBg: `var(--devaihub-type-${info.colorRole}-bg)`,\n tileFill: `var(--devaihub-tile-${info.colorRole})`,\n };\n}\n\n/** Display labels for the known framework tokens; unknown pass through. */\nconst FRAMEWORK_LABELS: Record<string, string> = {\n 'claude-code': 'Claude Code',\n 'github-copilot': 'GitHub Copilot',\n 'google-gemini': 'Google Gemini',\n cursor: 'Cursor',\n opencode: 'OpenCode',\n all: 'All tools',\n};\n\nexport function frameworkLabel(token: string): string {\n return FRAMEWORK_LABELS[token] ?? token;\n}\n"],"names":[],"mappings":";;;AAiBA,MAAM,KAAA,GAA+C;AAAA,EACnD,KAAA,EAAO,WAAA;AAAA,EACP,KAAA,EAAO,YAAA;AAAA,EACP,KAAA,EAAO,gBAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,MAAA,EAAQ,YAAA;AAAA,EACR,KAAA,EAAO;AACT,CAAA;AAcO,SAAS,YAAY,IAAA,EAA8B;AACxD,EAAA,MAAM,IAAA,GAAO,uBAAuB,IAAI,CAAA;AACxC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAAA,IACrB,KAAA,EAAO,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA,CAAA;AAAA,IAC5C,OAAA,EAAS,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,IAAA,CAAA;AAAA,IAC9C,QAAA,EAAU,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA;AAAA,GACjD;AACF;AAGA,MAAM,gBAAA,GAA2C;AAAA,EAC/C,aAAA,EAAe,aAAA;AAAA,EACf,gBAAA,EAAkB,gBAAA;AAAA,EAClB,eAAA,EAAiB,eAAA;AAAA,EACjB,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,GAAA,EAAK;AACP,CAAA;AAEO,SAAS,eAAe,KAAA,EAAuB;AACpD,EAAA,OAAO,gBAAA,CAAiB,KAAK,CAAA,IAAK,KAAA;AACpC;;;;"}
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { RiInfinityLine } from '@remixicon/react';
2
+ import { RiInfinityLine, RiTerminalBoxLine } from '@remixicon/react';
3
3
  import { siGooglegemini, siGithub, siAnthropic } from 'simple-icons';
4
4
 
5
5
  const CURSOR_PATH = "m20.42 6.73l-8-4.62a.82.82 0 0 0-.83 0L3.58 6.73c-.22.12-.35.36-.35.61v9.32c0 .25.13.48.35.61l8.01 4.62c.26.15.57.15.83 0l8.01-4.62c.22-.12.35-.36.35-.61V7.34c0-.25-.13-.48-.35-.61Zm-.5.98L12.19 21.1c-.05.09-.19.05-.19-.05v-8.77c0-.18-.09-.34-.25-.43L4.16 7.47c-.09-.05-.05-.19.05-.19h15.46c.22 0 .36.24.25.43";
@@ -27,6 +27,21 @@ function ToolIcon({
27
27
  }
28
28
  );
29
29
  }
30
+ if (tool === "opencode") {
31
+ return /* @__PURE__ */ jsx(
32
+ RiTerminalBoxLine,
33
+ {
34
+ size,
35
+ className,
36
+ style: {
37
+ color: branded ? "var(--bui-fg-primary)" : "inherit",
38
+ ...style
39
+ },
40
+ "aria-label": "OpenCode",
41
+ role: "img"
42
+ }
43
+ );
44
+ }
30
45
  if (tool === "cursor") {
31
46
  return /* @__PURE__ */ jsx(
32
47
  "svg",
@@ -1 +1 @@
1
- {"version":3,"file":"ToolIcon.esm.js","sources":["../../../src/components/ToolIcon/ToolIcon.tsx"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { RiInfinityLine } from '@remixicon/react';\nimport { siAnthropic, siGithub, siGooglegemini } from 'simple-icons';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\n\n/* Cursor's cube mark isn't in simple-icons; path from Boxicons Brands\n (`bxl:cursor-ai`, MIT). Monochrome brand — no branded hex. */\nconst CURSOR_PATH =\n 'm20.42 6.73l-8-4.62a.82.82 0 0 0-.83 0L3.58 6.73c-.22.12-.35.36-.35.61v9.32c0 .25.13.48.35.61l8.01 4.62c.26.15.57.15.83 0l8.01-4.62c.22-.12.35-.36.35-.61V7.34c0-.25-.13-.48-.35-.61Zm-.5.98L12.19 21.1c-.05.09-.19.05-.19-.05v-8.77c0-.18-.09-.34-.25-.43L4.16 7.47c-.09-.05-.05-.19.05-.19h15.46c.22 0 .36.24.25.43';\n\ntype SvgTool = Exclude<FrameworkToken, 'all' | 'cursor'>;\n\n/**\n * Anthropic's and GitHub's marks are monochrome brand assets (near-black,\n * `#191919` / `#181717`) meant to be manually inverted per surface — used\n * literally, they're invisible on the dark theme's dark backgrounds. Treat\n * them like `cursor` below (theme-aware `--bui-fg-primary`) instead of their\n * literal hex. Google's mark is an actual mid-tone brand colour (`#8E75B2`)\n * that reads fine on both themes, so it keeps its literal hex.\n */\nconst TOOL_ICON: Record<\n SvgTool,\n { path: string; hex: string; label: string; monochrome?: boolean }\n> = {\n 'claude-code': { ...siAnthropic, label: 'Claude Code', monochrome: true },\n 'github-copilot': { ...siGithub, label: 'GitHub Copilot', monochrome: true },\n 'google-gemini': { ...siGooglegemini, label: 'Google Gemini' },\n};\n\ninterface ToolIconProps {\n tool: FrameworkToken;\n /** Use the brand's official color. Defaults to true. */\n branded?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Icon size in pixels. Defaults to 20. */\n size?: number;\n}\n\nexport function ToolIcon({\n tool,\n branded = true,\n className,\n style,\n size = 20,\n}: ToolIconProps) {\n if (tool === 'all') {\n return (\n <RiInfinityLine\n size={size}\n className={className}\n style={{ color: 'var(--bui-fg-secondary)', ...style }}\n aria-label=\"Universal\"\n // Without an explicit role the label is not exposed as an image name,\n // which the other two branches already set. It matters wherever the\n // icon stands alone (resource cards), where it is the only thing\n // naming the tool.\n role=\"img\"\n />\n );\n }\n\n if (tool === 'cursor') {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{\n color: branded ? 'var(--bui-fg-primary)' : 'inherit',\n ...style,\n }}\n aria-label=\"Cursor\"\n role=\"img\"\n >\n <path d={CURSOR_PATH} />\n </svg>\n );\n }\n\n const cfg = TOOL_ICON[tool as SvgTool];\n if (!cfg) return null;\n\n const brandedColor = cfg.monochrome ? 'var(--bui-fg-primary)' : `#${cfg.hex}`;\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{ color: branded ? brandedColor : 'inherit', ...style }}\n aria-label={cfg.label}\n role=\"img\"\n >\n <path d={cfg.path} />\n </svg>\n );\n}\n"],"names":[],"mappings":";;;;AAOA,MAAM,WAAA,GACJ,uTAAA;AAYF,MAAM,SAAA,GAGF;AAAA,EACF,eAAe,EAAE,GAAG,aAAa,KAAA,EAAO,aAAA,EAAe,YAAY,IAAA,EAAK;AAAA,EACxE,kBAAkB,EAAE,GAAG,UAAU,KAAA,EAAO,gBAAA,EAAkB,YAAY,IAAA,EAAK;AAAA,EAC3E,eAAA,EAAiB,EAAE,GAAG,cAAA,EAAgB,OAAO,eAAA;AAC/C,CAAA;AAYO,SAAS,QAAA,CAAS;AAAA,EACvB,IAAA;AAAA,EACA,OAAA,GAAU,IAAA;AAAA,EACV,SAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA,GAAO;AACT,CAAA,EAAkB;AAChB,EAAA,IAAI,SAAS,KAAA,EAAO;AAClB,IAAA,uBACE,GAAA;AAAA,MAAC,cAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,SAAA;AAAA,QACA,KAAA,EAAO,EAAE,KAAA,EAAO,yBAAA,EAA2B,GAAG,KAAA,EAAM;AAAA,QACpD,YAAA,EAAW,WAAA;AAAA,QAKX,IAAA,EAAK;AAAA;AAAA,KACP;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,uBACE,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAM,4BAAA;AAAA,QACN,OAAA,EAAQ,WAAA;AAAA,QACR,KAAA,EAAO,IAAA;AAAA,QACP,MAAA,EAAQ,IAAA;AAAA,QACR,IAAA,EAAK,cAAA;AAAA,QACL,SAAA;AAAA,QACA,KAAA,EAAO;AAAA,UACL,KAAA,EAAO,UAAU,uBAAA,GAA0B,SAAA;AAAA,UAC3C,GAAG;AAAA,SACL;AAAA,QACA,YAAA,EAAW,QAAA;AAAA,QACX,IAAA,EAAK,KAAA;AAAA,QAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,WAAA,EAAa;AAAA;AAAA,KACxB;AAAA,EAEJ;AAEA,EAAA,MAAM,GAAA,GAAM,UAAU,IAAe,CAAA;AACrC,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,MAAM,eAAe,GAAA,CAAI,UAAA,GAAa,uBAAA,GAA0B,CAAA,CAAA,EAAI,IAAI,GAAG,CAAA,CAAA;AAE3E,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAM,4BAAA;AAAA,MACN,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAO,IAAA;AAAA,MACP,MAAA,EAAQ,IAAA;AAAA,MACR,IAAA,EAAK,cAAA;AAAA,MACL,SAAA;AAAA,MACA,OAAO,EAAE,KAAA,EAAO,UAAU,YAAA,GAAe,SAAA,EAAW,GAAG,KAAA,EAAM;AAAA,MAC7D,cAAY,GAAA,CAAI,KAAA;AAAA,MAChB,IAAA,EAAK,KAAA;AAAA,MAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,GAAA,CAAI,IAAA,EAAM;AAAA;AAAA,GACrB;AAEJ;;;;"}
1
+ {"version":3,"file":"ToolIcon.esm.js","sources":["../../../src/components/ToolIcon/ToolIcon.tsx"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { RiInfinityLine, RiTerminalBoxLine } from '@remixicon/react';\nimport { siAnthropic, siGithub, siGooglegemini } from 'simple-icons';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\n\n/* Cursor's cube mark isn't in simple-icons; path from Boxicons Brands\n (`bxl:cursor-ai`, MIT). Monochrome brand — no branded hex. */\nconst CURSOR_PATH =\n 'm20.42 6.73l-8-4.62a.82.82 0 0 0-.83 0L3.58 6.73c-.22.12-.35.36-.35.61v9.32c0 .25.13.48.35.61l8.01 4.62c.26.15.57.15.83 0l8.01-4.62c.22-.12.35-.36.35-.61V7.34c0-.25-.13-.48-.35-.61Zm-.5.98L12.19 21.1c-.05.09-.19.05-.19-.05v-8.77c0-.18-.09-.34-.25-.43L4.16 7.47c-.09-.05-.05-.19.05-.19h15.46c.22 0 .36.24.25.43';\n\ntype SvgTool = Exclude<FrameworkToken, 'all' | 'cursor' | 'opencode'>;\n\n/**\n * Anthropic's and GitHub's marks are monochrome brand assets (near-black,\n * `#191919` / `#181717`) meant to be manually inverted per surface — used\n * literally, they're invisible on the dark theme's dark backgrounds. Treat\n * them like `cursor` below (theme-aware `--bui-fg-primary`) instead of their\n * literal hex. Google's mark is an actual mid-tone brand colour (`#8E75B2`)\n * that reads fine on both themes, so it keeps its literal hex.\n */\nconst TOOL_ICON: Record<\n SvgTool,\n { path: string; hex: string; label: string; monochrome?: boolean }\n> = {\n 'claude-code': { ...siAnthropic, label: 'Claude Code', monochrome: true },\n 'github-copilot': { ...siGithub, label: 'GitHub Copilot', monochrome: true },\n 'google-gemini': { ...siGooglegemini, label: 'Google Gemini' },\n};\n\ninterface ToolIconProps {\n tool: FrameworkToken;\n /** Use the brand's official color. Defaults to true. */\n branded?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Icon size in pixels. Defaults to 20. */\n size?: number;\n}\n\nexport function ToolIcon({\n tool,\n branded = true,\n className,\n style,\n size = 20,\n}: ToolIconProps) {\n if (tool === 'all') {\n return (\n <RiInfinityLine\n size={size}\n className={className}\n style={{ color: 'var(--bui-fg-secondary)', ...style }}\n aria-label=\"Universal\"\n // Without an explicit role the label is not exposed as an image name,\n // which the other two branches already set. It matters wherever the\n // icon stands alone (resource cards), where it is the only thing\n // naming the tool.\n role=\"img\"\n />\n );\n }\n\n if (tool === 'opencode') {\n // No verified brand mark is available to embed (unlike Cursor's, sourced\n // from a known open-license icon set) — a neutral terminal icon avoids\n // guessing at OpenCode's actual logomark geometry.\n return (\n <RiTerminalBoxLine\n size={size}\n className={className}\n style={{\n color: branded ? 'var(--bui-fg-primary)' : 'inherit',\n ...style,\n }}\n aria-label=\"OpenCode\"\n role=\"img\"\n />\n );\n }\n\n if (tool === 'cursor') {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{\n color: branded ? 'var(--bui-fg-primary)' : 'inherit',\n ...style,\n }}\n aria-label=\"Cursor\"\n role=\"img\"\n >\n <path d={CURSOR_PATH} />\n </svg>\n );\n }\n\n const cfg = TOOL_ICON[tool as SvgTool];\n if (!cfg) return null;\n\n const brandedColor = cfg.monochrome ? 'var(--bui-fg-primary)' : `#${cfg.hex}`;\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{ color: branded ? brandedColor : 'inherit', ...style }}\n aria-label={cfg.label}\n role=\"img\"\n >\n <path d={cfg.path} />\n </svg>\n );\n}\n"],"names":[],"mappings":";;;;AAOA,MAAM,WAAA,GACJ,uTAAA;AAYF,MAAM,SAAA,GAGF;AAAA,EACF,eAAe,EAAE,GAAG,aAAa,KAAA,EAAO,aAAA,EAAe,YAAY,IAAA,EAAK;AAAA,EACxE,kBAAkB,EAAE,GAAG,UAAU,KAAA,EAAO,gBAAA,EAAkB,YAAY,IAAA,EAAK;AAAA,EAC3E,eAAA,EAAiB,EAAE,GAAG,cAAA,EAAgB,OAAO,eAAA;AAC/C,CAAA;AAYO,SAAS,QAAA,CAAS;AAAA,EACvB,IAAA;AAAA,EACA,OAAA,GAAU,IAAA;AAAA,EACV,SAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA,GAAO;AACT,CAAA,EAAkB;AAChB,EAAA,IAAI,SAAS,KAAA,EAAO;AAClB,IAAA,uBACE,GAAA;AAAA,MAAC,cAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,SAAA;AAAA,QACA,KAAA,EAAO,EAAE,KAAA,EAAO,yBAAA,EAA2B,GAAG,KAAA,EAAM;AAAA,QACpD,YAAA,EAAW,WAAA;AAAA,QAKX,IAAA,EAAK;AAAA;AAAA,KACP;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,UAAA,EAAY;AAIvB,IAAA,uBACE,GAAA;AAAA,MAAC,iBAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,SAAA;AAAA,QACA,KAAA,EAAO;AAAA,UACL,KAAA,EAAO,UAAU,uBAAA,GAA0B,SAAA;AAAA,UAC3C,GAAG;AAAA,SACL;AAAA,QACA,YAAA,EAAW,UAAA;AAAA,QACX,IAAA,EAAK;AAAA;AAAA,KACP;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,uBACE,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAM,4BAAA;AAAA,QACN,OAAA,EAAQ,WAAA;AAAA,QACR,KAAA,EAAO,IAAA;AAAA,QACP,MAAA,EAAQ,IAAA;AAAA,QACR,IAAA,EAAK,cAAA;AAAA,QACL,SAAA;AAAA,QACA,KAAA,EAAO;AAAA,UACL,KAAA,EAAO,UAAU,uBAAA,GAA0B,SAAA;AAAA,UAC3C,GAAG;AAAA,SACL;AAAA,QACA,YAAA,EAAW,QAAA;AAAA,QACX,IAAA,EAAK,KAAA;AAAA,QAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,WAAA,EAAa;AAAA;AAAA,KACxB;AAAA,EAEJ;AAEA,EAAA,MAAM,GAAA,GAAM,UAAU,IAAe,CAAA;AACrC,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,MAAM,eAAe,GAAA,CAAI,UAAA,GAAa,uBAAA,GAA0B,CAAA,CAAA,EAAI,IAAI,GAAG,CAAA,CAAA;AAE3E,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAM,4BAAA;AAAA,MACN,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAO,IAAA;AAAA,MACP,MAAA,EAAQ,IAAA;AAAA,MACR,IAAA,EAAK,cAAA;AAAA,MACL,SAAA;AAAA,MACA,OAAO,EAAE,KAAA,EAAO,UAAU,YAAA,GAAe,SAAA,EAAW,GAAG,KAAA,EAAM;AAAA,MAC7D,cAAY,GAAA,CAAI,KAAA;AAAA,MAChB,IAAA,EAAK,KAAA;AAAA,MAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,GAAA,CAAI,IAAA,EAAM;AAAA;AAAA,GACrB;AAEJ;;;;"}
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
2
2
  import { useApi } from '@backstage/core-plugin-api';
3
3
  import { devAiHubResourceApiRef, ResourceBodyError } from '../api/DevAiHubResourceClient.esm.js';
4
4
 
5
+ const bodyCache = /* @__PURE__ */ new Map();
5
6
  function useResourceBody(entityRef, enabled) {
6
7
  const api = useApi(devAiHubResourceApiRef);
7
8
  const [body, setBody] = useState();
@@ -10,15 +11,23 @@ function useResourceBody(entityRef, enabled) {
10
11
  const [attempt, setAttempt] = useState(0);
11
12
  const retry = useCallback(() => setAttempt((a) => a + 1), []);
12
13
  useEffect(() => {
13
- setBody(void 0);
14
14
  setError(void 0);
15
15
  if (!entityRef || !enabled) {
16
+ setBody(void 0);
16
17
  setLoading(false);
17
18
  return void 0;
18
19
  }
20
+ const cached = bodyCache.get(entityRef);
21
+ if (cached) {
22
+ setBody(cached);
23
+ setLoading(false);
24
+ return void 0;
25
+ }
26
+ setBody(void 0);
19
27
  let cancelled = false;
20
28
  setLoading(true);
21
29
  api.getEntityBody(entityRef).then((result) => {
30
+ bodyCache.set(entityRef, result);
22
31
  if (!cancelled) setBody(result);
23
32
  }).catch((err) => {
24
33
  if (cancelled) return;
@@ -1 +1 @@
1
- {"version":3,"file":"useResourceBody.esm.js","sources":["../../src/hooks/useResourceBody.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n devAiHubResourceApiRef,\n ResourceBody,\n ResourceBodyError,\n} from '../api/DevAiHubResourceClient';\n\nexport type ResourceBodyErrorKind = 'not-found' | 'upstream';\n\nexport interface ResourceBodyState {\n body?: ResourceBody;\n loading: boolean;\n error?: ResourceBodyErrorKind;\n retry: () => void;\n}\n\n/**\n * Lazily fetch a resource's viewable body when the detail drawer opens.\n * `enabled` is false for non-actionable resources (no source-location) —\n * nothing is fetched and the state stays empty.\n */\nexport function useResourceBody(\n entityRef: string | undefined,\n enabled: boolean,\n): ResourceBodyState {\n const api = useApi(devAiHubResourceApiRef);\n const [body, setBody] = useState<ResourceBody>();\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<ResourceBodyErrorKind>();\n const [attempt, setAttempt] = useState(0);\n\n const retry = useCallback(() => setAttempt(a => a + 1), []);\n\n useEffect(() => {\n setBody(undefined);\n setError(undefined);\n if (!entityRef || !enabled) {\n setLoading(false);\n return undefined;\n }\n\n let cancelled = false;\n setLoading(true);\n api\n .getEntityBody(entityRef)\n .then(result => {\n if (!cancelled) setBody(result);\n })\n .catch(err => {\n if (cancelled) return;\n setError(\n err instanceof ResourceBodyError && err.status === 404\n ? 'not-found'\n : 'upstream',\n );\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [api, entityRef, enabled, attempt]);\n\n return { body, loading, error, retry };\n}\n"],"names":[],"mappings":";;;;AAsBO,SAAS,eAAA,CACd,WACA,OAAA,EACmB;AACnB,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,QAAA,EAAuB;AAC/C,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,QAAA,EAAgC;AAC1D,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,CAAC,CAAA;AAExC,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM,UAAA,CAAW,OAAK,CAAA,GAAI,CAAC,CAAA,EAAG,EAAE,CAAA;AAE1D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,OAAA,CAAQ,MAAS,CAAA;AACjB,IAAA,QAAA,CAAS,MAAS,CAAA;AAClB,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,GAAA,CACG,aAAA,CAAc,SAAS,CAAA,CACvB,IAAA,CAAK,CAAA,MAAA,KAAU;AACd,MAAA,IAAI,CAAC,SAAA,EAAW,OAAA,CAAQ,MAAM,CAAA;AAAA,IAChC,CAAC,CAAA,CACA,KAAA,CAAM,CAAA,GAAA,KAAO;AACZ,MAAA,IAAI,SAAA,EAAW;AACf,MAAA,QAAA;AAAA,QACE,GAAA,YAAe,iBAAA,IAAqB,GAAA,CAAI,MAAA,KAAW,MAC/C,WAAA,GACA;AAAA,OACN;AAAA,IACF,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,MAAA,IAAI,CAAC,SAAA,EAAW,UAAA,CAAW,KAAK,CAAA;AAAA,IAClC,CAAC,CAAA;AAEH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,GAAG,CAAC,GAAA,EAAK,SAAA,EAAW,OAAA,EAAS,OAAO,CAAC,CAAA;AAErC,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,KAAA,EAAM;AACvC;;;;"}
1
+ {"version":3,"file":"useResourceBody.esm.js","sources":["../../src/hooks/useResourceBody.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n devAiHubResourceApiRef,\n ResourceBody,\n ResourceBodyError,\n} from '../api/DevAiHubResourceClient';\n\nexport type ResourceBodyErrorKind = 'not-found' | 'upstream';\n\nexport interface ResourceBodyState {\n body?: ResourceBody;\n loading: boolean;\n error?: ResourceBodyErrorKind;\n retry: () => void;\n}\n\n/**\n * Session-lifetime cache, keyed by entityRef. Reopening a resource within the\n * same tab shouldn't repay its network cost every time. Populated on success\n * only — a failed fetch is never cached, so Retry always hits the network.\n * Module-scoped (not per-hook-instance) since the drawer's body content is\n * the same regardless of which component instance asks for it.\n */\nconst bodyCache = new Map<string, ResourceBody>();\n\n/** Test-only seam: the cache is module-scoped and outlives any one render. */\nexport function clearResourceBodyCache(): void {\n bodyCache.clear();\n}\n\n/**\n * Lazily fetch a resource's viewable body when the detail drawer opens.\n * `enabled` is false for non-actionable resources (no source-location) —\n * nothing is fetched and the state stays empty.\n */\nexport function useResourceBody(\n entityRef: string | undefined,\n enabled: boolean,\n): ResourceBodyState {\n const api = useApi(devAiHubResourceApiRef);\n const [body, setBody] = useState<ResourceBody>();\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<ResourceBodyErrorKind>();\n const [attempt, setAttempt] = useState(0);\n\n const retry = useCallback(() => setAttempt(a => a + 1), []);\n\n useEffect(() => {\n setError(undefined);\n if (!entityRef || !enabled) {\n setBody(undefined);\n setLoading(false);\n return undefined;\n }\n\n const cached = bodyCache.get(entityRef);\n if (cached) {\n setBody(cached);\n setLoading(false);\n return undefined;\n }\n\n setBody(undefined);\n let cancelled = false;\n setLoading(true);\n api\n .getEntityBody(entityRef)\n .then(result => {\n bodyCache.set(entityRef, result);\n if (!cancelled) setBody(result);\n })\n .catch(err => {\n if (cancelled) return;\n setError(\n err instanceof ResourceBodyError && err.status === 404\n ? 'not-found'\n : 'upstream',\n );\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [api, entityRef, enabled, attempt]);\n\n return { body, loading, error, retry };\n}\n"],"names":[],"mappings":";;;;AAwBA,MAAM,SAAA,uBAAgB,GAAA,EAA0B;AAYzC,SAAS,eAAA,CACd,WACA,OAAA,EACmB;AACnB,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,QAAA,EAAuB;AAC/C,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,QAAA,EAAgC;AAC1D,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,CAAC,CAAA;AAExC,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM,UAAA,CAAW,OAAK,CAAA,GAAI,CAAC,CAAA,EAAG,EAAE,CAAA;AAE1D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,QAAA,CAAS,MAAS,CAAA;AAClB,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,OAAA,CAAQ,MAAS,CAAA;AACjB,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,MAAA,GAAS,SAAA,CAAU,GAAA,CAAI,SAAS,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,OAAA,CAAQ,MAAM,CAAA;AACd,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,OAAA,CAAQ,MAAS,CAAA;AACjB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,GAAA,CACG,aAAA,CAAc,SAAS,CAAA,CACvB,IAAA,CAAK,CAAA,MAAA,KAAU;AACd,MAAA,SAAA,CAAU,GAAA,CAAI,WAAW,MAAM,CAAA;AAC/B,MAAA,IAAI,CAAC,SAAA,EAAW,OAAA,CAAQ,MAAM,CAAA;AAAA,IAChC,CAAC,CAAA,CACA,KAAA,CAAM,CAAA,GAAA,KAAO;AACZ,MAAA,IAAI,SAAA,EAAW;AACf,MAAA,QAAA;AAAA,QACE,GAAA,YAAe,iBAAA,IAAqB,GAAA,CAAI,MAAA,KAAW,MAC/C,WAAA,GACA;AAAA,OACN;AAAA,IACF,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,MAAA,IAAI,CAAC,SAAA,EAAW,UAAA,CAAW,KAAK,CAAA;AAAA,IAClC,CAAC,CAAA;AAEH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,GAAG,CAAC,GAAA,EAAK,SAAA,EAAW,OAAA,EAAS,OAAO,CAAC,CAAA;AAErC,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,KAAA,EAAM;AACvC;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/plugin-dev-ai-hub",
3
- "version": "1.0.0-rc1",
3
+ "version": "1.0.1",
4
4
  "description": "Frontend plugin for Dev AI Hub",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.esm.js",
@@ -58,8 +58,9 @@
58
58
  "@backstage/core-components": "^0.18.10",
59
59
  "@backstage/core-plugin-api": "^1.12.6",
60
60
  "@backstage/frontend-plugin-api": "^0.17.0",
61
+ "@backstage/plugin-catalog-react": "^3.0.0",
61
62
  "@backstage/ui": "^0.15.0",
62
- "@nospt/plugin-dev-ai-hub-common": "^1.0.0-rc1",
63
+ "@nospt/plugin-dev-ai-hub-common": "^1.0.1",
63
64
  "@remixicon/react": "^4.6.0",
64
65
  "react-markdown": "^9.0.0",
65
66
  "react-syntax-highlighter": "^16.1.1",
@@ -72,7 +73,6 @@
72
73
  "@backstage/frontend-defaults": "^0.5.2",
73
74
  "@backstage/plugin-app-react": "^0.2.3",
74
75
  "@backstage/plugin-catalog": "^2.0.5",
75
- "@backstage/plugin-catalog-react": "^3.0.0",
76
76
  "@backstage/plugin-user-settings": "^0.9.3",
77
77
  "@playwright/test": "^1.60.0",
78
78
  "@testing-library/dom": "^10.0.0",