@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.
- package/README.md +93 -2
- package/config.schema.json +15 -0
- package/dist/api/DevAiHubResourceClient.esm.js +42 -2
- package/dist/api/DevAiHubResourceClient.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +98 -20
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +2 -2
- package/dist/components/AiResourcesPage/ResourceCard.esm.js +13 -4
- package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +2 -2
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +55 -9
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js +57 -3
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +4 -2
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/SortControl.esm.js +139 -0
- package/dist/components/AiResourcesPage/SortControl.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/SortControl.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/SortControl.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ownerLabel.esm.js +6 -0
- package/dist/components/AiResourcesPage/ownerLabel.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/typeMeta.esm.js +1 -12
- package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -1
- package/dist/components/Backoffice/Backoffice.module.css.esm.js +8 -0
- package/dist/components/Backoffice/Backoffice.module.css.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeCuration.esm.js +191 -0
- package/dist/components/Backoffice/BackofficeCuration.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeExclusion.esm.js +185 -0
- package/dist/components/Backoffice/BackofficeExclusion.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeGate.esm.js +25 -0
- package/dist/components/Backoffice/BackofficeGate.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeHeaderAction.esm.js +43 -0
- package/dist/components/Backoffice/BackofficeHeaderAction.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeOverview.esm.js +517 -0
- package/dist/components/Backoffice/BackofficeOverview.esm.js.map +1 -0
- package/dist/components/Backoffice/BackofficeSection.esm.js +37 -0
- package/dist/components/Backoffice/BackofficeSection.esm.js.map +1 -0
- package/dist/components/Backoffice/TypeDistributionBar.esm.js +44 -0
- package/dist/components/Backoffice/TypeDistributionBar.esm.js.map +1 -0
- package/dist/components/Backoffice/rowEntrance.esm.js +11 -0
- package/dist/components/Backoffice/rowEntrance.esm.js.map +1 -0
- package/dist/components/Backoffice/useCurationAccess.esm.js +12 -0
- package/dist/components/Backoffice/useCurationAccess.esm.js.map +1 -0
- package/dist/components/RecommendedBadge/RecommendedBadgeIcon.esm.js +25 -0
- package/dist/components/RecommendedBadge/RecommendedBadgeIcon.esm.js.map +1 -0
- package/dist/components/RecommendedBadge/recommended-badge.svg +9 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js +41 -6
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
- package/dist/hooks/useDefaultSortMode.esm.js +37 -0
- package/dist/hooks/useDefaultSortMode.esm.js.map +1 -0
- package/dist/hooks/useFrameworkLabel.esm.js +42 -0
- package/dist/hooks/useFrameworkLabel.esm.js.map +1 -0
- package/dist/hooks/useResources.esm.js +25 -14
- package/dist/hooks/useResources.esm.js.map +1 -1
- package/dist/hooks/useTelemetryCounts.esm.js +16 -7
- package/dist/hooks/useTelemetryCounts.esm.js.map +1 -1
- package/dist/hooks/useTelemetrySummary.esm.js +46 -0
- package/dist/hooks/useTelemetrySummary.esm.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.esm.js +1 -1
- package/dist/plugin.esm.js +90 -3
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js +2 -1
- package/dist/routes.esm.js.map +1 -1
- package/dist/theme/typeTokens.module.css.esm.js +5 -0
- package/dist/theme/typeTokens.module.css.esm.js.map +1 -0
- package/package.json +10 -4
|
@@ -3,8 +3,11 @@ import { Text } from '@backstage/ui';
|
|
|
3
3
|
import { RiEyeLine, RiDownloadLine } from '@remixicon/react';
|
|
4
4
|
import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
|
|
5
5
|
import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
|
|
6
|
+
import { RecommendedBadgeIcon } from '../RecommendedBadge/RecommendedBadgeIcon.esm.js';
|
|
6
7
|
import { countLabel } from './countLabel.esm.js';
|
|
7
|
-
import {
|
|
8
|
+
import { formatOwnerLabel } from './ownerLabel.esm.js';
|
|
9
|
+
import { getTypeMeta } from './typeMeta.esm.js';
|
|
10
|
+
import { useFrameworkLabel } from '../../hooks/useFrameworkLabel.esm.js';
|
|
8
11
|
import styles from './ResourceCard.module.css.esm.js';
|
|
9
12
|
|
|
10
13
|
const MAX_VISIBLE_TAGS = 3;
|
|
@@ -13,13 +16,15 @@ function ResourceCard({
|
|
|
13
16
|
resource,
|
|
14
17
|
onView,
|
|
15
18
|
className,
|
|
16
|
-
style
|
|
19
|
+
style,
|
|
20
|
+
seedCounts
|
|
17
21
|
}) {
|
|
18
22
|
const meta = getTypeMeta(resource.type);
|
|
19
|
-
const
|
|
23
|
+
const frameworkLabel = useFrameworkLabel();
|
|
24
|
+
const counts = useTelemetryCounts(resource.entityRef, seedCounts);
|
|
20
25
|
const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);
|
|
21
26
|
const hiddenTagCount = resource.tags.length - visibleTags.length;
|
|
22
|
-
const ownerLabel = resource.owner
|
|
27
|
+
const ownerLabel = formatOwnerLabel(resource.owner);
|
|
23
28
|
return /* @__PURE__ */ jsxs(
|
|
24
29
|
"div",
|
|
25
30
|
{
|
|
@@ -51,6 +56,10 @@ function ResourceCard({
|
|
|
51
56
|
children: resource.title ?? resource.name
|
|
52
57
|
}
|
|
53
58
|
)
|
|
59
|
+
] }),
|
|
60
|
+
resource.recommended && /* @__PURE__ */ jsxs("span", { className: styles.recommendedBadge, children: [
|
|
61
|
+
/* @__PURE__ */ jsx(RecommendedBadgeIcon, { size: 12 }),
|
|
62
|
+
"Recommended"
|
|
54
63
|
] })
|
|
55
64
|
] }),
|
|
56
65
|
/* @__PURE__ */ jsxs("div", { className: styles.body, children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiDownloadLine, RiEyeLine } from '@remixicon/react';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\nimport { countLabel } from './countLabel';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceCard.module.css';\n\ninterface ResourceCardProps {\n resource: ResourceSummary;\n onView: (entityRef: string) => void;\n /** Extra class (e.g. the initial-load entrance animation) merged onto the\n * card's own root — kept off a wrapper div so this stays the grid's direct\n * child and CSS Grid's row-height stretch (the footer's margin-top: auto\n * relies on it for equal card heights) keeps working. */\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * The per-type card: type identity (colour/icon/label from the registry),\n * title, framework badges, tags, and a footer of version · owner plus the\n * view and install counts.\n *\n * Deliberately no description — clamped to two lines at card width it was\n * unreadable, and the drawer shows it in full the moment a card is opened.\n * Deliberately no \"View source\" link either: it competed with the card's own\n * click target and lives in the drawer, which is where someone deciding\n * whether to install is already looking.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst POPULAR_THRESHOLD = 5;\n\nexport function ResourceCard({\n resource,\n onView,\n className,\n style,\n}: ResourceCardProps) {\n const meta = getTypeMeta(resource.type);\n const counts = useTelemetryCounts(resource.entityRef);\n\n const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);\n const hiddenTagCount = resource.tags.length - visibleTags.length;\n const ownerLabel = resource.owner?.replace(/^(group|user):(default\\/)?/, '');\n\n return (\n <div\n className={className ? `${styles.card} ${className}` : styles.card}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n ...style,\n } as React.CSSProperties\n }\n onClick={() => onView(resource.entityRef)}\n onKeyDown={e => {\n if (e.key === 'Enter') onView(resource.entityRef);\n }}\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${resource.title ?? resource.name}`}\n >\n <div className={styles.header}>\n <div className={styles.iconBox}>\n <meta.Icon size={18} />\n </div>\n <div className={styles.headerText}>\n {/* Type reads as an eyebrow above the title. Below it, the reserved\n second title line would leave a gap between a one-line title and\n its own label; above it, that space falls before the description\n where nothing looks detached. */}\n <Text variant=\"body-x-small\" className={styles.typeLabel}>\n {meta.label}\n </Text>\n <Text\n variant=\"title-x-small\"\n weight=\"bold\"\n as=\"h3\"\n className={styles.title}\n >\n {resource.title ?? resource.name}\n </Text>\n </div>\n </div>\n\n <div className={styles.body}>\n {resource.frameworks.length > 0 && (\n <div className={styles.badges}>\n {/* Icon only — the tool's mark is recognisable enough on a card,\n and dropping the label keeps the row narrow. The name stays\n reachable: ToolIcon carries it as an aria-label, and `title`\n surfaces it on hover. Every framework is shown: icons are\n narrow enough that the full set fits, so there is no overflow\n pill to hide the tail behind. */}\n {resource.frameworks.map(f => (\n <span\n key={f}\n className={styles.frameworkBadge}\n title={frameworkLabel(f)}\n >\n <ToolIcon tool={f as FrameworkToken} size={16} />\n </span>\n ))}\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.tags}>\n {visibleTags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n {hiddenTagCount > 0 && (\n <span\n className={styles.tag}\n title={resource.tags.slice(MAX_VISIBLE_TAGS).join(', ')}\n >\n +{hiddenTagCount}\n </span>\n )}\n </div>\n )}\n\n {(resource.version ||\n ownerLabel ||\n !!counts?.install ||\n !!counts?.view) && (\n <div className={styles.footer}>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {[\n resource.version && `v${resource.version.replace(/^v/, '')}`,\n ownerLabel,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Text>\n {/* Views and installs are the social proof someone browsing\n actually scans for, so they are chips at full foreground rather\n than grey footnotes beside the version. */}\n <span className={styles.countsRow}>\n {!!counts?.view && (\n <span\n className={styles.count}\n title={countLabel(counts.view, 'view')}\n >\n <RiEyeLine size={14} />\n {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={\n counts.install >= POPULAR_THRESHOLD\n ? styles.countPopular\n : styles.count\n }\n title={countLabel(counts.install, 'install')}\n >\n {counts.install >= POPULAR_THRESHOLD ? (\n '\\u{1F525}'\n ) : (\n <RiDownloadLine size={14} />\n )}\n {counts.install}\n </span>\n )}\n </span>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiDownloadLine, RiEyeLine } from '@remixicon/react';\nimport type {\n ResourceSummary,\n TelemetryCounts,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { RecommendedBadgeIcon } from '../RecommendedBadge';\nimport { countLabel } from './countLabel';\nimport { formatOwnerLabel } from './ownerLabel';\nimport { getTypeMeta } from './typeMeta';\nimport { useFrameworkLabel } from '../../hooks/useFrameworkLabel';\nimport styles from './ResourceCard.module.css';\n\ninterface ResourceCardProps {\n resource: ResourceSummary;\n onView: (entityRef: string) => void;\n /** Extra class (e.g. the initial-load entrance animation) merged onto the\n * card's own root — kept off a wrapper div so this stays the grid's direct\n * child and CSS Grid's row-height stretch (the footer's margin-top: auto\n * relies on it for equal card heights) keeps working. */\n className?: string;\n style?: React.CSSProperties;\n /**\n * This resource's slice of the page-level telemetry summary (issue #102 —\n * fetched once to drive sorting), seeding `useTelemetryCounts` so the card\n * does not also fire its own initial request. Omit to fall back to the\n * card fetching for itself.\n */\n seedCounts?: TelemetryCounts;\n}\n\n/**\n * The per-type card: type identity (colour/icon/label from the registry),\n * title, framework badges, tags, and a footer of version · owner plus the\n * view and install counts.\n *\n * Deliberately no description — clamped to two lines at card width it was\n * unreadable, and the drawer shows it in full the moment a card is opened.\n * Deliberately no \"View source\" link either: it competed with the card's own\n * click target and lives in the drawer, which is where someone deciding\n * whether to install is already looking.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst POPULAR_THRESHOLD = 5;\n\nexport function ResourceCard({\n resource,\n onView,\n className,\n style,\n seedCounts,\n}: ResourceCardProps) {\n const meta = getTypeMeta(resource.type);\n const frameworkLabel = useFrameworkLabel();\n const counts = useTelemetryCounts(resource.entityRef, seedCounts);\n\n const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);\n const hiddenTagCount = resource.tags.length - visibleTags.length;\n const ownerLabel = formatOwnerLabel(resource.owner);\n\n return (\n <div\n className={className ? `${styles.card} ${className}` : styles.card}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n ...style,\n } as React.CSSProperties\n }\n onClick={() => onView(resource.entityRef)}\n onKeyDown={e => {\n if (e.key === 'Enter') onView(resource.entityRef);\n }}\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${resource.title ?? resource.name}`}\n >\n <div className={styles.header}>\n <div className={styles.iconBox}>\n <meta.Icon size={18} />\n </div>\n <div className={styles.headerText}>\n {/* Type reads as an eyebrow above the title. Below it, the reserved\n second title line would leave a gap between a one-line title and\n its own label; above it, that space falls before the description\n where nothing looks detached. */}\n <Text variant=\"body-x-small\" className={styles.typeLabel}>\n {meta.label}\n </Text>\n <Text\n variant=\"title-x-small\"\n weight=\"bold\"\n as=\"h3\"\n className={styles.title}\n >\n {resource.title ?? resource.name}\n </Text>\n </div>\n {/* A curator's opinion (#68, ADR-0017), not something the resource's\n author declared — kept visually apart from the type identity so\n it never reads as catalog metadata. */}\n {resource.recommended && (\n <span className={styles.recommendedBadge}>\n <RecommendedBadgeIcon size={12} />\n Recommended\n </span>\n )}\n </div>\n\n <div className={styles.body}>\n {resource.frameworks.length > 0 && (\n <div className={styles.badges}>\n {/* Icon only — the tool's mark is recognisable enough on a card,\n and dropping the label keeps the row narrow. The name stays\n reachable: ToolIcon carries it as an aria-label, and `title`\n surfaces it on hover. Every framework is shown: icons are\n narrow enough that the full set fits, so there is no overflow\n pill to hide the tail behind. */}\n {resource.frameworks.map(f => (\n <span\n key={f}\n className={styles.frameworkBadge}\n title={frameworkLabel(f)}\n >\n <ToolIcon tool={f} size={16} />\n </span>\n ))}\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.tags}>\n {visibleTags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n {hiddenTagCount > 0 && (\n <span\n className={styles.tag}\n title={resource.tags.slice(MAX_VISIBLE_TAGS).join(', ')}\n >\n +{hiddenTagCount}\n </span>\n )}\n </div>\n )}\n\n {(resource.version ||\n ownerLabel ||\n !!counts?.install ||\n !!counts?.view) && (\n <div className={styles.footer}>\n <Text variant=\"body-x-small\" color=\"secondary\">\n {[\n resource.version && `v${resource.version.replace(/^v/, '')}`,\n ownerLabel,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Text>\n {/* Views and installs are the social proof someone browsing\n actually scans for, so they are chips at full foreground rather\n than grey footnotes beside the version. */}\n <span className={styles.countsRow}>\n {!!counts?.view && (\n <span\n className={styles.count}\n title={countLabel(counts.view, 'view')}\n >\n <RiEyeLine size={14} />\n {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={\n counts.install >= POPULAR_THRESHOLD\n ? styles.countPopular\n : styles.count\n }\n title={countLabel(counts.install, 'install')}\n >\n {counts.install >= POPULAR_THRESHOLD ? (\n '\\u{1F525}'\n ) : (\n <RiDownloadLine size={14} />\n )}\n {counts.install}\n </span>\n )}\n </span>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@layer components {\n /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .
|
|
4
|
-
var styles = {"card":"
|
|
3
|
+
var css_248z = "@layer components {\n /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .ResourceCard_card__26c3976233 {\n background: var(--bui-bg-neutral-1);\n border: 1px solid var(--bui-border-1);\n /* The type's colour, kept as the card's identity at a glance in the grid.\n 2px rather than 3: on a 16px radius a thick stripe fights the curve. */\n border-left: 2px solid var(--card-accent);\n border-radius: var(--bui-radius-5);\n overflow: hidden;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n padding: var(--bui-space-4);\n /* Room for the tallest realistic card (2-line title, badges, tags,\n footer) so the grid reads as a grid. The footer's margin-top: auto\n absorbs the slack on lighter cards. */\n min-height: 176px;\n transition: box-shadow 160ms ease,\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1), border-color 160ms ease;\n }\n\n .ResourceCard_card__26c3976233:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent) 40%,\n var(--bui-border-1)\n );\n border-left-color: var(--card-accent);\n /* Two-part shadow: a soft ambient one and a tighter contact one. A single\n large blur reads as a glow rather than as elevation. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),\n 0 12px 28px -12px color-mix(in srgb, var(--card-accent) 40%, transparent);\n transform: translateY(-2px);\n }\n\n .ResourceCard_card__26c3976233:focus-visible {\n outline: 2px solid var(--bui-ring, var(--card-accent));\n outline-offset: 2px;\n }\n\n /* The header is a fixed-height zone with its contents centred, rather than\n a top-aligned block with a reserved second title line. Same outcome for\n alignment across the grid — everything below starts at one height — but a\n one-line title now sits centred in that zone instead of leaving an\n orphaned gap beneath it, which is what made short-titled cards read as\n detached from their own content. */\n .ResourceCard_header__26c3976233 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n flex-shrink: 0;\n min-height: 3.5rem;\n }\n\n .ResourceCard_iconBox__26c3976233 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-4);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceCard_headerText__26c3976233 {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n }\n\n .ResourceCard_title__26c3976233 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n /* Tighter leading than body copy: a two-line title should read as one\n object rather than as two stacked lines. */\n line-height: 1.25;\n }\n\n .ResourceCard_typeLabel__26c3976233 {\n display: block;\n color: var(--bui-fg-secondary);\n font-weight: 600;\n font-size: 0.6875rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n margin-bottom: 2px;\n }\n\n /* A curator's opinion (#68, ADR-0017), not the type's own colour — amber\n rather than the card's accent so it never reads as part of the type\n identity, and a pill rather than a stripe so it stays legible at the\n small size the header row allows. */\n .ResourceCard_recommendedBadge__26c3976233 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n flex-shrink: 0;\n align-self: flex-start;\n background: var(--bui-bg-warning, #fff4e5);\n color: var(--bui-fg-warning, #92400e);\n border-radius: var(--bui-radius-full, 999px);\n padding: 2px var(--bui-space-2);\n font-size: 0.6875rem;\n font-weight: 600;\n line-height: 1.4;\n white-space: nowrap;\n }\n\n .ResourceCard_body__26c3976233 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n flex: 1;\n }\n\n .ResourceCard_badges__26c3976233 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Holds a single tool icon, so the padding is square and the badge reads as\n a circle rather than a pill. */\n .ResourceCard_frameworkBadge__26c3976233 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-full, 999px);\n padding: var(--bui-space-1);\n font-size: 0.8125rem;\n line-height: 1;\n color: var(--bui-fg-primary);\n }\n\n .ResourceCard_tags__26c3976233 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Quiet chips rather than bare \"#text\": a chip reads as a discrete value,\n which is what a tag is, and the surface keeps them from competing with the\n description. Not interactive, so they stay low-contrast. */\n .ResourceCard_tag__26c3976233 {\n font-size: 0.6875rem;\n line-height: 1.6;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n\n /* No rule above the footer — the gap does the separating, and the meta is\n already de-emphasised enough to read as a different tier. */\n .ResourceCard_footer__26c3976233 {\n margin-top: auto;\n padding-top: var(--bui-space-1);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-2);\n font-size: 0.6875rem;\n }\n\n .ResourceCard_countsRow__26c3976233 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n flex-shrink: 0;\n }\n\n /* Chips, not footnotes: views and installs are the signal a browser scans\n for, so they carry a surface and the full foreground colour while the\n version/owner meta stays secondary beside them. */\n .ResourceCard_count__26c3976233 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n padding: 2px var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--bui-fg-primary);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n /* The glyph takes the card's accent while the number stays foreground: a\n fully monochrome chip reads as detached on a card that is otherwise\n colour-coded, and colouring the number too would cost its legibility. */\n .ResourceCard_count__26c3976233 svg {\n color: var(--card-accent);\n flex-shrink: 0;\n }\n\n /* Past the popularity threshold the install chip takes the type's own\n colour — the one place on the card where the accent marks a value rather\n than an identity. */\n .ResourceCard_countPopular__26c3976233 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n }\n}\n";
|
|
4
|
+
var styles = {"card":"ResourceCard_card__26c3976233","header":"ResourceCard_header__26c3976233","iconBox":"ResourceCard_iconBox__26c3976233","headerText":"ResourceCard_headerText__26c3976233","title":"ResourceCard_title__26c3976233","typeLabel":"ResourceCard_typeLabel__26c3976233","recommendedBadge":"ResourceCard_recommendedBadge__26c3976233","body":"ResourceCard_body__26c3976233","badges":"ResourceCard_badges__26c3976233","frameworkBadge":"ResourceCard_frameworkBadge__26c3976233","tags":"ResourceCard_tags__26c3976233","tag":"ResourceCard_tag__26c3976233","footer":"ResourceCard_footer__26c3976233","countsRow":"ResourceCard_countsRow__26c3976233","count":"ResourceCard_count__26c3976233","countPopular":"ResourceCard_countPopular__26c3976233 ResourceCard_count__26c3976233"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { styles as default };
|
|
@@ -2,7 +2,7 @@ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import ReactMarkdown from 'react-markdown';
|
|
4
4
|
import { Skeleton, Flex, Box, Text, ButtonIcon, Button, Link } from '@backstage/ui';
|
|
5
|
-
import { RiPriceTag3Line, RiEyeLine, RiInstallLine, RiCloseLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
|
|
5
|
+
import { RiPriceTag3Line, RiEyeLine, RiInstallLine, RiCloseLine, RiFileCopyLine, RiDownloadLine, RiExternalLinkLine, RiErrorWarningLine, RiCheckLine, RiShareLine } from '@remixicon/react';
|
|
6
6
|
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
|
7
7
|
import { EntityRefLink } from '@backstage/plugin-catalog-react';
|
|
8
8
|
import { isParseableEntityRef, hasCopyableBody, hasDownloadableArtifact, showsContentSection } from '@nospt/plugin-dev-ai-hub-common';
|
|
@@ -14,10 +14,38 @@ import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
|
|
|
14
14
|
import { ResourceInstallDialog } from './ResourceInstallDialog.esm.js';
|
|
15
15
|
import { countLabel } from './countLabel.esm.js';
|
|
16
16
|
import { stripFrontmatter } from './stripFrontmatter.esm.js';
|
|
17
|
-
import { getTypeMeta
|
|
17
|
+
import { getTypeMeta } from './typeMeta.esm.js';
|
|
18
|
+
import { useFrameworkLabel } from '../../hooks/useFrameworkLabel.esm.js';
|
|
18
19
|
import styles from './ResourceDetailPanel.module.css.esm.js';
|
|
19
20
|
|
|
20
21
|
const ENTITY_REF_ROWS = /* @__PURE__ */ new Set(["Owner", "Entity ref"]);
|
|
22
|
+
const COPY_LABEL = {
|
|
23
|
+
idle: "Copy",
|
|
24
|
+
done: "Copied!",
|
|
25
|
+
failed: "Copy failed"
|
|
26
|
+
};
|
|
27
|
+
const SHARE_LABEL = {
|
|
28
|
+
idle: "Share link",
|
|
29
|
+
done: "Link copied",
|
|
30
|
+
failed: "Couldn\u2019t share link"
|
|
31
|
+
};
|
|
32
|
+
const SHARE_ICON = {
|
|
33
|
+
idle: /* @__PURE__ */ jsx(RiShareLine, {}),
|
|
34
|
+
done: /* @__PURE__ */ jsx(RiCheckLine, {}),
|
|
35
|
+
failed: /* @__PURE__ */ jsx(RiErrorWarningLine, {})
|
|
36
|
+
};
|
|
37
|
+
async function copyToClipboard(text) {
|
|
38
|
+
try {
|
|
39
|
+
await navigator.clipboard.writeText(text);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function flash(set, ok) {
|
|
46
|
+
set(ok ? "done" : "failed");
|
|
47
|
+
setTimeout(() => set("idle"), 2e3);
|
|
48
|
+
}
|
|
21
49
|
const DEFAULT_PANEL_WIDTH = 615;
|
|
22
50
|
const MIN_PANEL_WIDTH = 320;
|
|
23
51
|
function ResourceDetailPanel({
|
|
@@ -26,6 +54,7 @@ function ResourceDetailPanel({
|
|
|
26
54
|
items = [],
|
|
27
55
|
onOpen
|
|
28
56
|
}) {
|
|
57
|
+
const frameworkLabel = useFrameworkLabel();
|
|
29
58
|
const isOpen = !!resourceProp;
|
|
30
59
|
const [displayResource, setDisplayResource] = useState(resourceProp);
|
|
31
60
|
useEffect(() => {
|
|
@@ -46,16 +75,24 @@ function ResourceDetailPanel({
|
|
|
46
75
|
const bodyState = useResourceBody(displayResource?.entityRef, actionable);
|
|
47
76
|
const counts = useTelemetryCounts(displayResource?.entityRef);
|
|
48
77
|
const [installOpen, setInstallOpen] = useState(false);
|
|
49
|
-
const [
|
|
78
|
+
const [copyState, setCopyState] = useState("idle");
|
|
79
|
+
const [shareState, setShareState] = useState("idle");
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setShareState("idle");
|
|
82
|
+
}, [displayResource?.entityRef]);
|
|
50
83
|
if (!displayResource) return null;
|
|
51
84
|
const resource = displayResource;
|
|
52
85
|
const meta = getTypeMeta(resource.type);
|
|
53
86
|
const handleCopy = async () => {
|
|
54
87
|
if (!bodyState.body) return;
|
|
55
|
-
await
|
|
56
|
-
track(resource.entityRef, "copy");
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
const ok = await copyToClipboard(bodyState.body.content);
|
|
89
|
+
if (ok) track(resource.entityRef, "copy");
|
|
90
|
+
flash(setCopyState, ok);
|
|
91
|
+
};
|
|
92
|
+
const handleShare = async () => {
|
|
93
|
+
const url = new URL(window.location.href);
|
|
94
|
+
url.searchParams.set("resource", resource.entityRef);
|
|
95
|
+
flash(setShareState, await copyToClipboard(url.toString()));
|
|
59
96
|
};
|
|
60
97
|
const handleDownload = () => {
|
|
61
98
|
track(resource.entityRef, "download");
|
|
@@ -149,12 +186,21 @@ function ResourceDetailPanel({
|
|
|
149
186
|
] })
|
|
150
187
|
] })
|
|
151
188
|
] }),
|
|
189
|
+
/* @__PURE__ */ jsx(
|
|
190
|
+
ButtonIcon,
|
|
191
|
+
{
|
|
192
|
+
icon: SHARE_ICON[shareState],
|
|
193
|
+
variant: "tertiary",
|
|
194
|
+
onPress: handleShare,
|
|
195
|
+
"aria-label": SHARE_LABEL[shareState]
|
|
196
|
+
}
|
|
197
|
+
),
|
|
152
198
|
/* @__PURE__ */ jsx(
|
|
153
199
|
ButtonIcon,
|
|
154
200
|
{
|
|
155
201
|
icon: /* @__PURE__ */ jsx(RiCloseLine, {}),
|
|
156
202
|
variant: "tertiary",
|
|
157
|
-
|
|
203
|
+
onPress: onClose,
|
|
158
204
|
"aria-label": "Close"
|
|
159
205
|
}
|
|
160
206
|
)
|
|
@@ -187,7 +233,7 @@ function ResourceDetailPanel({
|
|
|
187
233
|
iconStart: /* @__PURE__ */ jsx(RiFileCopyLine, {}),
|
|
188
234
|
isDisabled: !bodyState.body,
|
|
189
235
|
onPress: handleCopy,
|
|
190
|
-
children:
|
|
236
|
+
children: COPY_LABEL[copyState]
|
|
191
237
|
}
|
|
192
238
|
),
|
|
193
239
|
hasDownloadableArtifact(resource.type) && /* @__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 RiPriceTag3Line,\n} from '@remixicon/react';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { EntityRefLink } from '@backstage/plugin-catalog-react';\nimport {\n hasCopyableBody,\n hasDownloadableArtifact,\n isParseableEntityRef,\n showsContentSection,\n type FrameworkToken,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport {\n useTelemetryCounts,\n useTrackTelemetry,\n} from '../../hooks/useTelemetryCounts';\nimport { ResourceRelationships } from './ResourceRelationships';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { countLabel } from './countLabel';\nimport { stripFrontmatter } from './stripFrontmatter';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n /** Every caller-visible resource, so relationship refs can be resolved. */\n items?: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen?: (entityRef: string) => void;\n}\n\n/**\n * Metadata rows that name a catalog entity and should link there. Rendering\n * still falls back to plain text per-row when the value isn't a parseable\n * ref (`isParseableEntityRef`) — Owner isn't guaranteed parseable, since\n * `toResourceSummary` deliberately passes a malformed one through rather\n * than dropping the resource, and `EntityRefLink` throws on a ref it can't\n * parse. Entity ref is always valid (built with `stringifyEntityRef`), so it\n * always renders as a link. Owner is also surfaced near the top of the\n * drawer; it stays in this list too.\n */\nconst ENTITY_REF_ROWS = new Set(['Owner', 'Entity ref']);\n\n/** Drawer width when `devAiHub.ui.detailPanelWidth` is unset (px). */\nconst DEFAULT_PANEL_WIDTH = 615;\n/** Floor for a configured width, so a fat-fingered value can't collapse it. */\nconst MIN_PANEL_WIDTH = 320;\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n items = [],\n onOpen,\n}: ResourceDetailPanelProps) {\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const config = useApi(configApiRef);\n const panelWidth = Math.max(\n MIN_PANEL_WIDTH,\n config.getOptionalNumber('devAiHub.ui.detailPanelWidth') ??\n DEFAULT_PANEL_WIDTH,\n );\n\n // A `view` is a deliberate look at one resource, so it is recorded here —\n // when the drawer opens — and not on card render, where merely loading the\n // page counted a view for every card in the grid. Keyed on the *live* prop,\n // not `displayResource`: that one stays set through the exit animation, and\n // closing must not count a second view.\n const openedRef = resourceProp?.entityRef;\n useEffect(() => {\n if (openedRef) track(openedRef, 'view');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [openedRef]);\n\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n await navigator.clipboard.writeText(bodyState.body.content);\n track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = () => {\n track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n // The `install` event is recorded by the dialog, which alone knows whether\n // it produced a launch button to wait for (ADR-0011).\n const handleInstall = () => setInstallOpen(true);\n\n // Only the two entity references live here. Type is already the coloured\n // eyebrow under the title, and version/views/installs are header chips.\n const metadataRows: [string, string | undefined][] = [\n ['Owner', resource.owner],\n ['Entity ref', resource.entityRef],\n ];\n\n // Body-fetch problems (missing location, 404, upstream failure + retry).\n // Pulled out because Copy/Install depend on the body even when the \"Content\"\n // section itself is suppressed (every JSON-bodied type) — the user still\n // needs to see the failure and be able to retry.\n const bodyProblem = (\n <>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not have\n access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button size=\"small\" variant=\"tertiary\" onPress={bodyState.retry}>\n Retry\n </Button>\n </Flex>\n )}\n </>\n );\n\n const hasBodyProblem =\n !actionable ||\n bodyState.error === 'not-found' ||\n bodyState.error === 'upstream';\n\n const contentInner = (\n <>\n {bodyProblem}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {bodyState.body && (\n <div className={styles.markdown}>\n <ReactMarkdown>\n {stripFrontmatter(bodyState.body.content)}\n </ReactMarkdown>\n </div>\n )}\n </>\n );\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={\n {\n '--drawer-accent': meta.color,\n '--drawer-width': `${panelWidth}px`,\n } as React.CSSProperties\n }\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n className={styles.typeLabel}\n style={{ color: meta.color }}\n >\n {meta.label}\n </Text>\n {/* Version and the telemetry counts ride in the header rather than\n the body: the header sits outside the scrolling `.body`, so they\n stay in view, and they read as facts about the resource rather\n than another body section. Same pill as the framework badges. */}\n {(resource.version || !!counts?.view || !!counts?.install) && (\n <div className={styles.headerBadges}>\n {resource.version && (\n <span className={styles.frameworkBadge}>\n <RiPriceTag3Line size={14} />v\n {resource.version.replace(/^v/, '')}\n </span>\n )}\n {!!counts?.view && (\n <span className={styles.frameworkBadge}>\n <RiEyeLine size={14} /> {countLabel(counts.view, 'view')}\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.frameworkBadge}>\n <RiInstallLine size={14} />{' '}\n {countLabel(counts.install, 'install')}\n </span>\n )}\n </div>\n )}\n </Box>\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onClick={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Description:\n </Text>\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata:\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>\n {ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v!) ? (\n <EntityRefLink entityRef={v!}>{v}</EntityRefLink>\n ) : (\n v\n )}\n </dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with:\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n {onOpen && (\n <ResourceRelationships\n resource={resource}\n items={items}\n onOpen={onOpen}\n />\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags:\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.frameworkBadge}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {showsContentSection(resource.type) && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content:\n </Text>\n {contentInner}\n </div>\n )}\n\n {/* Content section suppressed for JSON-bodied resources, but the body\n failure is still surfaced with Retry so Copy/Install aren't\n silently broken. */}\n {!showsContentSection(resource.type) && hasBodyProblem && (\n <div className={styles.section}>{bodyProblem}</div>\n )}\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n bodyLoading={bodyState.loading}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"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 RiCheckLine,\n RiCloseLine,\n RiDownloadLine,\n RiErrorWarningLine,\n RiExternalLinkLine,\n RiEyeLine,\n RiFileCopyLine,\n RiInstallLine,\n RiPriceTag3Line,\n RiShareLine,\n} from '@remixicon/react';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { EntityRefLink } from '@backstage/plugin-catalog-react';\nimport {\n hasCopyableBody,\n hasDownloadableArtifact,\n isParseableEntityRef,\n showsContentSection,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport {\n useTelemetryCounts,\n useTrackTelemetry,\n} from '../../hooks/useTelemetryCounts';\nimport { ResourceRelationships } from './ResourceRelationships';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { countLabel } from './countLabel';\nimport { stripFrontmatter } from './stripFrontmatter';\nimport { getTypeMeta } from './typeMeta';\nimport { useFrameworkLabel } from '../../hooks/useFrameworkLabel';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n /** Every caller-visible resource, so relationship refs can be resolved. */\n items?: ResourceSummary[];\n /** Opens another resource's detail (the existing `?resource=` swap). */\n onOpen?: (entityRef: string) => void;\n}\n\n/**\n * Metadata rows that name a catalog entity and should link there. Rendering\n * still falls back to plain text per-row when the value isn't a parseable\n * ref (`isParseableEntityRef`) — Owner isn't guaranteed parseable, since\n * `toResourceSummary` deliberately passes a malformed one through rather\n * than dropping the resource, and `EntityRefLink` throws on a ref it can't\n * parse. Entity ref is always valid (built with `stringifyEntityRef`), so it\n * always renders as a link. Owner is also surfaced near the top of the\n * drawer; it stays in this list too.\n */\nconst ENTITY_REF_ROWS = new Set(['Owner', 'Entity ref']);\n\ntype CopyState = 'idle' | 'done' | 'failed';\n\nconst COPY_LABEL: Record<CopyState, string> = {\n idle: 'Copy',\n done: 'Copied!',\n failed: 'Copy failed',\n};\n// None of these may contain \"copy\": e2e specs find the drawer's Copy button\n// by accessible-name substring.\nconst SHARE_LABEL: Record<CopyState, string> = {\n idle: 'Share link',\n done: 'Link copied',\n failed: 'Couldn’t share link',\n};\nconst SHARE_ICON: Record<CopyState, React.ReactElement> = {\n idle: <RiShareLine />,\n done: <RiCheckLine />,\n failed: <RiErrorWarningLine />,\n};\n\n/**\n * `navigator.clipboard` is missing outside a secure context (plain http on\n * anything but localhost) and `writeText` rejects when the browser denies\n * permission; either way the caller gets `false` instead of an unhandled\n * rejection behind a button that silently did nothing.\n */\nasync function copyToClipboard(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Shows a copy outcome for two seconds, then returns the control to idle. */\nfunction flash(set: (state: CopyState) => void, ok: boolean) {\n set(ok ? 'done' : 'failed');\n setTimeout(() => set('idle'), 2000);\n}\n\n/** Drawer width when `devAiHub.ui.detailPanelWidth` is unset (px). */\nconst DEFAULT_PANEL_WIDTH = 615;\n/** Floor for a configured width, so a fat-fingered value can't collapse it. */\nconst MIN_PANEL_WIDTH = 320;\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n items = [],\n onOpen,\n}: ResourceDetailPanelProps) {\n const frameworkLabel = useFrameworkLabel();\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n const track = useTrackTelemetry();\n const config = useApi(configApiRef);\n const panelWidth = Math.max(\n MIN_PANEL_WIDTH,\n config.getOptionalNumber('devAiHub.ui.detailPanelWidth') ??\n DEFAULT_PANEL_WIDTH,\n );\n\n // A `view` is a deliberate look at one resource, so it is recorded here —\n // when the drawer opens — and not on card render, where merely loading the\n // page counted a view for every card in the grid. Keyed on the *live* prop,\n // not `displayResource`: that one stays set through the exit animation, and\n // closing must not count a second view.\n const openedRef = resourceProp?.entityRef;\n useEffect(() => {\n if (openedRef) track(openedRef, 'view');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [openedRef]);\n\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copyState, setCopyState] = useState<CopyState>('idle');\n const [shareState, setShareState] = useState<CopyState>('idle');\n // The panel stays mounted while a different resource is selected, so this\n // state would otherwise survive across resources — showing the previous\n // resource's \"copied\" feedback on the newly opened one.\n useEffect(() => {\n setShareState('idle');\n }, [displayResource?.entityRef]);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n const ok = await copyToClipboard(bodyState.body.content);\n if (ok) track(resource.entityRef, 'copy');\n flash(setCopyState, ok);\n };\n\n // The drawer is URL-driven (`?resource=`), so the current location with that\n // param set is already a deep link straight back to this resource.\n const handleShare = async () => {\n const url = new URL(window.location.href);\n url.searchParams.set('resource', resource.entityRef);\n flash(setShareState, await copyToClipboard(url.toString()));\n };\n\n const handleDownload = () => {\n track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n // The `install` event is recorded by the dialog, which alone knows whether\n // it produced a launch button to wait for (ADR-0011).\n const handleInstall = () => setInstallOpen(true);\n\n // Only the two entity references live here. Type is already the coloured\n // eyebrow under the title, and version/views/installs are header chips.\n const metadataRows: [string, string | undefined][] = [\n ['Owner', resource.owner],\n ['Entity ref', resource.entityRef],\n ];\n\n // Body-fetch problems (missing location, 404, upstream failure + retry).\n // Pulled out because Copy/Install depend on the body even when the \"Content\"\n // section itself is suppressed (every JSON-bodied type) — the user still\n // needs to see the failure and be able to retry.\n const bodyProblem = (\n <>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not have\n access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button size=\"small\" variant=\"tertiary\" onPress={bodyState.retry}>\n Retry\n </Button>\n </Flex>\n )}\n </>\n );\n\n const hasBodyProblem =\n !actionable ||\n bodyState.error === 'not-found' ||\n bodyState.error === 'upstream';\n\n const contentInner = (\n <>\n {bodyProblem}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {bodyState.body && (\n <div className={styles.markdown}>\n <ReactMarkdown>\n {stripFrontmatter(bodyState.body.content)}\n </ReactMarkdown>\n </div>\n )}\n </>\n );\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={\n {\n '--drawer-accent': meta.color,\n '--drawer-width': `${panelWidth}px`,\n } as React.CSSProperties\n }\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n className={styles.typeLabel}\n style={{ color: meta.color }}\n >\n {meta.label}\n </Text>\n {/* Version and the telemetry counts ride in the header rather than\n the body: the header sits outside the scrolling `.body`, so they\n stay in view, and they read as facts about the resource rather\n than another body section. Same pill as the framework badges. */}\n {(resource.version || !!counts?.view || !!counts?.install) && (\n <div className={styles.headerBadges}>\n {resource.version && (\n <span className={styles.frameworkBadge}>\n <RiPriceTag3Line size={14} />v\n {resource.version.replace(/^v/, '')}\n </span>\n )}\n {!!counts?.view && (\n <span className={styles.frameworkBadge}>\n <RiEyeLine size={14} /> {countLabel(counts.view, 'view')}\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.frameworkBadge}>\n <RiInstallLine size={14} />{' '}\n {countLabel(counts.install, 'install')}\n </span>\n )}\n </div>\n )}\n </Box>\n <ButtonIcon\n icon={SHARE_ICON[shareState]}\n variant=\"tertiary\"\n onPress={handleShare}\n aria-label={SHARE_LABEL[shareState]}\n />\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onPress={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Description:\n </Text>\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata:\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>\n {ENTITY_REF_ROWS.has(k) && isParseableEntityRef(v!) ? (\n <EntityRefLink entityRef={v!}>{v}</EntityRefLink>\n ) : (\n v\n )}\n </dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with:\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {COPY_LABEL[copyState]}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n {onOpen && (\n <ResourceRelationships\n resource={resource}\n items={items}\n onOpen={onOpen}\n />\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags:\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.frameworkBadge}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {showsContentSection(resource.type) && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content:\n </Text>\n {contentInner}\n </div>\n )}\n\n {/* Content section suppressed for JSON-bodied resources, but the body\n failure is still surfaced with Retry so Copy/Install aren't\n silently broken. */}\n {!showsContentSection(resource.type) && hasBodyProblem && (\n <div className={styles.section}>{bodyProblem}</div>\n )}\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n bodyLoading={bodyState.loading}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAkEA,MAAM,kCAAkB,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,YAAY,CAAC,CAAA;AAIvD,MAAM,UAAA,GAAwC;AAAA,EAC5C,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,SAAA;AAAA,EACN,MAAA,EAAQ;AACV,CAAA;AAGA,MAAM,WAAA,GAAyC;AAAA,EAC7C,IAAA,EAAM,YAAA;AAAA,EACN,IAAA,EAAM,aAAA;AAAA,EACN,MAAA,EAAQ;AACV,CAAA;AACA,MAAM,UAAA,GAAoD;AAAA,EACxD,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,EACnB,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,EACnB,MAAA,sBAAS,kBAAA,EAAA,EAAmB;AAC9B,CAAA;AAQA,eAAe,gBAAgB,IAAA,EAAgC;AAC7D,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,CAAU,SAAA,CAAU,SAAA,CAAU,IAAI,CAAA;AACxC,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGA,SAAS,KAAA,CAAM,KAAiC,EAAA,EAAa;AAC3D,EAAA,GAAA,CAAI,EAAA,GAAK,SAAS,QAAQ,CAAA;AAC1B,EAAA,UAAA,CAAW,MAAM,GAAA,CAAI,MAAM,CAAA,EAAG,GAAI,CAAA;AACpC;AAGA,MAAM,mBAAA,GAAsB,GAAA;AAE5B,MAAM,eAAA,GAAkB,GAAA;AAQjB,SAAS,mBAAA,CAAoB;AAAA,EAClC,QAAA,EAAU,YAAA;AAAA,EACV,OAAA;AAAA,EACA,QAAQ,EAAC;AAAA,EACT;AACF,CAAA,EAA6B;AAC3B,EAAA,MAAM,iBAAiB,iBAAA,EAAkB;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,YAAA;AAOjB,EAAA,MAAM,CAAC,eAAA,EAAiB,kBAAkB,CAAA,GAAI,SAAS,YAAY,CAAA;AACnE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,YAAA,qBAAiC,YAAY,CAAA;AAAA,EACnD,CAAA,EAAG,CAAC,YAAY,CAAC,CAAA;AAEjB,EAAA,MAAM,GAAA,GAAM,OAAO,sBAAsB,CAAA;AACzC,EAAA,MAAM,QAAQ,iBAAA,EAAkB;AAChC,EAAA,MAAM,MAAA,GAAS,OAAO,YAAY,CAAA;AAClC,EAAA,MAAM,aAAa,IAAA,CAAK,GAAA;AAAA,IACtB,eAAA;AAAA,IACA,MAAA,CAAO,iBAAA,CAAkB,8BAA8B,CAAA,IACrD;AAAA,GACJ;AAOA,EAAA,MAAM,YAAY,YAAA,EAAc,SAAA;AAChC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAA,EAAW,KAAA,CAAM,SAAA,EAAW,MAAM,CAAA;AAAA,EAExC,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAA,MAAM,UAAA,GAAa,CAAC,CAAC,eAAA,EAAiB,cAAA;AACtC,EAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,eAAA,EAAiB,SAAA,EAAW,UAAU,CAAA;AACxE,EAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,eAAA,EAAiB,SAAS,CAAA;AAC5D,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAS,KAAK,CAAA;AACpD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAoB,MAAM,CAAA;AAC5D,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAoB,MAAM,CAAA;AAI9D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,aAAA,CAAc,MAAM,CAAA;AAAA,EACtB,CAAA,EAAG,CAAC,eAAA,EAAiB,SAAS,CAAC,CAAA;AAE/B,EAAA,IAAI,CAAC,iBAAiB,OAAO,IAAA;AAE7B,EAAA,MAAM,QAAA,GAAW,eAAA;AACjB,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,QAAA,CAAS,IAAI,CAAA;AAEtC,EAAA,MAAM,aAAa,YAAY;AAC7B,IAAA,IAAI,CAAC,UAAU,IAAA,EAAM;AACrB,IAAA,MAAM,EAAA,GAAK,MAAM,eAAA,CAAgB,SAAA,CAAU,KAAK,OAAO,CAAA;AACvD,IAAA,IAAI,EAAA,EAAI,KAAA,CAAM,QAAA,CAAS,SAAA,EAAW,MAAM,CAAA;AACxC,IAAA,KAAA,CAAM,cAAc,EAAE,CAAA;AAAA,EACxB,CAAA;AAIA,EAAA,MAAM,cAAc,YAAY;AAC9B,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,MAAA,CAAO,SAAS,IAAI,CAAA;AACxC,IAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,UAAA,EAAY,QAAA,CAAS,SAAS,CAAA;AACnD,IAAA,KAAA,CAAM,eAAe,MAAM,eAAA,CAAgB,GAAA,CAAI,QAAA,EAAU,CAAC,CAAA;AAAA,EAC5D,CAAA;AAEA,EAAA,MAAM,iBAAiB,MAAM;AAC3B,IAAA,KAAA,CAAM,QAAA,CAAS,WAAW,UAAU,CAAA;AACpC,IAAA,OAAO,GAAA,CAAI,kBAAA,CAAmB,QAAA,CAAS,SAAS,CAAA;AAAA,EAClD,CAAA;AAIA,EAAA,MAAM,aAAA,GAAgB,MAAM,cAAA,CAAe,IAAI,CAAA;AAI/C,EAAA,MAAM,YAAA,GAA+C;AAAA,IACnD,CAAC,OAAA,EAAS,QAAA,CAAS,KAAK,CAAA;AAAA,IACxB,CAAC,YAAA,EAAc,QAAA,CAAS,SAAS;AAAA,GACnC;AAMA,EAAA,MAAM,8BACJ,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,CAAC,UAAA,wBACC,IAAA,EAAA,EAAK,OAAA,EAAQ,cAAa,EAAA,EAAG,GAAA,EAAI,KAAA,EAAM,WAAA,EAAY,QAAA,EAAA,kDAAA,EAEpD,CAAA;AAAA,IAED,UAAA,IAAc,SAAA,CAAU,KAAA,KAAU,WAAA,oBACjC,GAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,YAAA,EAAa,EAAA,EAAG,GAAA,EAAI,KAAA,EAAM,WAAA,EAAY,QAAA,EAAA,0FAAA,EAGpD,CAAA;AAAA,IAED,UAAA,IAAc,UAAU,KAAA,KAAU,UAAA,yBAChC,IAAA,EAAA,EAAK,KAAA,EAAM,QAAA,EAAS,GAAA,EAAI,GAAA,EACvB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,YAAA,EAAa,IAAG,GAAA,EAAI,KAAA,EAAM,aAAY,QAAA,EAAA,kDAAA,EAEpD,CAAA;AAAA,sBACA,GAAA,CAAC,UAAO,IAAA,EAAK,OAAA,EAAQ,SAAQ,UAAA,EAAW,OAAA,EAAS,SAAA,CAAU,KAAA,EAAO,QAAA,EAAA,OAAA,EAElE;AAAA,KAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAGF,EAAA,MAAM,iBACJ,CAAC,UAAA,IACD,UAAU,KAAA,KAAU,WAAA,IACpB,UAAU,KAAA,KAAU,UAAA;AAEtB,EAAA,MAAM,+BACJ,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,WAAA;AAAA,IACA,cAAc,SAAA,CAAU,OAAA,yBACtB,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,WAAA,EACrB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,MAAA,EAAO,MAAA,EAAQ,EAAA,EAAI,CAAA;AAAA,sBACnC,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,KAAA,EAAM,QAAQ,EAAA,EAAI,CAAA;AAAA,sBAClC,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,KAAA,EAAM,QAAQ,EAAA,EAAI;AAAA,KAAA,EACpC,CAAA;AAAA,IAED,SAAA,CAAU,IAAA,oBACT,GAAA,CAAC,KAAA,EAAA,EAAI,WAAW,MAAA,CAAO,QAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,aAAA,EAAA,EACE,QAAA,EAAA,gBAAA,CAAiB,SAAA,CAAU,IAAA,CAAK,OAAO,GAC1C,CAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAGF,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,WAAW,MAAA,CAAO,OAAA;AAAA,QAClB,WAAA,EAAW,MAAA;AAAA,QACX,OAAA,EAAS,OAAA;AAAA,QACT,IAAA,EAAK,cAAA;AAAA,QACL,eAAa,CAAC;AAAA;AAAA,KAChB;AAAA,oBACA,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,WAAW,MAAA,CAAO,MAAA;AAAA,QAClB,WAAA,EAAW,MAAA;AAAA,QACX,KAAA,EACE;AAAA,UACE,mBAAmB,IAAA,CAAK,KAAA;AAAA,UACxB,gBAAA,EAAkB,GAAG,UAAU,CAAA,EAAA;AAAA,SACjC;AAAA,QAEF,IAAA,EAAK,QAAA;AAAA,QACL,YAAA,EAAY,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA;AAAA,QACvC,eAAa,CAAC,MAAA;AAAA,QAEd,QAAA,EAAA;AAAA,0BAAA,IAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,MAAA,EACtB,QAAA,EAAA;AAAA,4BAAA,GAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,WAAW,MAAA,CAAO,OAAA;AAAA,gBAClB,KAAA,EACE;AAAA,kBACE,iBAAiB,IAAA,CAAK,KAAA;AAAA,kBACtB,oBAAoB,IAAA,CAAK;AAAA,iBAC3B;AAAA,gBAGF,QAAA,kBAAA,GAAA,CAAC,IAAA,CAAK,IAAA,EAAL,EAAU,MAAM,EAAA,EAAI;AAAA;AAAA,aACvB;AAAA,4BACA,IAAA,CAAC,GAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,UAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,aAAA,EAAc,MAAA,EAAO,MAAA,EAAO,IAAG,IAAA,EAC1C,QAAA,EAAA,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA,EAC9B,CAAA;AAAA,8BACA,GAAA;AAAA,gBAAC,IAAA;AAAA,gBAAA;AAAA,kBACC,OAAA,EAAQ,cAAA;AAAA,kBACR,WAAW,MAAA,CAAO,SAAA;AAAA,kBAClB,KAAA,EAAO,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM;AAAA,kBAE1B,QAAA,EAAA,IAAA,CAAK;AAAA;AAAA,eACR;AAAA,cAAA,CAKE,QAAA,CAAS,OAAA,IAAW,CAAC,CAAC,QAAQ,IAAA,IAAQ,CAAC,CAAC,MAAA,EAAQ,OAAA,qBAChD,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,YAAA,EACpB,QAAA,EAAA;AAAA,gBAAA,QAAA,CAAS,OAAA,oBACR,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,eAAA,EAAA,EAAgB,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE,GAAA;AAAA,kBAC5B,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,EAAE;AAAA,iBAAA,EACpC,CAAA;AAAA,gBAED,CAAC,CAAC,MAAA,EAAQ,IAAA,yBACR,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,SAAA,EAAA,EAAU,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE,GAAA;AAAA,kBAAE,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,MAAM;AAAA,iBAAA,EACzD,CAAA;AAAA,gBAED,CAAC,CAAC,MAAA,EAAQ,OAAA,yBACR,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,cAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,aAAA,EAAA,EAAc,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAG,GAAA;AAAA,kBAC3B,UAAA,CAAW,MAAA,CAAO,OAAA,EAAS,SAAS;AAAA,iBAAA,EACvC;AAAA,eAAA,EAEJ;AAAA,aAAA,EAEJ,CAAA;AAAA,4BACA,GAAA;AAAA,cAAC,UAAA;AAAA,cAAA;AAAA,gBACC,IAAA,EAAM,WAAW,UAAU,CAAA;AAAA,gBAC3B,OAAA,EAAQ,UAAA;AAAA,gBACR,OAAA,EAAS,WAAA;AAAA,gBACT,YAAA,EAAY,YAAY,UAAU;AAAA;AAAA,aACpC;AAAA,4BACA,GAAA;AAAA,cAAC,UAAA;AAAA,cAAA;AAAA,gBACC,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,gBACnB,OAAA,EAAQ,UAAA;AAAA,gBACR,OAAA,EAAS,OAAA;AAAA,gBACT,YAAA,EAAW;AAAA;AAAA;AACb,WAAA,EACF,CAAA;AAAA,0BAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,IAAA,EACpB,QAAA,EAAA;AAAA,YAAA,QAAA,CAAS,WAAA,oBACR,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,cAAA,EAE7D,CAAA;AAAA,kCACC,IAAA,EAAA,EAAK,OAAA,EAAQ,cAAa,EAAA,EAAG,GAAA,EAC3B,mBAAS,WAAA,EACZ;AAAA,aAAA,EACF,CAAA;AAAA,4BAGF,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,WAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAW,MAAA,CAAO,QAAA,EACnB,uBACE,MAAA,CAAO,CAAC,GAAG,CAAC,CAAA,KAAM,CAAC,CAAA,CACnB,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,sBACT,IAAA,CAAC,KAAA,EAAA,EAAY,SAAA,EAAW,MAAA,CAAO,OAAA,EAC7B,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,QAAI,QAAA,EAAA,CAAA,EAAE,CAAA;AAAA,gCACP,GAAA,CAAC,IAAA,EAAA,EACE,QAAA,EAAA,eAAA,CAAgB,GAAA,CAAI,CAAC,CAAA,IAAK,oBAAA,CAAqB,CAAE,CAAA,uBAC/C,aAAA,EAAA,EAAc,SAAA,EAAW,CAAA,EAAK,QAAA,EAAA,CAAA,EAAE,IAEjC,CAAA,EAEJ;AAAA,eAAA,EAAA,EARQ,CASV,CACD,CAAA,EACL;AAAA,aAAA,EACF,CAAA;AAAA,YAEC,QAAA,CAAS,WAAW,MAAA,GAAS,CAAA,yBAC3B,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,aAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACpB,QAAA,EAAA,QAAA,CAAS,UAAA,CAAW,GAAA,CAAI,CAAA,CAAA,qBACvB,IAAA,CAAC,MAAA,EAAA,EAAa,SAAA,EAAW,OAAO,cAAA,EAC9B,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,QAAA,EAAA,EAAS,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,EAAA,EAAI,CAAA;AAAA,gBAC5B,eAAe,CAAC;AAAA,eAAA,EAAA,EAFR,CAGX,CACD,CAAA,EACH;AAAA,aAAA,EACF,CAAA;AAAA,YAGD,UAAA,oBACC,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACpB,QAAA,EAAA;AAAA,cAAA,eAAA,CAAgB,QAAA,CAAS,IAAI,CAAA,oBAC5B,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,WAAA;AAAA,kBACR,SAAA,sBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,kBAC3B,UAAA,EAAY,CAAC,SAAA,CAAU,IAAA;AAAA,kBACvB,OAAA,EAAS,UAAA;AAAA,kBAER,qBAAW,SAAS;AAAA;AAAA,eACvB;AAAA,cAED,uBAAA,CAAwB,QAAA,CAAS,IAAI,CAAA,oBACpC,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,WAAA;AAAA,kBACR,SAAA,sBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,kBAC3B,OAAA,EAAS,cAAA;AAAA,kBACV,QAAA,EAAA;AAAA;AAAA,eAED;AAAA,8BAEF,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,OAAA;AAAA,kBACL,OAAA,EAAQ,SAAA;AAAA,kBACR,SAAA,sBAAY,aAAA,EAAA,EAAc,CAAA;AAAA,kBAC1B,OAAA,EAAS,aAAA;AAAA,kBACV,QAAA,EAAA;AAAA;AAAA;AAED,aAAA,EACF,CAAA;AAAA,YAGD,MAAA,oBACC,GAAA;AAAA,cAAC,qBAAA;AAAA,cAAA;AAAA,gBACC,QAAA;AAAA,gBACA,KAAA;AAAA,gBACA;AAAA;AAAA,aACF;AAAA,YAGD,QAAA,CAAS,KAAK,MAAA,GAAS,CAAA,yBACrB,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,OAAA,EAE7D,CAAA;AAAA,8BACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,MAAA,EACpB,QAAA,EAAA,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,CAAA,CAAA,qBACjB,IAAA,CAAC,MAAA,EAAA,EAAa,SAAA,EAAW,OAAO,cAAA,EAAgB,QAAA,EAAA;AAAA,gBAAA,GAAA;AAAA,gBAC5C;AAAA,eAAA,EAAA,EADO,CAEX,CACD,CAAA,EACH;AAAA,aAAA,EACF,CAAA;AAAA,YAGD,mBAAA,CAAoB,SAAS,IAAI,CAAA,yBAC/B,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAK,OAAA,EAAQ,cAAA,EAAe,OAAM,WAAA,EAAY,MAAA,EAAO,QAAO,QAAA,EAAA,UAAA,EAE7D,CAAA;AAAA,cACC;AAAA,aAAA,EACH,CAAA;AAAA,YAMD,CAAC,mBAAA,CAAoB,QAAA,CAAS,IAAI,CAAA,IAAK,cAAA,oBACtC,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EAAU,QAAA,EAAA,WAAA,EAAY,CAAA;AAAA,YAG9C,SAAS,cAAA,oBACR,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,OAAA,EACrB,QAAA,kBAAA,GAAA;AAAA,cAAC,IAAA;AAAA,cAAA;AAAA,gBACC,IAAA,EAAM,QAAA,CAAS,cAAA,CAAe,OAAA,CAAQ,SAAS,EAAE,CAAA;AAAA,gBACjD,MAAA,EAAO,QAAA;AAAA,gBACP,GAAA,EAAI,qBAAA;AAAA,gBAEJ,QAAA,kBAAA,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,CAAO,iBAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,kBAAA,EAAA,EAAmB,MAAM,EAAA,EAAI,CAAA;AAAA,kBAAE;AAAA,iBAAA,EAClC;AAAA;AAAA,aACF,EACF;AAAA,WAAA,EAEJ;AAAA;AAAA;AAAA,KACF;AAAA,oBAEA,GAAA;AAAA,MAAC,qBAAA;AAAA,MAAA;AAAA,QACC,QAAA;AAAA,QACA,MAAM,SAAA,CAAU,IAAA;AAAA,QAChB,aAAa,SAAA,CAAU,OAAA;AAAA,QACvB,MAAA,EAAQ,WAAA;AAAA,QACR,YAAA,EAAc;AAAA;AAAA;AAChB,GAAA,EACF,CAAA;AAEJ;;;;"}
|
|
@@ -3,7 +3,9 @@ import { useState, useRef, useEffect } from 'react';
|
|
|
3
3
|
import { Flex, SearchField, Box, Text } from '@backstage/ui';
|
|
4
4
|
import { RiAppsLine, RiCheckLine } from '@remixicon/react';
|
|
5
5
|
import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getTypeMeta } from './typeMeta.esm.js';
|
|
7
|
+
import { useFrameworkLabel } from '../../hooks/useFrameworkLabel.esm.js';
|
|
8
|
+
import { SortControl } from './SortControl.esm.js';
|
|
7
9
|
import styles from './ResourceFilters.module.css.esm.js';
|
|
8
10
|
|
|
9
11
|
function useDropdownClose(ref, onClose, open) {
|
|
@@ -163,8 +165,11 @@ function ResourceFilters({
|
|
|
163
165
|
value,
|
|
164
166
|
onChange,
|
|
165
167
|
availableFrameworks,
|
|
166
|
-
availableTags
|
|
168
|
+
availableTags,
|
|
169
|
+
availableTypes,
|
|
170
|
+
sort
|
|
167
171
|
}) {
|
|
172
|
+
const frameworkLabel = useFrameworkLabel();
|
|
168
173
|
const toolSelectOptions = [
|
|
169
174
|
{
|
|
170
175
|
value: "all",
|
|
@@ -183,6 +188,27 @@ function ResourceFilters({
|
|
|
183
188
|
icon: /* @__PURE__ */ jsx(ToolIcon, { tool: fw, branded: true, size: 14 })
|
|
184
189
|
}))
|
|
185
190
|
];
|
|
191
|
+
const typeSelectOptions = [
|
|
192
|
+
{
|
|
193
|
+
value: "all",
|
|
194
|
+
label: "All Types",
|
|
195
|
+
icon: /* @__PURE__ */ jsx(
|
|
196
|
+
RiAppsLine,
|
|
197
|
+
{
|
|
198
|
+
size: 14,
|
|
199
|
+
style: { color: "var(--bui-fg-secondary)", flexShrink: 0 }
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
},
|
|
203
|
+
...(availableTypes ?? []).map((type) => {
|
|
204
|
+
const meta = getTypeMeta(type);
|
|
205
|
+
return {
|
|
206
|
+
value: type,
|
|
207
|
+
label: meta.pluralLabel,
|
|
208
|
+
icon: /* @__PURE__ */ jsx(meta.Icon, { size: 14, style: { color: meta.color, flexShrink: 0 } })
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
];
|
|
186
212
|
return /* @__PURE__ */ jsxs(Flex, { className: styles.container, children: [
|
|
187
213
|
/* @__PURE__ */ jsx(
|
|
188
214
|
SearchField,
|
|
@@ -195,6 +221,26 @@ function ResourceFilters({
|
|
|
195
221
|
}
|
|
196
222
|
),
|
|
197
223
|
/* @__PURE__ */ jsxs(Flex, { className: styles.filtersRow, children: [
|
|
224
|
+
availableTypes && availableTypes.length > 0 && /* @__PURE__ */ jsxs(Box, { className: styles.filterBox, children: [
|
|
225
|
+
/* @__PURE__ */ jsx(
|
|
226
|
+
Text,
|
|
227
|
+
{
|
|
228
|
+
variant: "body-x-small",
|
|
229
|
+
color: "secondary",
|
|
230
|
+
className: styles.filterLabel,
|
|
231
|
+
children: "Type"
|
|
232
|
+
}
|
|
233
|
+
),
|
|
234
|
+
/* @__PURE__ */ jsx(
|
|
235
|
+
IconSelectBox,
|
|
236
|
+
{
|
|
237
|
+
ariaLabel: "Filter by type",
|
|
238
|
+
value: value.type ?? "all",
|
|
239
|
+
options: typeSelectOptions,
|
|
240
|
+
onChange: (key) => onChange({ ...value, type: key === "all" ? void 0 : key })
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
] }),
|
|
198
244
|
(availableTags.length > 0 || value.tags.length > 0) && /* @__PURE__ */ jsxs(Box, { className: styles.filterBox, children: [
|
|
199
245
|
/* @__PURE__ */ jsx(
|
|
200
246
|
Text,
|
|
@@ -233,7 +279,15 @@ function ResourceFilters({
|
|
|
233
279
|
onChange: (key) => onChange({ ...value, framework: key === "all" ? void 0 : key })
|
|
234
280
|
}
|
|
235
281
|
)
|
|
236
|
-
] })
|
|
282
|
+
] }),
|
|
283
|
+
sort && /* @__PURE__ */ jsx(
|
|
284
|
+
SortControl,
|
|
285
|
+
{
|
|
286
|
+
mode: sort.mode,
|
|
287
|
+
direction: sort.direction,
|
|
288
|
+
onChange: sort.onChange
|
|
289
|
+
}
|
|
290
|
+
)
|
|
237
291
|
] })
|
|
238
292
|
] });
|
|
239
293
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceFilters.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceFilters.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Box, Flex, SearchField, Text } from '@backstage/ui';\nimport { RiAppsLine, RiCheckLine } from '@remixicon/react';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\nimport { ToolIcon } from '../ToolIcon';\nimport { frameworkLabel } from './typeMeta';\nimport styles from './ResourceFilters.module.css';\n\nexport interface ResourceFiltersValue {\n search: string;\n framework?: string;\n tags: string[];\n}\n\ninterface ResourceFiltersProps {\n value: ResourceFiltersValue;\n onChange: (next: ResourceFiltersValue) => void;\n availableFrameworks: string[];\n availableTags: string[];\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\nfunction IconSelectBox<T extends string>({\n value: selected,\n options,\n onChange,\n ariaLabel,\n}: {\n value: T;\n options: { value: T; label: string; icon: ReactNode }[];\n onChange: (v: T) => void;\n ariaLabel: string;\n}) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n useDropdownClose(containerRef, () => setOpen(false), open);\n\n const current = options.find(o => o.value === selected) ?? options[0];\n\n return (\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={ariaLabel}\n aria-haspopup=\"listbox\"\n >\n <span className={styles.iconSelectTriggerContent}>\n {current.icon}\n <span>{current.label}</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={ariaLabel}\n >\n {options.map(opt => (\n <button\n key={opt.value}\n type=\"button\"\n role=\"option\"\n aria-selected={opt.value === selected}\n className={`${styles.iconSelectItem} ${\n opt.value === selected ? styles.iconSelectItemActive : ''\n }`}\n onClick={() => {\n onChange(opt.value);\n setOpen(false);\n }}\n >\n <span className={styles.iconSelectItemIcon}>{opt.icon}</span>\n <span>{opt.label}</span>\n {opt.value === selected && (\n <RiCheckLine size={14} className={styles.iconSelectCheck} />\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction TagsFilterBox({\n selectedTags,\n availableTags,\n onChange,\n}: {\n selectedTags: string[];\n availableTags: string[];\n onChange: (tags: string[]) => void;\n}) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const containerRef = useRef<HTMLDivElement>(null);\n useDropdownClose(containerRef, () => setOpen(false), open);\n\n const allTags = Array.from(new Set([...availableTags, ...selectedTags]));\n const filtered = allTags.filter(t =>\n t.toLowerCase().includes(search.toLowerCase()),\n );\n\n const toggleTag = (tag: string) => {\n onChange(\n selectedTags.includes(tag)\n ? selectedTags.filter(t => t !== tag)\n : [...selectedTags, tag],\n );\n };\n\n const triggerLabel =\n selectedTags.length > 0\n ? selectedTags.map(t => `#${t}`).join(' ')\n : 'All Tags';\n const showZeroResultsHint =\n availableTags.length === 0 && selectedTags.length > 0;\n\n return (\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=\"Filter by tags\"\n aria-haspopup=\"dialog\"\n >\n <span>{triggerLabel}</span>\n <span className={styles.tagsDropdownArrow}>▾</span>\n </button>\n {showZeroResultsHint && (\n <p className={styles.tagsDropdownNoResults}>\n Clear tags to see results…\n </p>\n )}\n {open && (\n <div\n className={styles.tagsDropdownPanel}\n role=\"dialog\"\n aria-label=\"Filter by tags\"\n >\n <input\n type=\"search\"\n className={styles.tagsDropdownSearch}\n placeholder=\"Search tags…\"\n value={search}\n onChange={e => setSearch(e.target.value)}\n // eslint-disable-next-line jsx-a11y/no-autofocus\n autoFocus\n />\n <div className={styles.tagsDropdownSectionLabel}>\n FILTER YOUR SEARCH\n </div>\n <div className={styles.tagsDropdownList}>\n {filtered.length === 0 && (\n <div className={styles.tagsDropdownNoResults}>No tags found</div>\n )}\n {filtered.map(tag => (\n <label key={tag} className={styles.tagsDropdownItem}>\n <input\n type=\"checkbox\"\n className={styles.tagsDropdownCheckbox}\n aria-label={tag}\n checked={selectedTags.includes(tag)}\n onChange={() => toggleTag(tag)}\n />\n <span>{tag}</span>\n </label>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Search + labelled filter boxes, copied from the legacy AssetFilters\n * design: full-width search bar with the Tags and AI Tool dropdowns below\n * it. Composes with the StatTiles type filter; search is client-side over\n * name/description/tags (bodies are not in the catalog, ADR-0001).\n */\nexport function ResourceFilters({\n value,\n onChange,\n availableFrameworks,\n availableTags,\n}: ResourceFiltersProps) {\n const toolSelectOptions = [\n {\n value: 'all',\n label: 'All Tools',\n icon: (\n <RiAppsLine\n size={14}\n style={{ color: 'var(--bui-fg-secondary)', flexShrink: 0 }}\n />\n ),\n },\n ...availableFrameworks.map(fw => ({\n value: fw,\n label: frameworkLabel(fw),\n icon: <ToolIcon tool={fw as FrameworkToken} branded size={14} />,\n })),\n ];\n\n return (\n <Flex className={styles.container}>\n <SearchField\n aria-label=\"Search resources\"\n className={styles.searchField}\n placeholder=\"Search by name, description or tags…\"\n value={value.search}\n onChange={search => onChange({ ...value, search })}\n />\n\n <Flex className={styles.filtersRow}>\n {(availableTags.length > 0 || value.tags.length > 0) && (\n <Box className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n Tags\n </Text>\n <TagsFilterBox\n selectedTags={value.tags}\n availableTags={availableTags}\n onChange={tags => onChange({ ...value, tags })}\n />\n </Box>\n )}\n\n <Box className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n AI Tool\n </Text>\n <IconSelectBox\n ariaLabel=\"Filter by AI tool\"\n value={value.framework ?? 'all'}\n options={toolSelectOptions}\n onChange={key =>\n onChange({ ...value, framework: key === 'all' ? undefined : key })\n }\n />\n </Box>\n </Flex>\n </Flex>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ResourceFilters.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceFilters.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Box, Flex, SearchField, Text } from '@backstage/ui';\nimport { RiAppsLine, RiCheckLine } from '@remixicon/react';\nimport type {\n ResourceType,\n SortDirection,\n SortMode,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { ToolIcon } from '../ToolIcon';\nimport { getTypeMeta } from './typeMeta';\nimport { useFrameworkLabel } from '../../hooks/useFrameworkLabel';\nimport { SortControl } from './SortControl';\nimport styles from './ResourceFilters.module.css';\n\nexport interface ResourceFiltersValue {\n search: string;\n framework?: string;\n tags: string[];\n /** AiResource type filter. Only surfaced when `availableTypes` is passed\n * (the browse page filters type through StatTiles instead). */\n type?: ResourceType;\n}\n\ninterface ResourceFiltersProps {\n value: ResourceFiltersValue;\n onChange: (next: ResourceFiltersValue) => void;\n availableFrameworks: string[];\n availableTags: string[];\n /** When provided and non-empty, a Type dropdown is shown alongside Tags and\n * AI Tool. Omitted by the browse page, which owns type via its stat tiles. */\n availableTypes?: ResourceType[];\n /**\n * The sort dropdown + direction toggle (issue #102), rendered at the right\n * edge of the filters row when provided. Kept as separate props rather\n * than folded into `ResourceFiltersValue`: sorting is a distinct concept\n * from filtering (it reorders rather than narrows the list), and every\n * other page that might reuse `ResourceFilters` in future need not carry\n * sort state just to render filters.\n */\n sort?: {\n mode: SortMode;\n direction: SortDirection;\n onChange: (mode: SortMode, direction: SortDirection) => void;\n };\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\nfunction IconSelectBox<T extends string>({\n value: selected,\n options,\n onChange,\n ariaLabel,\n}: {\n value: T;\n options: { value: T; label: string; icon: ReactNode }[];\n onChange: (v: T) => void;\n ariaLabel: string;\n}) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n useDropdownClose(containerRef, () => setOpen(false), open);\n\n const current = options.find(o => o.value === selected) ?? options[0];\n\n return (\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={ariaLabel}\n aria-haspopup=\"listbox\"\n >\n <span className={styles.iconSelectTriggerContent}>\n {current.icon}\n <span>{current.label}</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={ariaLabel}\n >\n {options.map(opt => (\n <button\n key={opt.value}\n type=\"button\"\n role=\"option\"\n aria-selected={opt.value === selected}\n className={`${styles.iconSelectItem} ${\n opt.value === selected ? styles.iconSelectItemActive : ''\n }`}\n onClick={() => {\n onChange(opt.value);\n setOpen(false);\n }}\n >\n <span className={styles.iconSelectItemIcon}>{opt.icon}</span>\n <span>{opt.label}</span>\n {opt.value === selected && (\n <RiCheckLine size={14} className={styles.iconSelectCheck} />\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction TagsFilterBox({\n selectedTags,\n availableTags,\n onChange,\n}: {\n selectedTags: string[];\n availableTags: string[];\n onChange: (tags: string[]) => void;\n}) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const containerRef = useRef<HTMLDivElement>(null);\n useDropdownClose(containerRef, () => setOpen(false), open);\n\n const allTags = Array.from(new Set([...availableTags, ...selectedTags]));\n const filtered = allTags.filter(t =>\n t.toLowerCase().includes(search.toLowerCase()),\n );\n\n const toggleTag = (tag: string) => {\n onChange(\n selectedTags.includes(tag)\n ? selectedTags.filter(t => t !== tag)\n : [...selectedTags, tag],\n );\n };\n\n const triggerLabel =\n selectedTags.length > 0\n ? selectedTags.map(t => `#${t}`).join(' ')\n : 'All Tags';\n const showZeroResultsHint =\n availableTags.length === 0 && selectedTags.length > 0;\n\n return (\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=\"Filter by tags\"\n aria-haspopup=\"dialog\"\n >\n <span>{triggerLabel}</span>\n <span className={styles.tagsDropdownArrow}>▾</span>\n </button>\n {showZeroResultsHint && (\n <p className={styles.tagsDropdownNoResults}>\n Clear tags to see results…\n </p>\n )}\n {open && (\n <div\n className={styles.tagsDropdownPanel}\n role=\"dialog\"\n aria-label=\"Filter by tags\"\n >\n <input\n type=\"search\"\n className={styles.tagsDropdownSearch}\n placeholder=\"Search tags…\"\n value={search}\n onChange={e => setSearch(e.target.value)}\n // eslint-disable-next-line jsx-a11y/no-autofocus\n autoFocus\n />\n <div className={styles.tagsDropdownSectionLabel}>\n FILTER YOUR SEARCH\n </div>\n <div className={styles.tagsDropdownList}>\n {filtered.length === 0 && (\n <div className={styles.tagsDropdownNoResults}>No tags found</div>\n )}\n {filtered.map(tag => (\n <label key={tag} className={styles.tagsDropdownItem}>\n <input\n type=\"checkbox\"\n className={styles.tagsDropdownCheckbox}\n aria-label={tag}\n checked={selectedTags.includes(tag)}\n onChange={() => toggleTag(tag)}\n />\n <span>{tag}</span>\n </label>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Search + labelled filter boxes, copied from the legacy AssetFilters\n * design: full-width search bar with the Tags and AI Tool dropdowns below\n * it. Composes with the StatTiles type filter; search is client-side over\n * name/description/tags (bodies are not in the catalog, ADR-0001).\n */\nexport function ResourceFilters({\n value,\n onChange,\n availableFrameworks,\n availableTags,\n availableTypes,\n sort,\n}: ResourceFiltersProps) {\n const frameworkLabel = useFrameworkLabel();\n const toolSelectOptions = [\n {\n value: 'all',\n label: 'All Tools',\n icon: (\n <RiAppsLine\n size={14}\n style={{ color: 'var(--bui-fg-secondary)', flexShrink: 0 }}\n />\n ),\n },\n ...availableFrameworks.map(fw => ({\n value: fw,\n label: frameworkLabel(fw),\n icon: <ToolIcon tool={fw} branded size={14} />,\n })),\n ];\n\n const typeSelectOptions: {\n value: ResourceType | 'all';\n label: string;\n icon: ReactNode;\n }[] = [\n {\n value: 'all',\n label: 'All Types',\n icon: (\n <RiAppsLine\n size={14}\n style={{ color: 'var(--bui-fg-secondary)', flexShrink: 0 }}\n />\n ),\n },\n ...(availableTypes ?? []).map(type => {\n const meta = getTypeMeta(type);\n return {\n value: type,\n label: meta.pluralLabel,\n icon: (\n <meta.Icon size={14} style={{ color: meta.color, flexShrink: 0 }} />\n ),\n };\n }),\n ];\n\n return (\n <Flex className={styles.container}>\n <SearchField\n aria-label=\"Search resources\"\n className={styles.searchField}\n placeholder=\"Search by name, description or tags…\"\n value={value.search}\n onChange={search => onChange({ ...value, search })}\n />\n\n <Flex className={styles.filtersRow}>\n {availableTypes && availableTypes.length > 0 && (\n <Box className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n Type\n </Text>\n <IconSelectBox<ResourceType | 'all'>\n ariaLabel=\"Filter by type\"\n value={value.type ?? 'all'}\n options={typeSelectOptions}\n onChange={key =>\n onChange({ ...value, type: key === 'all' ? undefined : key })\n }\n />\n </Box>\n )}\n\n {(availableTags.length > 0 || value.tags.length > 0) && (\n <Box className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n Tags\n </Text>\n <TagsFilterBox\n selectedTags={value.tags}\n availableTags={availableTags}\n onChange={tags => onChange({ ...value, tags })}\n />\n </Box>\n )}\n\n <Box className={styles.filterBox}>\n <Text\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.filterLabel}\n >\n AI Tool\n </Text>\n <IconSelectBox\n ariaLabel=\"Filter by AI tool\"\n value={value.framework ?? 'all'}\n options={toolSelectOptions}\n onChange={key =>\n onChange({ ...value, framework: key === 'all' ? undefined : key })\n }\n />\n </Box>\n\n {sort && (\n <SortControl\n mode={sort.mode}\n direction={sort.direction}\n onChange={sort.onChange}\n />\n )}\n </Flex>\n </Flex>\n );\n}\n"],"names":[],"mappings":"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"}
|