@nospt/plugin-dev-ai-hub 1.1.3 → 1.2.0-rc2

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.
Files changed (68) hide show
  1. package/README.md +93 -2
  2. package/config.schema.json +15 -0
  3. package/dist/api/DevAiHubResourceClient.esm.js +42 -2
  4. package/dist/api/DevAiHubResourceClient.esm.js.map +1 -1
  5. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +98 -20
  6. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -1
  7. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +2 -2
  8. package/dist/components/AiResourcesPage/ResourceCard.esm.js +13 -4
  9. package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -1
  10. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +2 -2
  11. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +55 -9
  12. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -1
  13. package/dist/components/AiResourcesPage/ResourceFilters.esm.js +57 -3
  14. package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -1
  15. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +4 -2
  16. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -1
  17. package/dist/components/AiResourcesPage/SortControl.esm.js +139 -0
  18. package/dist/components/AiResourcesPage/SortControl.esm.js.map +1 -0
  19. package/dist/components/AiResourcesPage/SortControl.module.css.esm.js +8 -0
  20. package/dist/components/AiResourcesPage/SortControl.module.css.esm.js.map +1 -0
  21. package/dist/components/AiResourcesPage/ownerLabel.esm.js +6 -0
  22. package/dist/components/AiResourcesPage/ownerLabel.esm.js.map +1 -0
  23. package/dist/components/AiResourcesPage/typeMeta.esm.js +1 -12
  24. package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -1
  25. package/dist/components/Backoffice/Backoffice.module.css.esm.js +8 -0
  26. package/dist/components/Backoffice/Backoffice.module.css.esm.js.map +1 -0
  27. package/dist/components/Backoffice/BackofficeCuration.esm.js +191 -0
  28. package/dist/components/Backoffice/BackofficeCuration.esm.js.map +1 -0
  29. package/dist/components/Backoffice/BackofficeExclusion.esm.js +185 -0
  30. package/dist/components/Backoffice/BackofficeExclusion.esm.js.map +1 -0
  31. package/dist/components/Backoffice/BackofficeGate.esm.js +25 -0
  32. package/dist/components/Backoffice/BackofficeGate.esm.js.map +1 -0
  33. package/dist/components/Backoffice/BackofficeHeaderAction.esm.js +43 -0
  34. package/dist/components/Backoffice/BackofficeHeaderAction.esm.js.map +1 -0
  35. package/dist/components/Backoffice/BackofficeOverview.esm.js +517 -0
  36. package/dist/components/Backoffice/BackofficeOverview.esm.js.map +1 -0
  37. package/dist/components/Backoffice/BackofficeSection.esm.js +37 -0
  38. package/dist/components/Backoffice/BackofficeSection.esm.js.map +1 -0
  39. package/dist/components/Backoffice/TypeDistributionBar.esm.js +44 -0
  40. package/dist/components/Backoffice/TypeDistributionBar.esm.js.map +1 -0
  41. package/dist/components/Backoffice/rowEntrance.esm.js +11 -0
  42. package/dist/components/Backoffice/rowEntrance.esm.js.map +1 -0
  43. package/dist/components/Backoffice/useCurationAccess.esm.js +12 -0
  44. package/dist/components/Backoffice/useCurationAccess.esm.js.map +1 -0
  45. package/dist/components/RecommendedBadge/RecommendedBadgeIcon.esm.js +25 -0
  46. package/dist/components/RecommendedBadge/RecommendedBadgeIcon.esm.js.map +1 -0
  47. package/dist/components/RecommendedBadge/recommended-badge.svg +9 -0
  48. package/dist/components/ToolIcon/ToolIcon.esm.js +41 -6
  49. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  50. package/dist/hooks/useDefaultSortMode.esm.js +37 -0
  51. package/dist/hooks/useDefaultSortMode.esm.js.map +1 -0
  52. package/dist/hooks/useFrameworkLabel.esm.js +42 -0
  53. package/dist/hooks/useFrameworkLabel.esm.js.map +1 -0
  54. package/dist/hooks/useResources.esm.js +25 -14
  55. package/dist/hooks/useResources.esm.js.map +1 -1
  56. package/dist/hooks/useTelemetryCounts.esm.js +16 -7
  57. package/dist/hooks/useTelemetryCounts.esm.js.map +1 -1
  58. package/dist/hooks/useTelemetrySummary.esm.js +46 -0
  59. package/dist/hooks/useTelemetrySummary.esm.js.map +1 -0
  60. package/dist/index.d.ts +8 -1
  61. package/dist/index.esm.js +1 -1
  62. package/dist/plugin.esm.js +90 -3
  63. package/dist/plugin.esm.js.map +1 -1
  64. package/dist/routes.esm.js +2 -1
  65. package/dist/routes.esm.js.map +1 -1
  66. package/dist/theme/typeTokens.module.css.esm.js +5 -0
  67. package/dist/theme/typeTokens.module.css.esm.js.map +1 -0
  68. package/package.json +10 -4
@@ -12,7 +12,7 @@ import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
12
12
  import { AppLaunchScope } from './AppLaunchScope.esm.js';
13
13
  import { CollapsibleSection } from './CollapsibleSection.esm.js';
14
14
  import { JsonBody } from './JsonBody.esm.js';
15
- import { frameworkLabel } from './typeMeta.esm.js';
15
+ import { useFrameworkLabel } from '../../hooks/useFrameworkLabel.esm.js';
16
16
  import styles from './ResourceInstallDialog.module.css.esm.js';
17
17
 
18
18
  const TYPE_HINTS = {
@@ -128,6 +128,7 @@ function MarketplaceJourney({
128
128
  addCommands,
129
129
  onLaunch
130
130
  }) {
131
+ const frameworkLabel = useFrameworkLabel();
131
132
  return /* @__PURE__ */ jsxs("div", { className: styles.journey, children: [
132
133
  /* @__PURE__ */ jsx(Text, { variant: "body-small", as: "p", className: styles.stepTitle, children: "1. Add the marketplace to your AI tool" }),
133
134
  /* @__PURE__ */ jsx("dl", { className: styles.commandList, children: addCommands.map(({ framework, command, deepLinks }) => /* @__PURE__ */ jsxs("div", { className: styles.commandRow, children: [
@@ -186,6 +187,7 @@ function ResourceInstallDialog({
186
187
  isOpen,
187
188
  onOpenChange
188
189
  }) {
190
+ const frameworkLabel = useFrameworkLabel();
189
191
  const api = useApi(devAiHubResourceApiRef);
190
192
  const track = useTrackTelemetry();
191
193
  const [copied, setCopied] = useState(false);
@@ -300,7 +302,7 @@ function ResourceInstallDialog({
300
302
  );
301
303
  })() : (
302
304
  /* No prompt URI for this host — the same instruction,
303
- delivered by clipboard so the row is still actionable. */
305
+ delivered by clipboard so the row is still actionable. */
304
306
  /* @__PURE__ */ jsx(
305
307
  CopyPromptButton,
306
308
  {
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceInstallDialog.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceInstallDialog.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Button,\n ButtonIcon,\n ButtonLink,\n Dialog,\n DialogBody,\n DialogHeader,\n Text,\n} from '@backstage/ui';\nimport {\n RiCheckLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiFileCopyLine,\n} from '@remixicon/react';\nimport { siClaude } from 'simple-icons';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n expandInstallFrameworks,\n getAgentInstallLinks,\n getBodyShape,\n getMarketplaceAddCommands,\n getMcpInstallLinks,\n getMarketplaceInstallTemplate,\n getMarketplaceRepoSlug,\n getMarketplaceTeamSnippet,\n getInstallSteps,\n getResourceInstallTarget,\n hasCopyableBody,\n hasDownloadableArtifact,\n type FrameworkToken,\n type MarketplaceAddCommand,\n type ResourceInstallTarget,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport {\n devAiHubResourceApiRef,\n type ResourceBody,\n} from '../../api/DevAiHubResourceClient';\nimport { useTrackTelemetry } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { AppLaunchScope } from './AppLaunchScope';\nimport { CollapsibleSection } from './CollapsibleSection';\nimport { JsonBody } from './JsonBody';\nimport { frameworkLabel } from './typeMeta';\nimport styles from './ResourceInstallDialog.module.css';\n\ninterface ResourceInstallDialogProps {\n resource: ResourceSummary;\n body?: ResourceBody;\n /**\n * Whether the body is still being fetched. The mcp-config launchers are\n * derived from it, so the dialog-open telemetry fallback below must not\n * conclude \"no launcher\" while it is still in flight.\n */\n bodyLoading?: boolean;\n isOpen: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst TYPE_HINTS: Record<ResourceSummary['type'], string> = {\n skill:\n 'Download the skill (multi-file skills arrive as one zip) and extract it into the path for your framework.',\n agent:\n 'Download or copy the agent definition into the path for your framework.',\n hook: 'Register the hook definition at the path for your framework — most hosts merge it into a shared settings file.',\n 'mcp-config': 'Add this server entry to your MCP configuration file.',\n plugin:\n 'This plugin installs through its framework — follow the instructions below.',\n marketplace:\n 'Register this marketplace with your AI tool once, then install any of its plugins from it.',\n};\n\n/** The VS Code chevron mark (devicon's mono path), for a button's iconStart slot. */\nfunction VsCodeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 128 128\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64 1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68 50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z\"\n />\n </svg>\n );\n}\n\n/** The Claude starburst, sized for a button's iconStart slot. */\nfunction ClaudeIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={16}\n height={16}\n fill=\"currentColor\"\n aria-hidden\n >\n <path d={siClaude.path} />\n </svg>\n );\n}\n\n/**\n * The one-click install links for the types with a *native* install route,\n * rendered once in the actions row. `skill` and `hook` are absent on purpose:\n * their launchers are per-host and live in their own rows, so returning them\n * here too would render every button twice.\n */\nfunction getInstallLinks(\n resource: ResourceSummary,\n frameworks: string[],\n body?: ResourceBody,\n) {\n if (resource.type === 'agent') {\n return getAgentInstallLinks(\n resource.sourceLocation,\n resource.name,\n frameworks,\n );\n }\n if (resource.type === 'mcp-config') {\n return getMcpInstallLinks(frameworks, resource.name, body?.content);\n }\n return [];\n}\n\n/** Brand styling for the known deep-link hosts, keyed by link label. */\nfunction launchBrand(label: string): {\n className?: string;\n icon: React.ReactElement;\n} {\n switch (label) {\n case 'Claude':\n return { className: styles.claudeLaunch, icon: <ClaudeIcon /> };\n case 'VS Code':\n return { className: styles.vscodeLaunch, icon: <VsCodeIcon /> };\n case 'VS Code Insiders':\n return { className: styles.insidersLaunch, icon: <VsCodeIcon /> };\n case 'Cursor':\n return {\n className: styles.cursorLaunch,\n icon: <ToolIcon tool=\"cursor\" branded={false} size={16} />,\n };\n default:\n return { icon: <RiExternalLinkLine /> };\n }\n}\n\n/**\n * Icon-only copy button for a single generated command (marketplace journey\n * rows); flashes a check mark as the copied feedback.\n */\nfunction CopyCommandButton({ text }: { text: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <ButtonIcon\n size=\"small\"\n variant=\"tertiary\"\n icon={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n aria-label={copied ? 'Copied' : 'Copy command'}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n />\n );\n}\n\n/**\n * Clipboard fallback for a host with no prompt URI (Copilot, Gemini). It\n * carries the identical instruction a launcher would pre-fill, so the only\n * difference is the delivery — the user pastes it into their own agent.\n */\nfunction CopyPromptButton({ text, label }: { text: string; label: string }) {\n const [copied, setCopied] = useState(false);\n return (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={copied ? <RiCheckLine /> : <RiFileCopyLine />}\n onPress={async () => {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }}\n >\n {copied ? 'Copied!' : `Copy prompt for ${label}`}\n </Button>\n );\n}\n\n/**\n * The two-step marketplace journey (ADR-0010): ① register the catalog with\n * the AI tool, ② install plugins from it — plus a team-distribution snippet.\n * Whether this can render at all is decided by the caller, which needs the\n * same answer to know if the body doc is still required as a fallback.\n */\nfunction MarketplaceJourney({\n resource,\n repoSlug,\n addCommands,\n onLaunch,\n}: {\n resource: ResourceSummary;\n repoSlug: string;\n addCommands: MarketplaceAddCommand[];\n /**\n * Records the `install`, with the launched host as `tool` (ADR-0011).\n * `framework` is optional on the published `ResourceDeepLink`; a generated\n * link always carries one, and an event without a `tool` is still a\n * truthful install.\n */\n onLaunch: (framework: string | undefined) => void;\n}) {\n return (\n <div className={styles.journey}>\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 1. Add the marketplace to your AI tool\n </Text>\n <dl className={styles.commandList}>\n {addCommands.map(({ framework, command, deepLinks }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.commandCell}>\n <code>{command}</code>\n <CopyCommandButton text={command} />\n </dd>\n {deepLinks.length > 0 && (\n <dd className={styles.deepLinks}>\n {deepLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() => onLaunch(link.framework)}\n >\n Add in {link.label}\n </ButtonLink>\n );\n })}\n </dd>\n )}\n </div>\n ))}\n </dl>\n\n <Text variant=\"body-small\" as=\"p\" className={styles.stepTitle}>\n 2. Install plugins from it\n </Text>\n <div className={styles.commandCell}>\n <code>{getMarketplaceInstallTemplate(resource.name)}</code>\n </div>\n\n <details className={styles.teamSection}>\n <summary>For teams: auto-install via project settings</summary>\n <Text variant=\"body-x-small\" as=\"p\">\n Commit this to your repository’s <code>.claude/settings.json</code>{' '}\n and collaborators are prompted to install the marketplace when they\n trust the folder.\n </Text>\n <pre className={styles.codeBlock}>\n <code>{getMarketplaceTeamSnippet(resource.name, repoSlug)}</code>\n </pre>\n <CopyCommandButton\n text={getMarketplaceTeamSnippet(resource.name, repoSlug)}\n />\n </details>\n </div>\n );\n}\n\n/**\n * Install guidance wired to the body resolver (issue #30): copy the body,\n * download the artifact (file or zip, ADR-0009), and per-framework install\n * paths. The body is canonical — nothing here is reconstructed from\n * annotations (CONTEXT.md: body).\n */\nexport function ResourceInstallDialog({\n resource,\n body,\n bodyLoading = false,\n isOpen,\n onOpenChange,\n}: ResourceInstallDialogProps) {\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const [copied, setCopied] = useState(false);\n const [downloadError, setDownloadError] = useState(false);\n\n const shape = getBodyShape(resource.type);\n // The deployment allow-list removed every framework this resource declared.\n // An empty `frameworks` would otherwise fall back to the neutral `default`\n // path and every native launcher, re-exposing what the allow-list suppressed\n // — so skip all generated install content and say why. Copy/Download stay.\n const frameworksRestricted = !!resource.frameworksRestricted;\n // What install-path / launcher generation is scoped to: `installFrameworks`\n // when the backend inferred a scoped set (a bare resource under an active\n // allow-list), otherwise the declared-and-filtered `frameworks`. Badges\n // still read `frameworks` only.\n const installFrameworks = resource.installFrameworks ?? resource.frameworks;\n const installLinks = frameworksRestricted\n ? []\n : getInstallLinks(resource, installFrameworks, body);\n\n // A marketplace body doc repeats the generated journey almost verbatim —\n // same two steps, same commands — so rendering both doubles the dialog's\n // height to say one thing twice. The journey wins when it can be derived;\n // the body stays as ADR-0010's fallback for when the slug cannot be.\n const marketplaceRepoSlug =\n resource.type === 'marketplace'\n ? getMarketplaceRepoSlug(resource.sourceLocation)\n : undefined;\n const marketplaceAddCommands =\n marketplaceRepoSlug && !frameworksRestricted\n ? getMarketplaceAddCommands(installFrameworks, marketplaceRepoSlug)\n : [];\n const hasMarketplaceJourney = marketplaceAddCommands.length > 0;\n // plugin/marketplace bodies are the real manifest JSON (ADR-0014), so this\n // markdown fallback can only fire if shape is still 'markdown' — kept for\n // the (currently unreachable) case bodyShape ever becomes content-driven.\n const showBodyDoc =\n shape === 'markdown' &&\n (resource.type === 'plugin' ||\n (resource.type === 'marketplace' && !hasMarketplaceJourney));\n\n // skill/hook install by instructing the agent, so each host gets a row that\n // is actionable on its own: a launcher where the host has a prompt URI, a\n // copyable prompt where it does not. Other types keep the plain path list —\n // their launchers are native routes, rendered once in the actions row.\n const usesPromptInstall =\n resource.type === 'skill' || resource.type === 'hook';\n const installSteps =\n usesPromptInstall && !frameworksRestricted\n ? getInstallSteps(\n resource.type,\n resource.sourceLocation,\n resource.name,\n installFrameworks,\n )\n : [];\n const pathRows = frameworksRestricted\n ? []\n : expandInstallFrameworks(installFrameworks)\n .map(f => ({\n framework: f,\n target: getResourceInstallTarget(resource.type, f, resource.name),\n }))\n .filter(\n (row): row is { framework: string; target: ResourceInstallTarget } =>\n !!row.target,\n );\n const hasMergeTarget = (\n installSteps.length > 0\n ? installSteps.map(s => s.target)\n : pathRows.map(r => r.target)\n ).some(t => t.mode === 'merge');\n\n /**\n * Whether *this* dialog actually produced a launch button. Deliberately\n * per-resource, not per-type: a skill declaring only Copilot/Gemini, an\n * agent with no resolvable source URL, an unparseable or multi-server MCP\n * body, and a marketplace with no derivable repo slug all render a path and\n * no button, and ADR-0011 requires those to fall back to the dialog-open\n * signal rather than record nothing at all.\n */\n const hasLauncher =\n installLinks.length > 0 ||\n installSteps.some(step => step.link) ||\n marketplaceAddCommands.some(command => command.deepLinks.length > 0);\n\n /**\n * `mcp-config` alone derives its launchers from the body\n * (`getMcpInstallLinks`); every other type builds them from\n * `sourceLocation` and the declared frameworks. So only it may wait for the\n * fetch — for the rest the body is only ever Copy/Download material, and\n * waiting on it would drop the event entirely when the dialog is closed\n * before an unrelated request settles.\n */\n const awaitingMcpBody = resource.type === 'mcp-config' && bodyLoading;\n\n /**\n * The fallback arm of ADR-0011's \"record `install` at the most specific\n * point available\": with no launcher to wait for, opening the dialog is\n * that point.\n *\n * Skipped when the deployment allow-list left the resource with no install\n * path at all (post-dates this ADR): nothing is installable, so an\n * `install` event would claim something that cannot have happened.\n */\n useEffect(() => {\n if (!isOpen || frameworksRestricted || hasLauncher || awaitingMcpBody) {\n return;\n }\n track(resource.entityRef, 'install');\n // `awaitingMcpBody`, not the raw `bodyLoading`: for every other type the\n // body settling is unrelated to launcher availability, and depending on\n // it would re-run this effect after the event had already been recorded\n // — one install, counted twice.\n }, [\n isOpen,\n awaitingMcpBody,\n frameworksRestricted,\n hasLauncher,\n resource.entityRef,\n track,\n ]);\n\n const handleCopy = async () => {\n if (!body) return;\n await navigator.clipboard.writeText(body.content);\n track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = async () => {\n setDownloadError(false);\n try {\n await api.downloadEntityBody(resource.entityRef);\n track(resource.entityRef, 'download');\n } catch {\n setDownloadError(true);\n }\n };\n\n return (\n <Dialog\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n /* Size to the widest command line so generated commands stay unwrapped,\n clamped so prose-heavy bodies can't balloon the dialog. The height cap\n keeps a long fallback body scrolling inside the dialog rather than\n running off-screen — the install actions must always stay reachable. */\n style={{\n width: 'fit-content',\n minWidth: 'min(480px, calc(100vw - 3rem))',\n maxWidth: 'min(760px, calc(100vw - 3rem))',\n maxHeight: 'min(80vh, calc(100vh - 4rem))',\n }}\n >\n <DialogHeader>Install {resource.title ?? resource.name}</DialogHeader>\n <DialogBody>\n {/* Every launcher here is a custom-scheme deep link — keep the app's\n router from rewriting it (see AppLaunchScope). */}\n <AppLaunchScope>\n <div className={styles.content}>\n <Text variant=\"body-small\" as=\"p\">\n {frameworksRestricted\n ? 'No install path is available — none of the tools this resource ' +\n 'supports are enabled for this Backstage. You can still copy or ' +\n 'download the content below.'\n : TYPE_HINTS[resource.type]}\n </Text>\n\n {body && shape === 'json' && (\n <CollapsibleSection label=\"Content\">\n <JsonBody content={body.content} className={styles.codeBlock} />\n </CollapsibleSection>\n )}\n\n {hasMarketplaceJourney && marketplaceRepoSlug && (\n <MarketplaceJourney\n resource={resource}\n repoSlug={marketplaceRepoSlug}\n addCommands={marketplaceAddCommands}\n onLaunch={framework =>\n track(resource.entityRef, 'install', framework)\n }\n />\n )}\n\n {body && showBodyDoc && (\n <div className={styles.markdown}>\n <ReactMarkdown>{body.content}</ReactMarkdown>\n </div>\n )}\n\n {installSteps.length > 0 && (\n <dl className={styles.commandList}>\n {installSteps.map(({ framework, target, prompt, link }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework as FrameworkToken} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.pathRow}>\n <code>{target.path}</code>\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n <dd className={styles.deepLinks}>\n {link ? (\n (() => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })()\n ) : (\n /* No prompt URI for this host — the same instruction,\n delivered by clipboard so the row is still actionable. */\n <CopyPromptButton\n text={prompt}\n label={frameworkLabel(framework)}\n />\n )}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {installSteps.length === 0 && pathRows.length > 0 && (\n <>\n <dl className={styles.pathList}>\n {pathRows.map(({ framework, target }) => (\n <div key={framework} className={styles.pathRow}>\n <dt>\n {framework === 'default'\n ? 'Any framework'\n : frameworkLabel(framework)}\n </dt>\n <dd>\n <code>{target.path}</code>\n {/* A merge target is a file the user already owns —\n flagged inline so the path is never mistaken for a\n drop-in destination to overwrite. */}\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n </div>\n ))}\n </dl>\n {hasMergeTarget && (\n <Text\n variant=\"body-x-small\"\n as=\"p\"\n className={styles.mergeNote}\n >\n Paths marked <strong>merge into</strong> are shared\n configuration files. Add this entry to the existing file —\n replacing it discards your other settings.\n </Text>\n )}\n </>\n )}\n\n <div className={styles.actions}>\n {installLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', link.framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })}\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy content'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n </div>\n {downloadError && (\n <Text variant=\"body-x-small\" as=\"p\">\n Download failed — the source may be unreachable. Try again or\n use “View source”.\n </Text>\n )}\n </div>\n </AppLaunchScope>\n </DialogBody>\n </Dialog>\n );\n}\n"],"names":[],"mappings":"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"}
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 MarketplaceAddCommand,\n type ResourceInstallTarget,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport {\n devAiHubResourceApiRef,\n type ResourceBody,\n} from '../../api/DevAiHubResourceClient';\nimport { useTrackTelemetry } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { AppLaunchScope } from './AppLaunchScope';\nimport { CollapsibleSection } from './CollapsibleSection';\nimport { JsonBody } from './JsonBody';\nimport { useFrameworkLabel } from '../../hooks/useFrameworkLabel';\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 const frameworkLabel = useFrameworkLabel();\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} 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 frameworkLabel = useFrameworkLabel();\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const [copied, setCopied] = useState(false);\n const [downloadError, setDownloadError] = useState(false);\n\n const shape = getBodyShape(resource.type);\n // The deployment allow-list removed every framework this resource declared.\n // An empty `frameworks` would otherwise fall back to the neutral `default`\n // path and every native launcher, re-exposing what the allow-list suppressed\n // — so skip all generated install content and say why. Copy/Download stay.\n const frameworksRestricted = !!resource.frameworksRestricted;\n // What install-path / launcher generation is scoped to: `installFrameworks`\n // when the backend inferred a scoped set (a bare resource under an active\n // allow-list), otherwise the declared-and-filtered `frameworks`. Badges\n // still read `frameworks` only.\n const installFrameworks = resource.installFrameworks ?? resource.frameworks;\n const installLinks = frameworksRestricted\n ? []\n : getInstallLinks(resource, installFrameworks, body);\n\n // A marketplace body doc repeats the generated journey almost verbatim —\n // same two steps, same commands — so rendering both doubles the dialog's\n // height to say one thing twice. The journey wins when it can be derived;\n // the body stays as ADR-0010's fallback for when the slug cannot be.\n const marketplaceRepoSlug =\n resource.type === 'marketplace'\n ? getMarketplaceRepoSlug(resource.sourceLocation)\n : undefined;\n const marketplaceAddCommands =\n marketplaceRepoSlug && !frameworksRestricted\n ? getMarketplaceAddCommands(installFrameworks, marketplaceRepoSlug)\n : [];\n const hasMarketplaceJourney = marketplaceAddCommands.length > 0;\n // plugin/marketplace bodies are the real manifest JSON (ADR-0014), so this\n // markdown fallback can only fire if shape is still 'markdown' — kept for\n // the (currently unreachable) case bodyShape ever becomes content-driven.\n const showBodyDoc =\n shape === 'markdown' &&\n (resource.type === 'plugin' ||\n (resource.type === 'marketplace' && !hasMarketplaceJourney));\n\n // skill/hook install by instructing the agent, so each host gets a row that\n // is actionable on its own: a launcher where the host has a prompt URI, a\n // copyable prompt where it does not. Other types keep the plain path list —\n // their launchers are native routes, rendered once in the actions row.\n const usesPromptInstall =\n resource.type === 'skill' || resource.type === 'hook';\n const installSteps =\n usesPromptInstall && !frameworksRestricted\n ? getInstallSteps(\n resource.type,\n resource.sourceLocation,\n resource.name,\n installFrameworks,\n )\n : [];\n const pathRows = frameworksRestricted\n ? []\n : expandInstallFrameworks(installFrameworks)\n .map(f => ({\n framework: f,\n target: getResourceInstallTarget(resource.type, f, resource.name),\n }))\n .filter(\n (row): row is { framework: string; target: ResourceInstallTarget } =>\n !!row.target,\n );\n const hasMergeTarget = (\n installSteps.length > 0\n ? installSteps.map(s => s.target)\n : pathRows.map(r => r.target)\n ).some(t => t.mode === 'merge');\n\n /**\n * Whether *this* dialog actually produced a launch button. Deliberately\n * per-resource, not per-type: a skill declaring only Copilot/Gemini, an\n * agent with no resolvable source URL, an unparseable or multi-server MCP\n * body, and a marketplace with no derivable repo slug all render a path and\n * no button, and ADR-0011 requires those to fall back to the dialog-open\n * signal rather than record nothing at all.\n */\n const hasLauncher =\n installLinks.length > 0 ||\n installSteps.some(step => step.link) ||\n marketplaceAddCommands.some(command => command.deepLinks.length > 0);\n\n /**\n * `mcp-config` alone derives its launchers from the body\n * (`getMcpInstallLinks`); every other type builds them from\n * `sourceLocation` and the declared frameworks. So only it may wait for the\n * fetch — for the rest the body is only ever Copy/Download material, and\n * waiting on it would drop the event entirely when the dialog is closed\n * before an unrelated request settles.\n */\n const awaitingMcpBody = resource.type === 'mcp-config' && bodyLoading;\n\n /**\n * The fallback arm of ADR-0011's \"record `install` at the most specific\n * point available\": with no launcher to wait for, opening the dialog is\n * that point.\n *\n * Skipped when the deployment allow-list left the resource with no install\n * path at all (post-dates this ADR): nothing is installable, so an\n * `install` event would claim something that cannot have happened.\n */\n useEffect(() => {\n if (!isOpen || frameworksRestricted || hasLauncher || awaitingMcpBody) {\n return;\n }\n track(resource.entityRef, 'install');\n // `awaitingMcpBody`, not the raw `bodyLoading`: for every other type the\n // body settling is unrelated to launcher availability, and depending on\n // it would re-run this effect after the event had already been recorded\n // — one install, counted twice.\n }, [\n isOpen,\n awaitingMcpBody,\n frameworksRestricted,\n hasLauncher,\n resource.entityRef,\n track,\n ]);\n\n const handleCopy = async () => {\n if (!body) return;\n await navigator.clipboard.writeText(body.content);\n track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = async () => {\n setDownloadError(false);\n try {\n await api.downloadEntityBody(resource.entityRef);\n track(resource.entityRef, 'download');\n } catch {\n setDownloadError(true);\n }\n };\n\n return (\n <Dialog\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n /* Size to the widest command line so generated commands stay unwrapped,\n clamped so prose-heavy bodies can't balloon the dialog. The height cap\n keeps a long fallback body scrolling inside the dialog rather than\n running off-screen — the install actions must always stay reachable. */\n style={{\n width: 'fit-content',\n minWidth: 'min(480px, calc(100vw - 3rem))',\n maxWidth: 'min(760px, calc(100vw - 3rem))',\n maxHeight: 'min(80vh, calc(100vh - 4rem))',\n }}\n >\n <DialogHeader>Install {resource.title ?? resource.name}</DialogHeader>\n <DialogBody>\n {/* Every launcher here is a custom-scheme deep link — keep the app's\n router from rewriting it (see AppLaunchScope). */}\n <AppLaunchScope>\n <div className={styles.content}>\n <Text variant=\"body-small\" as=\"p\">\n {frameworksRestricted\n ? 'No install path is available — none of the tools this resource ' +\n 'supports are enabled for this Backstage. You can still copy or ' +\n 'download the content below.'\n : TYPE_HINTS[resource.type]}\n </Text>\n\n {body && shape === 'json' && (\n <CollapsibleSection label=\"Content\">\n <JsonBody content={body.content} className={styles.codeBlock} />\n </CollapsibleSection>\n )}\n\n {hasMarketplaceJourney && marketplaceRepoSlug && (\n <MarketplaceJourney\n resource={resource}\n repoSlug={marketplaceRepoSlug}\n addCommands={marketplaceAddCommands}\n onLaunch={framework =>\n track(resource.entityRef, 'install', framework)\n }\n />\n )}\n\n {body && showBodyDoc && (\n <div className={styles.markdown}>\n <ReactMarkdown>{body.content}</ReactMarkdown>\n </div>\n )}\n\n {installSteps.length > 0 && (\n <dl className={styles.commandList}>\n {installSteps.map(({ framework, target, prompt, link }) => (\n <div key={framework} className={styles.commandRow}>\n <dt className={styles.commandLabel}>\n <ToolIcon tool={framework} size={16} />\n {frameworkLabel(framework)}\n </dt>\n <dd className={styles.pathRow}>\n <code>{target.path}</code>\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n <dd className={styles.deepLinks}>\n {link ? (\n (() => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })()\n ) : (\n /* No prompt URI for this host — the same instruction,\n delivered by clipboard so the row is still actionable. */\n <CopyPromptButton\n text={prompt}\n label={frameworkLabel(framework)}\n />\n )}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {installSteps.length === 0 && pathRows.length > 0 && (\n <>\n <dl className={styles.pathList}>\n {pathRows.map(({ framework, target }) => (\n <div key={framework} className={styles.pathRow}>\n <dt>\n {framework === 'default'\n ? 'Any framework'\n : frameworkLabel(framework)}\n </dt>\n <dd>\n <code>{target.path}</code>\n {/* A merge target is a file the user already owns —\n flagged inline so the path is never mistaken for a\n drop-in destination to overwrite. */}\n {target.mode === 'merge' && (\n <span className={styles.mergeBadge}>merge into</span>\n )}\n </dd>\n </div>\n ))}\n </dl>\n {hasMergeTarget && (\n <Text\n variant=\"body-x-small\"\n as=\"p\"\n className={styles.mergeNote}\n >\n Paths marked <strong>merge into</strong> are shared\n configuration files. Add this entry to the existing file —\n replacing it discards your other settings.\n </Text>\n )}\n </>\n )}\n\n <div className={styles.actions}>\n {installLinks.map(link => {\n const brand = launchBrand(link.label);\n return (\n <ButtonLink\n key={link.label}\n size=\"small\"\n variant=\"secondary\"\n className={brand.className}\n iconStart={brand.icon}\n href={link.href}\n onPress={() =>\n track(resource.entityRef, 'install', link.framework)\n }\n >\n Install in {link.label}\n </ButtonLink>\n );\n })}\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy content'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n </div>\n {downloadError && (\n <Text variant=\"body-x-small\" as=\"p\">\n Download failed — the source may be unreachable. Try again or\n use “View source”.\n </Text>\n )}\n </div>\n </AppLaunchScope>\n </DialogBody>\n </Dialog>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { Text } from '@backstage/ui';
4
+ import { RiText, RiTimeLine, RiFireLine, RiCheckLine, RiSortAsc, RiSortDesc } from '@remixicon/react';
5
+ import { SORT_MODES, NATURAL_SORT_DIRECTION } from '@nospt/plugin-dev-ai-hub-common';
6
+ import styles from './ResourceFilters.module.css.esm.js';
7
+ import sortStyles from './SortControl.module.css.esm.js';
8
+
9
+ const MODE_LABELS = {
10
+ usage: "Most popular",
11
+ newest: "Newest",
12
+ name: "Name"
13
+ };
14
+ const MODE_ICONS = {
15
+ usage: RiFireLine,
16
+ newest: RiTimeLine,
17
+ name: RiText
18
+ };
19
+ const DIRECTION_LABEL = {
20
+ asc: "Ascending",
21
+ desc: "Descending"
22
+ };
23
+ function useDropdownClose(ref, onClose, open) {
24
+ useEffect(() => {
25
+ const handleClick = (e) => {
26
+ if (ref.current && !ref.current.contains(e.target)) onClose();
27
+ };
28
+ const handleKey = (e) => {
29
+ if (e.key === "Escape") onClose();
30
+ };
31
+ if (open) {
32
+ document.addEventListener("mousedown", handleClick);
33
+ document.addEventListener("keydown", handleKey);
34
+ }
35
+ return () => {
36
+ document.removeEventListener("mousedown", handleClick);
37
+ document.removeEventListener("keydown", handleKey);
38
+ };
39
+ }, [open, ref, onClose]);
40
+ }
41
+ function SortControl({ mode, direction, onChange }) {
42
+ const [open, setOpen] = useState(false);
43
+ const containerRef = useRef(null);
44
+ useDropdownClose(containerRef, () => setOpen(false), open);
45
+ const CurrentIcon = MODE_ICONS[mode];
46
+ const selectMode = (next) => {
47
+ onChange(next, next === mode ? direction : NATURAL_SORT_DIRECTION[next]);
48
+ setOpen(false);
49
+ };
50
+ const toggleDirection = () => {
51
+ onChange(mode, direction === "asc" ? "desc" : "asc");
52
+ };
53
+ return /* @__PURE__ */ jsxs("div", { className: sortStyles.sortControl, children: [
54
+ /* @__PURE__ */ jsxs("div", { className: styles.filterBox, children: [
55
+ /* @__PURE__ */ jsx(
56
+ Text,
57
+ {
58
+ variant: "body-x-small",
59
+ color: "secondary",
60
+ className: styles.filterLabel,
61
+ children: "Sort by"
62
+ }
63
+ ),
64
+ /* @__PURE__ */ jsxs("div", { ref: containerRef, className: styles.tagsDropdown, children: [
65
+ /* @__PURE__ */ jsxs(
66
+ "button",
67
+ {
68
+ type: "button",
69
+ className: styles.tagsDropdownTrigger,
70
+ onClick: () => setOpen((o) => !o),
71
+ "aria-expanded": open,
72
+ "aria-label": "Sort by",
73
+ "aria-haspopup": "listbox",
74
+ children: [
75
+ /* @__PURE__ */ jsxs("span", { className: styles.iconSelectTriggerContent, children: [
76
+ /* @__PURE__ */ jsx(
77
+ CurrentIcon,
78
+ {
79
+ size: 14,
80
+ style: { color: "var(--bui-fg-secondary)", flexShrink: 0 }
81
+ }
82
+ ),
83
+ /* @__PURE__ */ jsx("span", { children: MODE_LABELS[mode] })
84
+ ] }),
85
+ /* @__PURE__ */ jsx("span", { className: styles.tagsDropdownArrow, children: "\u25BE" })
86
+ ]
87
+ }
88
+ ),
89
+ open && /* @__PURE__ */ jsx(
90
+ "div",
91
+ {
92
+ className: styles.tagsDropdownPanel,
93
+ role: "listbox",
94
+ "aria-label": "Sort by",
95
+ children: SORT_MODES.map((m) => {
96
+ const Icon = MODE_ICONS[m];
97
+ return /* @__PURE__ */ jsxs(
98
+ "button",
99
+ {
100
+ type: "button",
101
+ role: "option",
102
+ "aria-selected": m === mode,
103
+ className: `${styles.iconSelectItem} ${m === mode ? styles.iconSelectItemActive : ""}`,
104
+ onClick: () => selectMode(m),
105
+ children: [
106
+ /* @__PURE__ */ jsx("span", { className: styles.iconSelectItemIcon, children: /* @__PURE__ */ jsx(Icon, { size: 14 }) }),
107
+ /* @__PURE__ */ jsx("span", { children: MODE_LABELS[m] }),
108
+ m === mode && /* @__PURE__ */ jsx(
109
+ RiCheckLine,
110
+ {
111
+ size: 14,
112
+ className: styles.iconSelectCheck
113
+ }
114
+ )
115
+ ]
116
+ },
117
+ m
118
+ );
119
+ })
120
+ }
121
+ )
122
+ ] })
123
+ ] }),
124
+ /* @__PURE__ */ jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ className: sortStyles.directionToggle,
129
+ onClick: toggleDirection,
130
+ "aria-label": `Sort direction: ${DIRECTION_LABEL[direction]}`,
131
+ title: DIRECTION_LABEL[direction],
132
+ children: direction === "asc" ? /* @__PURE__ */ jsx(RiSortAsc, { size: 16 }) : /* @__PURE__ */ jsx(RiSortDesc, { size: 16 })
133
+ }
134
+ )
135
+ ] });
136
+ }
137
+
138
+ export { SortControl };
139
+ //# sourceMappingURL=SortControl.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SortControl.esm.js","sources":["../../../src/components/AiResourcesPage/SortControl.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { Text } from '@backstage/ui';\nimport {\n RiCheckLine,\n RiFireLine,\n RiSortAsc,\n RiSortDesc,\n RiText,\n RiTimeLine,\n} from '@remixicon/react';\nimport {\n NATURAL_SORT_DIRECTION,\n SORT_MODES,\n type SortDirection,\n type SortMode,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport styles from './ResourceFilters.module.css';\nimport sortStyles from './SortControl.module.css';\n\nconst MODE_LABELS: Record<SortMode, string> = {\n usage: 'Most popular',\n newest: 'Newest',\n name: 'Name',\n};\n\nconst MODE_ICONS: Record<SortMode, React.ElementType> = {\n usage: RiFireLine,\n newest: RiTimeLine,\n name: RiText,\n};\n\nconst DIRECTION_LABEL: Record<SortDirection, string> = {\n asc: 'Ascending',\n desc: 'Descending',\n};\n\ninterface SortControlProps {\n mode: SortMode;\n direction: SortDirection;\n /** Called with both values together: picking a new mode also resets\n * direction to that mode's natural default (issue #102 design — every\n * mode opens the \"obvious\" way, e.g. Name starts A-Z, Most popular\n * starts highest-first), rather than leaving a stale direction in place. */\n onChange: (mode: SortMode, direction: SortDirection) => void;\n}\n\nfunction useDropdownClose(\n ref: React.RefObject<HTMLElement>,\n onClose: () => void,\n open: boolean,\n) {\n useEffect(() => {\n const handleClick = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) onClose();\n };\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n if (open) {\n document.addEventListener('mousedown', handleClick);\n document.addEventListener('keydown', handleKey);\n }\n return () => {\n document.removeEventListener('mousedown', handleClick);\n document.removeEventListener('keydown', handleKey);\n };\n }, [open, ref, onClose]);\n}\n\n/**\n * The browse page's sort dropdown plus direction toggle (issue #102).\n * Visually set apart from the left-hand filter cluster in `ResourceFilters`\n * — pushed to the row's right edge — since sorting is a distinct concept from\n * filtering, even though it lives on the same row.\n */\nexport function SortControl({ mode, direction, onChange }: SortControlProps) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n useDropdownClose(containerRef, () => setOpen(false), open);\n\n const CurrentIcon = MODE_ICONS[mode];\n\n const selectMode = (next: SortMode) => {\n onChange(next, next === mode ? direction : NATURAL_SORT_DIRECTION[next]);\n setOpen(false);\n };\n\n const toggleDirection = () => {\n onChange(mode, direction === 'asc' ? 'desc' : 'asc');\n };\n\n return (\n <div className={sortStyles.sortControl}>\n <div className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n Sort by\n </Text>\n <div ref={containerRef} className={styles.tagsDropdown}>\n <button\n type=\"button\"\n className={styles.tagsDropdownTrigger}\n onClick={() => setOpen(o => !o)}\n aria-expanded={open}\n aria-label=\"Sort by\"\n aria-haspopup=\"listbox\"\n >\n <span className={styles.iconSelectTriggerContent}>\n <CurrentIcon\n size={14}\n style={{ color: 'var(--bui-fg-secondary)', flexShrink: 0 }}\n />\n <span>{MODE_LABELS[mode]}</span>\n </span>\n <span className={styles.tagsDropdownArrow}>▾</span>\n </button>\n {open && (\n <div\n className={styles.tagsDropdownPanel}\n role=\"listbox\"\n aria-label=\"Sort by\"\n >\n {SORT_MODES.map(m => {\n const Icon = MODE_ICONS[m];\n return (\n <button\n key={m}\n type=\"button\"\n role=\"option\"\n aria-selected={m === mode}\n className={`${styles.iconSelectItem} ${\n m === mode ? styles.iconSelectItemActive : ''\n }`}\n onClick={() => selectMode(m)}\n >\n <span className={styles.iconSelectItemIcon}>\n <Icon size={14} />\n </span>\n <span>{MODE_LABELS[m]}</span>\n {m === mode && (\n <RiCheckLine\n size={14}\n className={styles.iconSelectCheck}\n />\n )}\n </button>\n );\n })}\n </div>\n )}\n </div>\n </div>\n\n <button\n type=\"button\"\n className={sortStyles.directionToggle}\n onClick={toggleDirection}\n aria-label={`Sort direction: ${DIRECTION_LABEL[direction]}`}\n title={DIRECTION_LABEL[direction]}\n >\n {direction === 'asc' ? (\n <RiSortAsc size={16} />\n ) : (\n <RiSortDesc size={16} />\n )}\n </button>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = "/*\n * The sort control sits at the right edge of the same row as the Type/Tags/\n * AI Tool filters (`ResourceFilters.module.css`'s `.filtersRow`) — pushed\n * there with its own left margin so it doesn't blend into the filter\n * cluster, even without a divider line marking the boundary.\n */\n@layer components {\n .SortControl_sortControl__c287d981cd {\n display: flex;\n align-items: flex-end;\n gap: var(--bui-space-2);\n margin-left: auto;\n }\n\n /* Narrow viewports: drop the auto-margin so the control wraps under the\n filters instead of colliding with them. */\n @media (max-width: 640px) {\n .SortControl_sortControl__c287d981cd {\n margin-left: 0;\n width: 100%;\n }\n }\n\n .SortControl_directionToggle__c287d981cd {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-2);\n background: Canvas;\n color: CanvasText;\n cursor: pointer;\n flex-shrink: 0;\n }\n\n .SortControl_directionToggle__c287d981cd:hover {\n border-color: var(--bui-fg-primary);\n }\n}\n";
4
+ var sortStyles = {"sortControl":"SortControl_sortControl__c287d981cd","directionToggle":"SortControl_directionToggle__c287d981cd"};
5
+ styleInject(css_248z);
6
+
7
+ export { sortStyles as default };
8
+ //# sourceMappingURL=SortControl.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SortControl.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,6 @@
1
+ function formatOwnerLabel(owner) {
2
+ return owner?.replace(/^(group|user):(default\/)?/, "");
3
+ }
4
+
5
+ export { formatOwnerLabel };
6
+ //# sourceMappingURL=ownerLabel.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownerLabel.esm.js","sources":["../../../src/components/AiResourcesPage/ownerLabel.ts"],"sourcesContent":["/**\n * Strips the entity-ref kind/namespace prefix from an owner ref for display\n * (`group:default/team-a` -> `team-a`) — `owner` is shown as a plain label\n * here, not linked, so the ref shorthand only adds noise.\n */\nexport function formatOwnerLabel(owner?: string): string | undefined {\n return owner?.replace(/^(group|user):(default\\/)?/, '');\n}\n"],"names":[],"mappings":"AAKO,SAAS,iBAAiB,KAAA,EAAoC;AACnE,EAAA,OAAO,KAAA,EAAO,OAAA,CAAQ,4BAAA,EAA8B,EAAE,CAAA;AACxD;;;;"}
@@ -19,17 +19,6 @@ function getTypeMeta(type) {
19
19
  tileFill: `var(--devaihub-tile-${info.colorRole})`
20
20
  };
21
21
  }
22
- const FRAMEWORK_LABELS = {
23
- "claude-code": "Claude Code",
24
- "github-copilot": "GitHub Copilot",
25
- "google-gemini": "Google Gemini",
26
- cursor: "Cursor",
27
- opencode: "OpenCode",
28
- all: "All tools"
29
- };
30
- function frameworkLabel(token) {
31
- return FRAMEWORK_LABELS[token] ?? token;
32
- }
33
22
 
34
- export { frameworkLabel, getTypeMeta };
23
+ export { getTypeMeta };
35
24
  //# sourceMappingURL=typeMeta.esm.js.map
@@ -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 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
+ {"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"],"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;;;;"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = "@layer components {\n .Backoffice_surface__8dd475cbef {\n padding: var(--bui-space-6);\n /* Stacked sections (e.g. Overview's \"Catalog overview\" + \"Usage\") need\n air between them — without this a section's last row touches the next\n section's heading directly. */\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-8);\n }\n\n .Backoffice_section__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-4);\n }\n\n .Backoffice_sectionHeader__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n }\n\n /* Shared frame for the \"loading failed\" and \"nothing here\" states, so a\n section never invents its own empty treatment. */\n .Backoffice_stateBlock__8dd475cbef {\n border: 1px dashed var(--bui-border-1);\n border-radius: var(--bui-radius-4);\n padding: var(--bui-space-7) var(--bui-space-4);\n text-align: center;\n }\n\n .Backoffice_grid__8dd475cbef {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: var(--bui-space-3);\n }\n\n /* The catalog's headline count, above the per-type grid (#83) — the grid\n answers \"which types\", this answers \"how many, in total\". */\n .Backoffice_totalSummary__8dd475cbef {\n display: flex;\n align-items: baseline;\n gap: var(--bui-space-2);\n }\n\n /* The \"By type\" stacked bar (#83): one glance at the catalog's\n composition, coloured with each type's identity (ADR-0008) — the grid\n below it already has the exact numbers, this is the shape of them. */\n .Backoffice_distributionBar__8dd475cbef {\n display: flex;\n height: var(--bui-space-3);\n border-radius: var(--bui-radius-full, 999px);\n overflow: hidden;\n background: var(--bui-bg-neutral-2);\n }\n\n .Backoffice_distributionSegment__8dd475cbef {\n background: var(--card-accent, var(--bui-fg-secondary));\n min-width: 2px;\n transition: opacity 160ms ease;\n }\n\n .Backoffice_distributionSegment__8dd475cbef:hover {\n opacity: 0.8;\n }\n\n .Backoffice_countCard__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n padding: var(--bui-space-4);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-4);\n background: var(--bui-bg-neutral-1);\n transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1),\n border-color 160ms ease, box-shadow 160ms ease;\n }\n\n /* The same quiet lift the browse-page cards get on hover\n (ResourceCard.module.css / StatTiles.module.css) — a small rise and a\n shadow so the row reads as a surface that sits on the page, not flat,\n inert chrome. */\n .Backoffice_countCard__8dd475cbef:hover {\n transform: translateY(-2px);\n border-color: color-mix(\n in srgb,\n var(--card-accent, var(--bui-border-1)) 30%,\n var(--bui-border-1)\n );\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08),\n 0 10px 20px -14px rgba(0, 0, 0, 0.35);\n }\n\n /* The \"By type\" row is the page's single vibrant grid, echoing the browse\n page's StatTiles (ADR-0008 identity colours): a soft tint of the type's\n own colour, its icon and headline percent in that colour, and the same\n hue-tinted lift on hover. Read-only here, so a soft tint rather than the\n browse page's saturated block — colourful enough to read the catalog's\n shape at a glance, without implying the tile is a button. */\n .Backoffice_typeCard__8dd475cbef {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n padding: var(--bui-space-4);\n border: 1px solid\n color-mix(in srgb, var(--card-accent, var(--bui-border)) 22%, transparent);\n border-radius: var(--bui-radius-4);\n background: var(--card-accent-bg, var(--bui-bg-surface-1));\n transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1),\n border-color 160ms ease, box-shadow 160ms ease;\n }\n\n .Backoffice_typeCard__8dd475cbef:hover {\n transform: translateY(-2px);\n border-color: color-mix(in srgb, var(--card-accent) 45%, transparent);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08),\n 0 12px 24px -12px color-mix(in srgb, var(--card-accent) 55%, transparent);\n }\n\n /* A solid surface chip on the tinted card — the inverse of the browse\n page's tinted-icon-on-white card, mirroring StatTiles' translucent panel\n on a coloured tile, so the icon stays legible against the tint. */\n .Backoffice_typeCardIcon__8dd475cbef {\n background: var(--bui-bg-surface-1);\n color: var(--card-accent);\n border-radius: var(--bui-radius-3);\n padding: var(--bui-space-1);\n display: inline-flex;\n flex-shrink: 0;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);\n }\n\n .Backoffice_typeValue__8dd475cbef {\n font-size: var(--bui-font-size-6);\n font-weight: var(--bui-font-weight-bold);\n line-height: 1.1;\n letter-spacing: -0.02em;\n color: var(--card-accent, var(--bui-fg-primary));\n }\n\n .Backoffice_countCardLabel__8dd475cbef {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n }\n\n /* The small type/lifecycle/framework/usage label beside each card's icon.\n Uppercase, tracked and muted — the same \"eyebrow\" typography the browse\n page gives its card type label (ResourceCard.module.css .typeLabel) and\n its stat-tile labels, so the two surfaces read as one design. Applied\n over BUI's <Text>, whose own size/colour would otherwise win depending on\n stylesheet order — hence colour and size are stated here explicitly. */\n .Backoffice_cardLabel__8dd475cbef {\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n line-height: 1.3;\n color: var(--bui-fg-secondary);\n }\n\n /* The type's identity colour (ADR-0008), boxed exactly like the browse-page\n card's `.iconBox` (same tinted square, `--card-accent`/`--card-accent-bg`\n set per card from typeMeta) — falls back to a neutral tint for rows with\n no type identity of their own (lifecycle, usage), so the icon-then-label\n shape reads as one system everywhere, coloured only where a colour\n actually means something. */\n .Backoffice_countCardIcon__8dd475cbef {\n background: var(--card-accent-bg, var(--bui-bg-neutral-2));\n color: var(--card-accent, var(--bui-fg-secondary));\n border-radius: var(--bui-radius-3);\n padding: var(--bui-space-1);\n display: inline-flex;\n flex-shrink: 0;\n }\n\n .Backoffice_countValue__8dd475cbef {\n font-size: var(--bui-font-size-6);\n font-weight: var(--bui-font-weight-bold);\n line-height: 1.1;\n letter-spacing: -0.02em;\n }\n\n /* A mini proportion bar reused across the count grids and ranked lists\n (#83) — the browse page's StatTiles share bar generalised, so every\n breakdown carries the \"how big is this slice?\" data-ink the raw number\n alone can't, coloured with whatever --card-accent the row already sets. */\n .Backoffice_shareBar__8dd475cbef {\n margin-top: var(--bui-space-2);\n height: 4px;\n border-radius: var(--bui-radius-full, 999px);\n background: color-mix(\n in srgb,\n var(--card-accent, var(--bui-fg-secondary)) 16%,\n transparent\n );\n overflow: hidden;\n }\n\n .Backoffice_shareBarFill__8dd475cbef {\n display: block;\n height: 100%;\n width: var(--share, 0%);\n border-radius: inherit;\n background: var(--card-accent, var(--bui-fg-secondary));\n transition: width 320ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n /* The curation section's list (#68, ADR-0017): one row per resource, not a\n grid. */\n .Backoffice_curationList__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n .Backoffice_curationRow__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n padding: var(--bui-space-3) var(--bui-space-4);\n border: 1px solid var(--bui-border-1);\n /* Same identity stripe as the browse-page card (ResourceCard.module.css)\n — an admin scanning this list gets the type at a glance instead of\n reading the title to guess it. */\n border-left: 2px solid var(--card-accent, var(--bui-border-1));\n border-radius: var(--bui-radius-4);\n background: var(--bui-bg-neutral-1);\n transition: border-color 160ms ease;\n }\n\n .Backoffice_curationRow__8dd475cbef:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent, transparent) 30%,\n var(--bui-border-1)\n );\n border-left-color: var(--card-accent, var(--bui-border-1));\n }\n\n .Backoffice_curationRowMain__8dd475cbef {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-3);\n }\n\n .Backoffice_curationRowTitle__8dd475cbef {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n }\n\n /* The type icon box, matching the browse-page card's iconBox exactly —\n same tinted square, same size ratio, so the identity language reads as\n one system rather than a second one invented for the admin surface. */\n .Backoffice_curationRowIcon__8dd475cbef {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-3);\n padding: var(--bui-space-1);\n display: inline-flex;\n flex-shrink: 0;\n }\n\n /* Same amber mark as the browse-page card badge (ADR-0017), so a curator\n recognises \"this is what everyone else sees\" rather than a second,\n admin-only visual language. */\n .Backoffice_curationRowBadge__8dd475cbef {\n color: var(--bui-warning-bg, #92400e);\n flex-shrink: 0;\n }\n\n /* \"Excluded by X\" attribution (ADR-0019). Secondary and on its own line so\n it never competes with the resource title, but always present while a\n resource is excluded — an exclusion nobody can attribute is one nobody\n will undo. */\n .Backoffice_curationRowMeta__8dd475cbef {\n margin-top: var(--bui-space-1);\n color: var(--bui-fg-secondary);\n }\n\n /* Catalog health / usage rollups (#83 backoffice overview): the eyebrow for\n a second (or third) grid/list stacked under the same section title.\n Separation is by space and by this tracked, muted uppercase treatment —\n the same \"quiet label, not a rule\" idiom the browse card uses for its type\n label (ResourceCard.module.css) — rather than a heavy divider line. */\n .Backoffice_subsectionHeading__8dd475cbef {\n margin-top: var(--bui-space-5);\n font-size: 0.6875rem;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n line-height: 1.3;\n color: var(--bui-fg-primary);\n }\n\n .Backoffice_healthList__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n /* Each health check is a card with a coloured left edge and a matching\n tinted status chip — green when clean, amber when something needs an\n owner's attention — so an admin scans the column's left edge rather than\n reading every line. Mirrors the card + icon-chip language used across the\n rest of this surface. */\n .Backoffice_healthCard__8dd475cbef {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n padding: var(--bui-space-3);\n border: 1px solid var(--bui-border-1);\n border-left: 3px solid var(--health-accent, var(--bui-border-1));\n border-radius: var(--bui-radius-4);\n background: var(--bui-bg-neutral-1);\n }\n\n .Backoffice_healthCardOk__8dd475cbef {\n --health-accent: var(--bui-positive-bg, #15803d);\n }\n\n .Backoffice_healthCardWarning__8dd475cbef {\n --health-accent: var(--bui-warning-bg, #92400e);\n }\n\n .Backoffice_healthCardIcon__8dd475cbef {\n background: color-mix(in srgb, var(--health-accent) 15%, transparent);\n color: var(--health-accent);\n border-radius: var(--bui-radius-3);\n padding: var(--bui-space-1);\n display: inline-flex;\n flex-shrink: 0;\n }\n\n /* \"Most installed\" / \"Not yet used\" rankings (#83 backoffice overview): a\n row per resource, not a grid — the count sits at a fixed right edge\n regardless of how long the title is. */\n .Backoffice_topList__8dd475cbef {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n .Backoffice_topListRow__8dd475cbef {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-3);\n padding: var(--bui-space-2) var(--bui-space-3);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-4);\n background: var(--bui-bg-neutral-1);\n transition: border-color 160ms ease, background 160ms ease;\n }\n\n .Backoffice_topListRow__8dd475cbef:hover {\n border-color: var(--bui-border-1);\n background: var(--bui-bg-surface-2, var(--bui-bg-neutral-1));\n }\n\n /* A numbered chip that turns an unordered-looking list into a visible\n ranking; the leader gets the brand-primary fill so \"#1\" reads at a\n glance. */\n .Backoffice_rankBadge__8dd475cbef {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--bui-space-5);\n height: var(--bui-space-5);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n font-size: var(--bui-font-size-1);\n font-weight: var(--bui-font-weight-bold);\n flex-shrink: 0;\n }\n\n .Backoffice_rankBadgeLead__8dd475cbef {\n background: var(--bui-fg-primary);\n color: var(--bui-bg-surface-1);\n }\n\n .Backoffice_topListCount__8dd475cbef {\n font-weight: var(--bui-font-weight-bold);\n flex-shrink: 0;\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n padding: 2px var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n font-size: var(--bui-font-size-1);\n }\n\n /* A breakdown row that carries a share bar under its label line, so the\n \"By owner\" and \"Most installed\" lists read as small bar charts (the mix,\n the leaders) rather than a column of bare numbers — the same data-ink the\n browse page's StatTiles share bar adds to the type row. */\n .Backoffice_barRow__8dd475cbef {\n display: flex;\n flex-direction: column;\n padding: var(--bui-space-2) var(--bui-space-3);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-4);\n background: var(--bui-bg-neutral-1);\n transition: border-color 160ms ease, background 160ms ease;\n }\n\n .Backoffice_barRow__8dd475cbef:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent, var(--bui-border-1)) 35%,\n var(--bui-border-1)\n );\n background: var(--bui-bg-surface-2, var(--bui-bg-neutral-1));\n }\n\n .Backoffice_barRowMain__8dd475cbef {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-3);\n }\n\n .Backoffice_barRowLeft__8dd475cbef {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n min-width: 0;\n }\n\n /* A coloured initials disc for each owning team/user, so the \"By owner\"\n list scans as distinct entities at a glance; the palette cycles the six\n type hues (ADR-0008) purely as a set of pleasant, distinguishable colours\n — no type meaning is implied here. Initials are uppercased so they never\n collide with a lowercase resource title in the DOM. */\n .Backoffice_ownerAvatar__8dd475cbef {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--bui-space-6);\n height: var(--bui-space-6);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--card-accent-bg, var(--bui-bg-neutral-2));\n color: var(--card-accent, var(--bui-fg-secondary));\n font-size: var(--bui-font-size-1);\n font-weight: var(--bui-font-weight-bold);\n flex-shrink: 0;\n text-transform: uppercase;\n }\n\n /* Staggered entrance for a section's rows on their initial render only\n (mirrors AiResourcesPage's .cardEntrance / playEntrance) — --row-index is\n set per-row. Every backoffice grid/list opts into the same \"the surface\n settles in\" feel the browse page has, rather than snapping into place. */\n .Backoffice_rowEntrance__8dd475cbef {\n animation: Backoffice_backoffice-row-entrance__8dd475cbef 280ms cubic-bezier(0.16, 1, 0.3, 1) both;\n animation-delay: calc(var(--row-index, 0) * 30ms);\n }\n\n @keyframes Backoffice_backoffice-row-entrance__8dd475cbef {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .Backoffice_rowEntrance__8dd475cbef {\n animation: none;\n }\n\n .Backoffice_countCard__8dd475cbef,\n .Backoffice_typeCard__8dd475cbef {\n transition: none;\n }\n\n .Backoffice_countCard__8dd475cbef:hover,\n .Backoffice_typeCard__8dd475cbef:hover {\n transform: none;\n }\n\n .Backoffice_shareBarFill__8dd475cbef {\n transition: none;\n }\n }\n}\n";
4
+ var styles = {"surface":"Backoffice_surface__8dd475cbef","section":"Backoffice_section__8dd475cbef","sectionHeader":"Backoffice_sectionHeader__8dd475cbef","stateBlock":"Backoffice_stateBlock__8dd475cbef","grid":"Backoffice_grid__8dd475cbef","totalSummary":"Backoffice_totalSummary__8dd475cbef","distributionBar":"Backoffice_distributionBar__8dd475cbef","distributionSegment":"Backoffice_distributionSegment__8dd475cbef","countCard":"Backoffice_countCard__8dd475cbef","typeCard":"Backoffice_typeCard__8dd475cbef","typeCardIcon":"Backoffice_typeCardIcon__8dd475cbef","typeValue":"Backoffice_typeValue__8dd475cbef","countCardLabel":"Backoffice_countCardLabel__8dd475cbef","cardLabel":"Backoffice_cardLabel__8dd475cbef","countCardIcon":"Backoffice_countCardIcon__8dd475cbef","countValue":"Backoffice_countValue__8dd475cbef","shareBar":"Backoffice_shareBar__8dd475cbef","shareBarFill":"Backoffice_shareBarFill__8dd475cbef","curationList":"Backoffice_curationList__8dd475cbef","curationRow":"Backoffice_curationRow__8dd475cbef","curationRowMain":"Backoffice_curationRowMain__8dd475cbef","curationRowTitle":"Backoffice_curationRowTitle__8dd475cbef","curationRowIcon":"Backoffice_curationRowIcon__8dd475cbef","curationRowBadge":"Backoffice_curationRowBadge__8dd475cbef","curationRowMeta":"Backoffice_curationRowMeta__8dd475cbef","subsectionHeading":"Backoffice_subsectionHeading__8dd475cbef","healthList":"Backoffice_healthList__8dd475cbef","healthCard":"Backoffice_healthCard__8dd475cbef","healthCardOk":"Backoffice_healthCardOk__8dd475cbef","healthCardWarning":"Backoffice_healthCardWarning__8dd475cbef","healthCardIcon":"Backoffice_healthCardIcon__8dd475cbef","topList":"Backoffice_topList__8dd475cbef","rankBadge":"Backoffice_rankBadge__8dd475cbef","rankBadgeLead":"Backoffice_rankBadgeLead__8dd475cbef","topListCount":"Backoffice_topListCount__8dd475cbef","barRow":"Backoffice_barRow__8dd475cbef","barRowMain":"Backoffice_barRowMain__8dd475cbef","barRowLeft":"Backoffice_barRowLeft__8dd475cbef","ownerAvatar":"Backoffice_ownerAvatar__8dd475cbef","rowEntrance":"Backoffice_rowEntrance__8dd475cbef"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=Backoffice.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Backoffice.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}