@nospt/plugin-dev-ai-hub 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/DevAiHubResourceClient.esm.js +97 -0
- package/dist/api/DevAiHubResourceClient.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +150 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceCard.esm.js +112 -0
- package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +238 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js +242 -0
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +366 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.aurora.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/StatTiles.aurora.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.console.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/StatTiles.console.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.esm.js +82 -0
- package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/index.esm.js +6 -0
- package/dist/components/AiResourcesPage/typeMeta.esm.js +34 -0
- package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js +28 -10
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
- package/dist/hooks/useResourceBody.esm.js +39 -0
- package/dist/hooks/useResourceBody.esm.js.map +1 -0
- package/dist/hooks/useResources.esm.js +26 -0
- package/dist/hooks/useResources.esm.js.map +1 -0
- package/dist/hooks/useTelemetryCounts.esm.js +24 -0
- package/dist/hooks/useTelemetryCounts.esm.js.map +1 -0
- package/dist/index.d.ts +1 -38
- package/dist/index.esm.js +1 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/plugin.esm.js +15 -24
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js +1 -2
- package/dist/routes.esm.js.map +1 -1
- package/package.json +17 -10
- package/.eslintrc.js +0 -1
- package/dev/index.tsx +0 -32
- package/dist/api/DevAiHubClient.esm.js +0 -78
- package/dist/api/DevAiHubClient.esm.js.map +0 -1
- package/dist/components/AssetCard/AssetCard.esm.js +0 -120
- package/dist/components/AssetCard/AssetCard.esm.js.map +0 -1
- package/dist/components/AssetCard/AssetCard.module.css.esm.js +0 -8
- package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +0 -1
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +0 -218
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +0 -1
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +0 -8
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +0 -1
- package/dist/components/AssetFilters/AssetFilters.esm.js +0 -176
- package/dist/components/AssetFilters/AssetFilters.esm.js.map +0 -1
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +0 -8
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +0 -1
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +0 -138
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +0 -1
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +0 -8
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +0 -195
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +0 -8
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/index.esm.js +0 -6
- package/dist/components/McpConfigPage/McpConfigPage.esm.js +0 -200
- package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +0 -1
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +0 -8
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +0 -1
- package/dist/components/McpConfigPage/index.esm.js +0 -6
- package/dist/components/McpConfigPage/index.esm.js.map +0 -1
- package/dist/hooks/index.esm.js +0 -114
- package/dist/hooks/index.esm.js.map +0 -1
- package/dist/pluginLegacy.esm.js +0 -27
- package/dist/pluginLegacy.esm.js.map +0 -1
- package/package.json-prepack +0 -85
- package/src/api/DevAiHubClient.ts +0 -110
- package/src/components/AssetCard/AssetCard.module.css +0 -153
- package/src/components/AssetCard/AssetCard.test.tsx +0 -182
- package/src/components/AssetCard/AssetCard.tsx +0 -152
- package/src/components/AssetCard/index.ts +0 -1
- package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +0 -207
- package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +0 -313
- package/src/components/AssetDetailPanel/index.ts +0 -1
- package/src/components/AssetFilters/AssetFilters.module.css +0 -67
- package/src/components/AssetFilters/AssetFilters.tsx +0 -219
- package/src/components/AssetFilters/index.ts +0 -2
- package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +0 -72
- package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +0 -176
- package/src/components/AssetInstallDialog/index.ts +0 -1
- package/src/components/DevAiHubPage/DevAiHubPage.module.css +0 -105
- package/src/components/DevAiHubPage/DevAiHubPage.tsx +0 -197
- package/src/components/DevAiHubPage/index.ts +0 -1
- package/src/components/McpConfigDialog/McpConfigDialog.module.css +0 -111
- package/src/components/McpConfigDialog/McpConfigDialog.tsx +0 -271
- package/src/components/McpConfigDialog/index.ts +0 -1
- package/src/components/McpConfigPage/McpConfigPage.module.css +0 -121
- package/src/components/McpConfigPage/McpConfigPage.tsx +0 -242
- package/src/components/McpConfigPage/index.ts +0 -1
- package/src/components/ToolIcon/ToolIcon.tsx +0 -65
- package/src/components/ToolIcon/index.ts +0 -1
- package/src/css-modules.d.ts +0 -4
- package/src/hooks/index.ts +0 -152
- package/src/index.ts +0 -10
- package/src/plugin.tsx +0 -60
- package/src/pluginLegacy.ts +0 -42
- package/src/routes.ts +0 -4
- package/tests/e2e/.auth.json +0 -14
- package/tests/e2e/asset-detail.spec.ts +0 -90
- package/tests/e2e/asset-filtering.spec.ts +0 -145
- package/tests/e2e/asset-installation.spec.ts +0 -74
- package/tests/e2e/dev-ai-hub-page.spec.ts +0 -89
- package/tests/e2e/fixtures/base.ts +0 -61
- package/tests/e2e/fixtures/mock-api.ts +0 -221
- package/tests/e2e/mcp-config.spec.ts +0 -88
- package/tsconfig.json +0 -10
- /package/dist/components/{DevAiHubPage → AiResourcesPage}/index.esm.js.map +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createApiRef } from '@backstage/core-plugin-api';
|
|
2
|
+
|
|
3
|
+
const devAiHubResourceApiRef = createApiRef({
|
|
4
|
+
id: "plugin.dev-ai-hub.resources"
|
|
5
|
+
});
|
|
6
|
+
class ResourceBodyError extends Error {
|
|
7
|
+
constructor(message, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "ResourceBodyError";
|
|
11
|
+
}
|
|
12
|
+
status;
|
|
13
|
+
}
|
|
14
|
+
class DevAiHubResourceClient {
|
|
15
|
+
constructor(discoveryApi, fetchApi) {
|
|
16
|
+
this.discoveryApi = discoveryApi;
|
|
17
|
+
this.fetchApi = fetchApi;
|
|
18
|
+
}
|
|
19
|
+
discoveryApi;
|
|
20
|
+
fetchApi;
|
|
21
|
+
async getResources() {
|
|
22
|
+
const base = await this.discoveryApi.getBaseUrl("dev-ai-hub");
|
|
23
|
+
const response = await this.fetchApi.fetch(`${base}/resources`);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
throw new Error(`Dev AI Hub API error ${response.status}: ${text}`);
|
|
27
|
+
}
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
return body.items;
|
|
30
|
+
}
|
|
31
|
+
async getEntityBodyUrl(entityRef, options) {
|
|
32
|
+
const base = await this.discoveryApi.getBaseUrl("dev-ai-hub");
|
|
33
|
+
const url = `${base}/entity/${encodeURIComponent(entityRef)}/raw`;
|
|
34
|
+
return options?.download ? `${url}?download=true` : url;
|
|
35
|
+
}
|
|
36
|
+
async getEntityBody(entityRef) {
|
|
37
|
+
const response = await this.fetchApi.fetch(
|
|
38
|
+
await this.getEntityBodyUrl(entityRef)
|
|
39
|
+
);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new ResourceBodyError(
|
|
42
|
+
`Failed to fetch body: ${response.status}`,
|
|
43
|
+
response.status
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: await response.text(),
|
|
48
|
+
contentType: response.headers.get("content-type") ?? "text/plain"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async downloadEntityBody(entityRef) {
|
|
52
|
+
const response = await this.fetchApi.fetch(
|
|
53
|
+
await this.getEntityBodyUrl(entityRef, { download: true })
|
|
54
|
+
);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new ResourceBodyError(
|
|
57
|
+
`Failed to download body: ${response.status}`,
|
|
58
|
+
response.status
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
62
|
+
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? `${entityRef.split("/").pop() ?? "resource"}.md`;
|
|
63
|
+
const blobUrl = URL.createObjectURL(await response.blob());
|
|
64
|
+
try {
|
|
65
|
+
const anchor = document.createElement("a");
|
|
66
|
+
anchor.href = blobUrl;
|
|
67
|
+
anchor.download = filename;
|
|
68
|
+
anchor.click();
|
|
69
|
+
} finally {
|
|
70
|
+
URL.revokeObjectURL(blobUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async track(ref, action, tool) {
|
|
74
|
+
try {
|
|
75
|
+
const base = await this.discoveryApi.getBaseUrl("dev-ai-hub");
|
|
76
|
+
await this.fetchApi.fetch(`${base}/telemetry`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ ref, action, tool })
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async getInstallCount(ref) {
|
|
85
|
+
const base = await this.discoveryApi.getBaseUrl("dev-ai-hub");
|
|
86
|
+
const response = await this.fetchApi.fetch(
|
|
87
|
+
`${base}/telemetry/${encodeURIComponent(ref)}`
|
|
88
|
+
);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Dev AI Hub API error ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
return await response.json();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { DevAiHubResourceClient, ResourceBodyError, devAiHubResourceApiRef };
|
|
97
|
+
//# sourceMappingURL=DevAiHubResourceClient.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevAiHubResourceClient.esm.js","sources":["../../src/api/DevAiHubResourceClient.ts"],"sourcesContent":["import {\n createApiRef,\n DiscoveryApi,\n FetchApi,\n} from '@backstage/core-plugin-api';\nimport type {\n ResourceListResponse,\n ResourceSummary,\n TelemetryAction,\n TelemetryCounts,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/**\n * v2 API client: consumes the catalog-backed `/resources` endpoint and knows\n * only the flat `ResourceSummary` contract (architecture.md).\n */\nexport const devAiHubResourceApiRef = createApiRef<DevAiHubResourceApi>({\n id: 'plugin.dev-ai-hub.resources',\n});\n\n/** The resolved body of a resource, as served by `GET /entity/:ref/raw`. */\nexport interface ResourceBody {\n content: string;\n contentType: string;\n}\n\n/** API error carrying the HTTP status so the UI can distinguish 404 from 502. */\nexport class ResourceBodyError extends Error {\n constructor(message: string, readonly status: number) {\n super(message);\n this.name = 'ResourceBodyError';\n }\n}\n\nexport interface DevAiHubResourceApi {\n getResources(): Promise<ResourceSummary[]>;\n /** Fetch the viewable body (a directory body's entry file). */\n getEntityBody(entityRef: string): Promise<ResourceBody>;\n /** Backend URL of the body; `download: true` yields the artifact form. */\n getEntityBodyUrl(\n entityRef: string,\n options?: { download?: boolean },\n ): Promise<string>;\n /** Trigger a browser download of the artifact (file, or zip if multi-file). */\n downloadEntityBody(entityRef: string): Promise<void>;\n /** Record a telemetry event (ADR-0007). Fire-and-forget from callers. */\n track(ref: string, action: TelemetryAction, tool?: string): Promise<void>;\n /** Raw per-action lifetime counts for a resource (ADR-0007). */\n getInstallCount(ref: string): Promise<TelemetryCounts>;\n}\n\nexport class DevAiHubResourceClient implements DevAiHubResourceApi {\n constructor(\n private readonly discoveryApi: DiscoveryApi,\n private readonly fetchApi: FetchApi,\n ) {}\n\n async getResources(): Promise<ResourceSummary[]> {\n const base = await this.discoveryApi.getBaseUrl('dev-ai-hub');\n const response = await this.fetchApi.fetch(`${base}/resources`);\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Dev AI Hub API error ${response.status}: ${text}`);\n }\n const body = (await response.json()) as ResourceListResponse;\n return body.items;\n }\n\n async getEntityBodyUrl(\n entityRef: string,\n options?: { download?: boolean },\n ): Promise<string> {\n const base = await this.discoveryApi.getBaseUrl('dev-ai-hub');\n const url = `${base}/entity/${encodeURIComponent(entityRef)}/raw`;\n return options?.download ? `${url}?download=true` : url;\n }\n\n async getEntityBody(entityRef: string): Promise<ResourceBody> {\n const response = await this.fetchApi.fetch(\n await this.getEntityBodyUrl(entityRef),\n );\n if (!response.ok) {\n throw new ResourceBodyError(\n `Failed to fetch body: ${response.status}`,\n response.status,\n );\n }\n return {\n content: await response.text(),\n contentType: response.headers.get('content-type') ?? 'text/plain',\n };\n }\n\n async downloadEntityBody(entityRef: string): Promise<void> {\n // The endpoint is auth-gated (ADR-0005), so a plain <a href> cannot carry\n // the token — fetch the artifact and hand the browser a blob instead.\n const response = await this.fetchApi.fetch(\n await this.getEntityBodyUrl(entityRef, { download: true }),\n );\n if (!response.ok) {\n throw new ResourceBodyError(\n `Failed to download body: ${response.status}`,\n response.status,\n );\n }\n const disposition = response.headers.get('content-disposition') ?? '';\n const filename =\n /filename=\"([^\"]+)\"/.exec(disposition)?.[1] ??\n `${entityRef.split('/').pop() ?? 'resource'}.md`;\n\n const blobUrl = URL.createObjectURL(await response.blob());\n try {\n const anchor = document.createElement('a');\n anchor.href = blobUrl;\n anchor.download = filename;\n anchor.click();\n } finally {\n URL.revokeObjectURL(blobUrl);\n }\n }\n\n async track(\n ref: string,\n action: TelemetryAction,\n tool?: string,\n ): Promise<void> {\n // Fire-and-forget (ADR-0007): a telemetry failure must never surface as\n // a user-facing error or block the copy/download/install it's tracking.\n try {\n const base = await this.discoveryApi.getBaseUrl('dev-ai-hub');\n await this.fetchApi.fetch(`${base}/telemetry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ref, action, tool }),\n });\n } catch {\n // ignored — see above\n }\n }\n\n async getInstallCount(ref: string): Promise<TelemetryCounts> {\n const base = await this.discoveryApi.getBaseUrl('dev-ai-hub');\n const response = await this.fetchApi.fetch(\n `${base}/telemetry/${encodeURIComponent(ref)}`,\n );\n if (!response.ok) {\n throw new Error(`Dev AI Hub API error ${response.status}`);\n }\n return (await response.json()) as TelemetryCounts;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { useSearchParams } from 'react-router-dom';
|
|
4
|
+
import { Skeleton, Text, Box, Flex, TablePagination } from '@backstage/ui';
|
|
5
|
+
import { RESOURCE_TYPES } from '@nospt/plugin-dev-ai-hub-common';
|
|
6
|
+
import { useResources } from '../../hooks/useResources.esm.js';
|
|
7
|
+
import { StatTiles } from './StatTiles.esm.js';
|
|
8
|
+
import { ResourceCard } from './ResourceCard.esm.js';
|
|
9
|
+
import { ResourceFilters } from './ResourceFilters.esm.js';
|
|
10
|
+
import { ResourceDetailPanel } from './ResourceDetailPanel.esm.js';
|
|
11
|
+
import styles from './AiResourcesPage.module.css.esm.js';
|
|
12
|
+
|
|
13
|
+
const PAGE_SIZE = 24;
|
|
14
|
+
const DEFAULT_FILTERS = {
|
|
15
|
+
search: "",
|
|
16
|
+
framework: void 0,
|
|
17
|
+
tags: []
|
|
18
|
+
};
|
|
19
|
+
function matchesSearch(r, query) {
|
|
20
|
+
const q = query.trim().toLowerCase();
|
|
21
|
+
if (!q) return true;
|
|
22
|
+
return r.name.toLowerCase().includes(q) || (r.title ?? "").toLowerCase().includes(q) || (r.description ?? "").toLowerCase().includes(q) || r.tags.some((t) => t.toLowerCase().includes(q));
|
|
23
|
+
}
|
|
24
|
+
function AiResourcesPage() {
|
|
25
|
+
const { items, error, loading } = useResources();
|
|
26
|
+
const [typeFilter, setTypeFilter] = useState(
|
|
27
|
+
void 0
|
|
28
|
+
);
|
|
29
|
+
const [filters, setFilters] = useState(DEFAULT_FILTERS);
|
|
30
|
+
const [page, setPage] = useState(1);
|
|
31
|
+
const [playEntrance, setPlayEntrance] = useState(true);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (loading || !playEntrance) return void 0;
|
|
34
|
+
const timer = setTimeout(() => setPlayEntrance(false), 900);
|
|
35
|
+
return () => clearTimeout(timer);
|
|
36
|
+
}, [loading, playEntrance]);
|
|
37
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
38
|
+
const selectedRef = searchParams.get("resource");
|
|
39
|
+
const openDetail = (entityRef) => setSearchParams((p) => {
|
|
40
|
+
const n = new URLSearchParams(p);
|
|
41
|
+
n.set("resource", entityRef);
|
|
42
|
+
return n;
|
|
43
|
+
});
|
|
44
|
+
const closeDetail = () => setSearchParams((p) => {
|
|
45
|
+
const n = new URLSearchParams(p);
|
|
46
|
+
n.delete("resource");
|
|
47
|
+
return n;
|
|
48
|
+
});
|
|
49
|
+
const counts = useMemo(() => {
|
|
50
|
+
const c = Object.fromEntries(RESOURCE_TYPES.map((t) => [t, 0]));
|
|
51
|
+
(items ?? []).forEach((r) => {
|
|
52
|
+
c[r.type] += 1;
|
|
53
|
+
});
|
|
54
|
+
return c;
|
|
55
|
+
}, [items]);
|
|
56
|
+
const availableFrameworks = useMemo(
|
|
57
|
+
() => Array.from(new Set((items ?? []).flatMap((r) => r.frameworks))).sort(),
|
|
58
|
+
[items]
|
|
59
|
+
);
|
|
60
|
+
const availableTags = useMemo(
|
|
61
|
+
() => Array.from(new Set((items ?? []).flatMap((r) => r.tags))).sort(),
|
|
62
|
+
[items]
|
|
63
|
+
);
|
|
64
|
+
const visible = useMemo(
|
|
65
|
+
() => (items ?? []).filter(
|
|
66
|
+
(r) => (!typeFilter || r.type === typeFilter) && (!filters.framework || r.frameworks.includes(filters.framework)) && (filters.tags.length === 0 || filters.tags.every((t) => r.tags.includes(t))) && matchesSearch(r, filters.search)
|
|
67
|
+
),
|
|
68
|
+
[items, typeFilter, filters]
|
|
69
|
+
);
|
|
70
|
+
const totalPages = Math.ceil(visible.length / PAGE_SIZE);
|
|
71
|
+
const pageItems = visible.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
72
|
+
const handleTileToggle = (type) => {
|
|
73
|
+
setTypeFilter((current) => current === type ? void 0 : type);
|
|
74
|
+
setPage(1);
|
|
75
|
+
};
|
|
76
|
+
const handleFiltersChange = (next) => {
|
|
77
|
+
setFilters(next);
|
|
78
|
+
setPage(1);
|
|
79
|
+
};
|
|
80
|
+
const selectedResource = selectedRef ? (items ?? []).find((r) => r.entityRef === selectedRef) : void 0;
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", { className: styles.pageRoot, children: [
|
|
82
|
+
/* @__PURE__ */ jsx(
|
|
83
|
+
StatTiles,
|
|
84
|
+
{
|
|
85
|
+
counts,
|
|
86
|
+
activeType: typeFilter,
|
|
87
|
+
onToggle: handleTileToggle
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
ResourceFilters,
|
|
92
|
+
{
|
|
93
|
+
value: filters,
|
|
94
|
+
onChange: handleFiltersChange,
|
|
95
|
+
availableFrameworks,
|
|
96
|
+
availableTags
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
loading && /* @__PURE__ */ jsx("div", { className: styles.grid, children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
100
|
+
Skeleton,
|
|
101
|
+
{
|
|
102
|
+
style: { height: 170, borderRadius: "var(--bui-radius-2)" }
|
|
103
|
+
},
|
|
104
|
+
i
|
|
105
|
+
)) }),
|
|
106
|
+
error && /* @__PURE__ */ jsxs("div", { className: styles.emptyState, children: [
|
|
107
|
+
/* @__PURE__ */ jsx(Text, { variant: "title-small", color: "secondary", weight: "bold", as: "p", children: "Could not load resources" }),
|
|
108
|
+
/* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", as: "p", children: error.message })
|
|
109
|
+
] }),
|
|
110
|
+
!loading && !error && visible.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx(Box, { mb: "4", children: /* @__PURE__ */ jsxs(Text, { variant: "body-x-small", color: "secondary", children: [
|
|
112
|
+
visible.length,
|
|
113
|
+
" resource",
|
|
114
|
+
visible.length !== 1 ? "s" : "",
|
|
115
|
+
" found"
|
|
116
|
+
] }) }),
|
|
117
|
+
/* @__PURE__ */ jsx("div", { className: styles.grid, children: pageItems.map((r, i) => /* @__PURE__ */ jsx(
|
|
118
|
+
ResourceCard,
|
|
119
|
+
{
|
|
120
|
+
resource: r,
|
|
121
|
+
onView: openDetail,
|
|
122
|
+
className: playEntrance ? styles.cardEntrance : void 0,
|
|
123
|
+
style: playEntrance ? { "--card-index": i } : void 0
|
|
124
|
+
},
|
|
125
|
+
r.entityRef
|
|
126
|
+
)) }),
|
|
127
|
+
totalPages > 1 && /* @__PURE__ */ jsx(Flex, { className: styles.paginationRow, children: /* @__PURE__ */ jsx(
|
|
128
|
+
TablePagination,
|
|
129
|
+
{
|
|
130
|
+
pageSize: PAGE_SIZE,
|
|
131
|
+
offset: (page - 1) * PAGE_SIZE,
|
|
132
|
+
totalCount: visible.length,
|
|
133
|
+
hasNextPage: page < totalPages,
|
|
134
|
+
hasPreviousPage: page > 1,
|
|
135
|
+
onNextPage: () => setPage((p) => p + 1),
|
|
136
|
+
onPreviousPage: () => setPage((p) => p - 1)
|
|
137
|
+
}
|
|
138
|
+
) })
|
|
139
|
+
] }),
|
|
140
|
+
!loading && !error && visible.length === 0 && /* @__PURE__ */ jsxs("div", { className: styles.emptyState, children: [
|
|
141
|
+
/* @__PURE__ */ jsx("div", { className: styles.emptyEmoji, children: "\u{1F916}" }),
|
|
142
|
+
/* @__PURE__ */ jsx(Text, { variant: "title-small", color: "secondary", weight: "bold", as: "p", children: "No AI resources found" }),
|
|
143
|
+
/* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", as: "p", children: items && items.length > 0 ? "No resources match the current filters." : "Register AiResource entities in the catalog to see them here." })
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ jsx(ResourceDetailPanel, { resource: selectedResource, onClose: closeDetail })
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { AiResourcesPage };
|
|
150
|
+
//# sourceMappingURL=AiResourcesPage.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AiResourcesPage.esm.js","sources":["../../../src/components/AiResourcesPage/AiResourcesPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { Box, Flex, Skeleton, TablePagination, Text } from '@backstage/ui';\nimport {\n RESOURCE_TYPES,\n type ResourceSummary,\n type ResourceType,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { useResources } from '../../hooks/useResources';\nimport { StatTiles } from './StatTiles';\nimport { ResourceCard } from './ResourceCard';\nimport { ResourceFilters, type ResourceFiltersValue } from './ResourceFilters';\nimport { ResourceDetailPanel } from './ResourceDetailPanel';\nimport styles from './AiResourcesPage.module.css';\n\nconst PAGE_SIZE = 24;\n\nconst DEFAULT_FILTERS: ResourceFiltersValue = {\n search: '',\n framework: undefined,\n tags: [],\n};\n\nfunction matchesSearch(r: ResourceSummary, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n r.name.toLowerCase().includes(q) ||\n (r.title ?? '').toLowerCase().includes(q) ||\n (r.description ?? '').toLowerCase().includes(q) ||\n r.tags.some(t => t.toLowerCase().includes(q))\n );\n}\n\nexport function AiResourcesPage() {\n const { items, error, loading } = useResources();\n const [typeFilter, setTypeFilter] = useState<ResourceType | undefined>(\n undefined,\n );\n const [filters, setFilters] = useState<ResourceFiltersValue>(DEFAULT_FILTERS);\n const [page, setPage] = useState(1);\n\n // Staggered card entrance plays once, on the initial load only — re-playing\n // it on every filter/search keystroke would be noisy rather than delightful\n // (\"add some flowers\").\n const [playEntrance, setPlayEntrance] = useState(true);\n useEffect(() => {\n if (loading || !playEntrance) return undefined;\n const timer = setTimeout(() => setPlayEntrance(false), 900);\n return () => clearTimeout(timer);\n }, [loading, playEntrance]);\n\n const [searchParams, setSearchParams] = useSearchParams();\n const selectedRef = searchParams.get('resource');\n\n const openDetail = (entityRef: string) =>\n setSearchParams(p => {\n const n = new URLSearchParams(p);\n n.set('resource', entityRef);\n return n;\n });\n\n const closeDetail = () =>\n setSearchParams(p => {\n const n = new URLSearchParams(p);\n n.delete('resource');\n return n;\n });\n\n const counts = useMemo(() => {\n const c = Object.fromEntries(RESOURCE_TYPES.map(t => [t, 0])) as Record<\n ResourceType,\n number\n >;\n (items ?? []).forEach(r => {\n c[r.type] += 1;\n });\n return c;\n }, [items]);\n\n const availableFrameworks = useMemo(\n () => Array.from(new Set((items ?? []).flatMap(r => r.frameworks))).sort(),\n [items],\n );\n\n const availableTags = useMemo(\n () => Array.from(new Set((items ?? []).flatMap(r => r.tags))).sort(),\n [items],\n );\n\n const visible = useMemo(\n () =>\n (items ?? []).filter(\n r =>\n (!typeFilter || r.type === typeFilter) &&\n (!filters.framework || r.frameworks.includes(filters.framework)) &&\n (filters.tags.length === 0 ||\n filters.tags.every(t => r.tags.includes(t))) &&\n matchesSearch(r, filters.search),\n ),\n [items, typeFilter, filters],\n );\n\n const totalPages = Math.ceil(visible.length / PAGE_SIZE);\n const pageItems = visible.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);\n\n const handleTileToggle = (type: ResourceType) => {\n setTypeFilter(current => (current === type ? undefined : type));\n setPage(1);\n };\n\n const handleFiltersChange = (next: ResourceFiltersValue) => {\n setFilters(next);\n setPage(1);\n };\n\n const selectedResource = selectedRef\n ? (items ?? []).find(r => r.entityRef === selectedRef)\n : undefined;\n\n return (\n <div className={styles.pageRoot}>\n <StatTiles\n counts={counts}\n activeType={typeFilter}\n onToggle={handleTileToggle}\n />\n\n <ResourceFilters\n value={filters}\n onChange={handleFiltersChange}\n availableFrameworks={availableFrameworks}\n availableTags={availableTags}\n />\n\n {loading && (\n <div className={styles.grid}>\n {Array.from({ length: 8 }).map((_, i) => (\n <Skeleton\n key={i}\n style={{ height: 170, borderRadius: 'var(--bui-radius-2)' }}\n />\n ))}\n </div>\n )}\n\n {error && (\n <div className={styles.emptyState}>\n <Text variant=\"title-small\" color=\"secondary\" weight=\"bold\" as=\"p\">\n Could not load resources\n </Text>\n <Text variant=\"body-small\" color=\"secondary\" as=\"p\">\n {error.message}\n </Text>\n </div>\n )}\n\n {!loading && !error && visible.length > 0 && (\n <>\n <Box mb=\"4\">\n <Text variant=\"body-x-small\" color=\"secondary\">\n {visible.length} resource{visible.length !== 1 ? 's' : ''} found\n </Text>\n </Box>\n <div className={styles.grid}>\n {pageItems.map((r, i) => (\n <ResourceCard\n key={r.entityRef}\n resource={r}\n onView={openDetail}\n className={playEntrance ? styles.cardEntrance : undefined}\n style={\n playEntrance\n ? ({ '--card-index': i } as React.CSSProperties)\n : undefined\n }\n />\n ))}\n </div>\n {totalPages > 1 && (\n <Flex className={styles.paginationRow}>\n <TablePagination\n pageSize={PAGE_SIZE}\n offset={(page - 1) * PAGE_SIZE}\n totalCount={visible.length}\n hasNextPage={page < totalPages}\n hasPreviousPage={page > 1}\n onNextPage={() => setPage(p => p + 1)}\n onPreviousPage={() => setPage(p => p - 1)}\n />\n </Flex>\n )}\n </>\n )}\n\n {!loading && !error && visible.length === 0 && (\n <div className={styles.emptyState}>\n <div className={styles.emptyEmoji}>🤖</div>\n <Text variant=\"title-small\" color=\"secondary\" weight=\"bold\" as=\"p\">\n No AI resources found\n </Text>\n <Text variant=\"body-small\" color=\"secondary\" as=\"p\">\n {items && items.length > 0\n ? 'No resources match the current filters.'\n : 'Register AiResource entities in the catalog to see them here.'}\n </Text>\n </div>\n )}\n\n <ResourceDetailPanel resource={selectedResource} onClose={closeDetail} />\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 * Plugin-owned colour tokens for the six ResourceType roles (ADR-0008,\n * ADR-0010, amended 2026-08-03).\n *\n * Source of truth is the NOS *digital* brand palette — `brand` in\n * nosportugal/backstage `packages/app/src/themes/palette.ts`, the same one\n * that app's MUI themes and BUI variable overrides derive from. Hue and\n * saturation come from there and never change; only lightness moves, and only\n * far enough to clear 3:1 against the card background of the theme the value\n * is used in (`--bui-bg-neutral-1`: #ededeb in nos-light, #252830 in\n * nos-dark). Several values are the brand hex untouched, because they already\n * cleared it.\n *\n * marketplace is the exception: it keeps NOS Coral (#f26b43, from the\n * presentation brand reference) rather than taking brand.red, which that\n * palette binds to `error` — a semantic colour used for identity is the\n * misuse ADR-0008 exists to avoid.\n *\n * This is the ONLY place raw hex may appear for type identity; components\n * reference the custom properties. Chrome accents are NOT here — they use\n * BUI's own tokens, so they track the host theme instead of competing.\n */\n:root,\n[data-theme-mode='light'] {\n /* green / pink / yellow / blue / turquoise darkened onto the off-white card */\n --devaihub-type-skill: #609112;\n --devaihub-type-agent: #e042b2;\n --devaihub-type-hook: #9b8100;\n --devaihub-type-plugin: #4f60d2;\n --devaihub-type-mcp-config: #1d9381;\n --devaihub-type-marketplace: #ef4716;\n\n --devaihub-type-skill-bg: rgba(96, 145, 18, 0.14);\n --devaihub-type-agent-bg: rgba(224, 66, 178, 0.14);\n --devaihub-type-hook-bg: rgba(155, 129, 0, 0.14);\n --devaihub-type-plugin-bg: rgba(79, 96, 210, 0.14);\n --devaihub-type-mcp-config-bg: rgba(29, 147, 129, 0.14);\n --devaihub-type-marketplace-bg: rgba(239, 71, 22, 0.14);\n}\n\n[data-theme-mode='dark'] {\n /* The brand hexes as published — all but blue already clear 3:1 on #252830 */\n --devaihub-type-skill: #6ea514;\n --devaihub-type-agent: #eb84cd;\n --devaihub-type-hook: #fcd200;\n --devaihub-type-plugin: #5d6dd6;\n --devaihub-type-mcp-config: #4bdbc5;\n --devaihub-type-marketplace: #f26b43;\n\n --devaihub-type-skill-bg: rgba(110, 165, 20, 0.2);\n --devaihub-type-agent-bg: rgba(235, 132, 205, 0.2);\n --devaihub-type-hook-bg: rgba(252, 210, 0, 0.2);\n --devaihub-type-plugin-bg: rgba(93, 109, 214, 0.2);\n --devaihub-type-mcp-config-bg: rgba(75, 219, 197, 0.2);\n --devaihub-type-marketplace-bg: rgba(242, 107, 67, 0.2);\n}\n\n/*\n * Stat-tile fills. The tiles are solid blocks of the type's colour, so unlike\n * the accents above they take the NOS brand hex **as published, identically in\n * both themes** — a filled tile carries its own contrast and has no page\n * behind it to react to.\n *\n * This is deliberately not `--devaihub-type-*`. Those values are tuned for\n * legibility *against* a card surface, which in light mode means darkened —\n * filling a tile with the light-mode accent produces the olive/dull row this\n * palette exists to avoid. Same hues, different job, so different tokens.\n *\n * White on these measures 5.34:1 (blue) down to 1.46:1 (yellow) and 1.72:1\n * (turquoise): three of six are below WCAG AA. That is a known trade, made\n * with the alternatives rendered — see ADR-0008. It is contained to this row;\n * every type accent on the cards is AA in both themes.\n */\n:root {\n --devaihub-tile-fg: #ffffff;\n\n --devaihub-tile-skill: #6ea514;\n --devaihub-tile-agent: #eb84cd;\n --devaihub-tile-hook: #fcd200;\n --devaihub-tile-plugin: #4f60d2;\n --devaihub-tile-mcp-config: #4bdbc5;\n --devaihub-tile-marketplace: #f26b43;\n}\n\n@layer components {\n .AiResourcesPage_pageRoot__cbc474d241 {\n padding: var(--bui-space-6);\n }\n\n .AiResourcesPage_grid__cbc474d241 {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: var(--bui-space-4);\n }\n\n .AiResourcesPage_paginationRow__cbc474d241 {\n justify-content: center;\n margin-top: var(--bui-space-5);\n }\n\n /* Staggered entrance for the initial card grid render only (see\n AiResourcesPage.tsx's playEntrance) — --card-index is set per-card. */\n .AiResourcesPage_cardEntrance__cbc474d241 {\n animation: AiResourcesPage_card-entrance__cbc474d241 320ms cubic-bezier(0.16, 1, 0.3, 1) both;\n animation-delay: calc(var(--card-index, 0) * 40ms);\n }\n\n @keyframes AiResourcesPage_card-entrance__cbc474d241 {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .AiResourcesPage_cardEntrance__cbc474d241 {\n animation: none;\n }\n }\n\n .AiResourcesPage_emptyState__cbc474d241 {\n text-align: center;\n padding: var(--bui-space-9) 0;\n }\n\n .AiResourcesPage_emptyEmoji__cbc474d241 {\n font-size: 40px;\n margin-bottom: var(--bui-space-2);\n animation: AiResourcesPage_empty-emoji-bounce__cbc474d241 700ms cubic-bezier(0.34, 1.56, 0.64, 1);\n }\n\n @keyframes AiResourcesPage_empty-emoji-bounce__cbc474d241 {\n 0% {\n opacity: 0;\n transform: scale(0.4) translateY(-6px);\n }\n 60% {\n opacity: 1;\n transform: scale(1.08) translateY(0);\n }\n 100% {\n transform: scale(1) translateY(0);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .AiResourcesPage_emptyEmoji__cbc474d241 {\n animation: none;\n }\n }\n}\n";
|
|
4
|
+
var styles = {"pageRoot":"AiResourcesPage_pageRoot__cbc474d241","grid":"AiResourcesPage_grid__cbc474d241","paginationRow":"AiResourcesPage_paginationRow__cbc474d241","cardEntrance":"AiResourcesPage_cardEntrance__cbc474d241","emptyState":"AiResourcesPage_emptyState__cbc474d241","emptyEmoji":"AiResourcesPage_emptyEmoji__cbc474d241"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { styles as default };
|
|
8
|
+
//# sourceMappingURL=AiResourcesPage.module.css.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AiResourcesPage.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { Text } from '@backstage/ui';
|
|
3
|
+
import { RiEyeLine, RiDownloadLine } from '@remixicon/react';
|
|
4
|
+
import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
|
|
5
|
+
import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
|
|
6
|
+
import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
|
|
7
|
+
import styles from './ResourceCard.module.css.esm.js';
|
|
8
|
+
|
|
9
|
+
const MAX_VISIBLE_TAGS = 3;
|
|
10
|
+
const POPULAR_THRESHOLD = 5;
|
|
11
|
+
function ResourceCard({
|
|
12
|
+
resource,
|
|
13
|
+
onView,
|
|
14
|
+
className,
|
|
15
|
+
style
|
|
16
|
+
}) {
|
|
17
|
+
const meta = getTypeMeta(resource.type);
|
|
18
|
+
const counts = useTelemetryCounts(resource.entityRef);
|
|
19
|
+
const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);
|
|
20
|
+
const hiddenTagCount = resource.tags.length - visibleTags.length;
|
|
21
|
+
const ownerLabel = resource.owner?.replace(/^(group|user):(default\/)?/, "");
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"div",
|
|
24
|
+
{
|
|
25
|
+
className: className ? `${styles.card} ${className}` : styles.card,
|
|
26
|
+
style: {
|
|
27
|
+
"--card-accent": meta.color,
|
|
28
|
+
"--card-accent-bg": meta.colorBg,
|
|
29
|
+
...style
|
|
30
|
+
},
|
|
31
|
+
onClick: () => onView(resource.entityRef),
|
|
32
|
+
onKeyDown: (e) => {
|
|
33
|
+
if (e.key === "Enter") onView(resource.entityRef);
|
|
34
|
+
},
|
|
35
|
+
role: "button",
|
|
36
|
+
tabIndex: 0,
|
|
37
|
+
"aria-label": `View ${resource.title ?? resource.name}`,
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: styles.header, children: [
|
|
40
|
+
/* @__PURE__ */ jsx("div", { className: styles.iconBox, children: /* @__PURE__ */ jsx(meta.Icon, { size: 18 }) }),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: styles.headerText, children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Text, { variant: "body-x-small", className: styles.typeLabel, children: meta.label }),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
Text,
|
|
45
|
+
{
|
|
46
|
+
variant: "title-x-small",
|
|
47
|
+
weight: "bold",
|
|
48
|
+
as: "h3",
|
|
49
|
+
className: styles.title,
|
|
50
|
+
children: resource.title ?? resource.name
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
] })
|
|
54
|
+
] }),
|
|
55
|
+
/* @__PURE__ */ jsxs("div", { className: styles.body, children: [
|
|
56
|
+
resource.frameworks.length > 0 && /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.frameworks.map((f) => /* @__PURE__ */ jsx(
|
|
57
|
+
"span",
|
|
58
|
+
{
|
|
59
|
+
className: styles.frameworkBadge,
|
|
60
|
+
title: frameworkLabel(f),
|
|
61
|
+
children: /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 16 })
|
|
62
|
+
},
|
|
63
|
+
f
|
|
64
|
+
)) }),
|
|
65
|
+
resource.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.tags, children: [
|
|
66
|
+
visibleTags.map((t) => /* @__PURE__ */ jsxs("span", { className: styles.tag, children: [
|
|
67
|
+
"#",
|
|
68
|
+
t
|
|
69
|
+
] }, t)),
|
|
70
|
+
hiddenTagCount > 0 && /* @__PURE__ */ jsxs(
|
|
71
|
+
"span",
|
|
72
|
+
{
|
|
73
|
+
className: styles.tag,
|
|
74
|
+
title: resource.tags.slice(MAX_VISIBLE_TAGS).join(", "),
|
|
75
|
+
children: [
|
|
76
|
+
"+",
|
|
77
|
+
hiddenTagCount
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] }),
|
|
82
|
+
(resource.version || ownerLabel || !!counts?.install || !!counts?.view) && /* @__PURE__ */ jsxs("div", { className: styles.footer, children: [
|
|
83
|
+
/* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", children: [
|
|
84
|
+
resource.version && `v${resource.version.replace(/^v/, "")}`,
|
|
85
|
+
ownerLabel
|
|
86
|
+
].filter(Boolean).join(" \xB7 ") }),
|
|
87
|
+
/* @__PURE__ */ jsxs("span", { className: styles.countsRow, children: [
|
|
88
|
+
!!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.count, title: `${counts.view} views`, children: [
|
|
89
|
+
/* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
|
|
90
|
+
counts.view
|
|
91
|
+
] }),
|
|
92
|
+
!!counts?.install && /* @__PURE__ */ jsxs(
|
|
93
|
+
"span",
|
|
94
|
+
{
|
|
95
|
+
className: counts.install >= POPULAR_THRESHOLD ? styles.countPopular : styles.count,
|
|
96
|
+
title: `${counts.install} installs`,
|
|
97
|
+
children: [
|
|
98
|
+
counts.install >= POPULAR_THRESHOLD ? "\u{1F525}" : /* @__PURE__ */ jsx(RiDownloadLine, { size: 14 }),
|
|
99
|
+
counts.install
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
] })
|
|
104
|
+
] })
|
|
105
|
+
] })
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { ResourceCard };
|
|
112
|
+
//# sourceMappingURL=ResourceCard.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport { RiDownloadLine, RiEyeLine } from '@remixicon/react';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceCard.module.css';\n\ninterface ResourceCardProps {\n resource: ResourceSummary;\n onView: (entityRef: string) => void;\n /** Extra class (e.g. the initial-load entrance animation) merged onto the\n * card's own root — kept off a wrapper div so this stays the grid's direct\n * child and CSS Grid's row-height stretch (the footer's margin-top: auto\n * relies on it for equal card heights) keeps working. */\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * The per-type card: type identity (colour/icon/label from the registry),\n * title, framework badges, tags, and a footer of version · owner plus the\n * view and install counts.\n *\n * Deliberately no description — clamped to two lines at card width it was\n * unreadable, and the drawer shows it in full the moment a card is opened.\n * Deliberately no \"View source\" link either: it competed with the card's own\n * click target and lives in the drawer, which is where someone deciding\n * whether to install is already looking.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst POPULAR_THRESHOLD = 5;\n\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 className={styles.count} title={`${counts.view} views`}>\n <RiEyeLine size={14} />\n {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={\n counts.install >= POPULAR_THRESHOLD\n ? styles.countPopular\n : styles.count\n }\n title={`${counts.install} installs`}\n >\n {counts.install >= POPULAR_THRESHOLD ? (\n '\\u{1F525}'\n ) : (\n <RiDownloadLine size={14} />\n )}\n {counts.install}\n </span>\n )}\n </span>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -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 /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .ResourceCard_card__82e843b6a3 {\n background: var(--bui-bg-neutral-1);\n border: 1px solid var(--bui-border-1);\n /* The type's colour, kept as the card's identity at a glance in the grid.\n 2px rather than 3: on a 16px radius a thick stripe fights the curve. */\n border-left: 2px solid var(--card-accent);\n border-radius: var(--bui-radius-5);\n overflow: hidden;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n padding: var(--bui-space-4);\n /* Room for the tallest realistic card (2-line title, badges, tags,\n footer) so the grid reads as a grid. The footer's margin-top: auto\n absorbs the slack on lighter cards. */\n min-height: 176px;\n transition: box-shadow 160ms ease,\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1), border-color 160ms ease;\n }\n\n .ResourceCard_card__82e843b6a3:hover {\n border-color: color-mix(\n in srgb,\n var(--card-accent) 40%,\n var(--bui-border-1)\n );\n border-left-color: var(--card-accent);\n /* Two-part shadow: a soft ambient one and a tighter contact one. A single\n large blur reads as a glow rather than as elevation. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),\n 0 12px 28px -12px color-mix(in srgb, var(--card-accent) 40%, transparent);\n transform: translateY(-2px);\n }\n\n .ResourceCard_card__82e843b6a3:focus-visible {\n outline: 2px solid var(--bui-ring, var(--card-accent));\n outline-offset: 2px;\n }\n\n /* The header is a fixed-height zone with its contents centred, rather than\n a top-aligned block with a reserved second title line. Same outcome for\n alignment across the grid — everything below starts at one height — but a\n one-line title now sits centred in that zone instead of leaving an\n orphaned gap beneath it, which is what made short-titled cards read as\n detached from their own content. */\n .ResourceCard_header__82e843b6a3 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n flex-shrink: 0;\n min-height: 3.5rem;\n }\n\n .ResourceCard_iconBox__82e843b6a3 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-4);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceCard_headerText__82e843b6a3 {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n }\n\n .ResourceCard_title__82e843b6a3 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n /* Tighter leading than body copy: a two-line title should read as one\n object rather than as two stacked lines. */\n line-height: 1.25;\n }\n\n .ResourceCard_typeLabel__82e843b6a3 {\n display: block;\n color: var(--bui-fg-secondary);\n font-weight: 600;\n font-size: 0.6875rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n margin-bottom: 2px;\n }\n\n .ResourceCard_body__82e843b6a3 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n flex: 1;\n }\n\n .ResourceCard_badges__82e843b6a3 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Holds a single tool icon, so the padding is square and the badge reads as\n a circle rather than a pill. */\n .ResourceCard_frameworkBadge__82e843b6a3 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-full, 999px);\n padding: var(--bui-space-1);\n font-size: 0.8125rem;\n line-height: 1;\n color: var(--bui-fg-primary);\n }\n\n .ResourceCard_tags__82e843b6a3 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n /* Quiet chips rather than bare \"#text\": a chip reads as a discrete value,\n which is what a tag is, and the surface keeps them from competing with the\n description. Not interactive, so they stay low-contrast. */\n .ResourceCard_tag__82e843b6a3 {\n font-size: 0.6875rem;\n line-height: 1.6;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n\n /* No rule above the footer — the gap does the separating, and the meta is\n already de-emphasised enough to read as a different tier. */\n .ResourceCard_footer__82e843b6a3 {\n margin-top: auto;\n padding-top: var(--bui-space-1);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-2);\n font-size: 0.6875rem;\n }\n\n .ResourceCard_countsRow__82e843b6a3 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n flex-shrink: 0;\n }\n\n /* Chips, not footnotes: views and installs are the signal a browser scans\n for, so they carry a surface and the full foreground colour while the\n version/owner meta stays secondary beside them. */\n .ResourceCard_count__82e843b6a3 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n padding: 2px var(--bui-space-2);\n border-radius: var(--bui-radius-full, 999px);\n background: var(--bui-bg-neutral-2);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--bui-fg-primary);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n /* The glyph takes the card's accent while the number stays foreground: a\n fully monochrome chip reads as detached on a card that is otherwise\n colour-coded, and colouring the number too would cost its legibility. */\n .ResourceCard_count__82e843b6a3 svg {\n color: var(--card-accent);\n flex-shrink: 0;\n }\n\n /* Past the popularity threshold the install chip takes the type's own\n colour — the one place on the card where the accent marks a value rather\n than an identity. */\n .ResourceCard_countPopular__82e843b6a3 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n }\n}\n";
|
|
4
|
+
var styles = {"card":"ResourceCard_card__82e843b6a3","header":"ResourceCard_header__82e843b6a3","iconBox":"ResourceCard_iconBox__82e843b6a3","headerText":"ResourceCard_headerText__82e843b6a3","title":"ResourceCard_title__82e843b6a3","typeLabel":"ResourceCard_typeLabel__82e843b6a3","body":"ResourceCard_body__82e843b6a3","badges":"ResourceCard_badges__82e843b6a3","frameworkBadge":"ResourceCard_frameworkBadge__82e843b6a3","tags":"ResourceCard_tags__82e843b6a3","tag":"ResourceCard_tag__82e843b6a3","footer":"ResourceCard_footer__82e843b6a3","countsRow":"ResourceCard_countsRow__82e843b6a3","count":"ResourceCard_count__82e843b6a3","countPopular":"ResourceCard_countPopular__82e843b6a3 ResourceCard_count__82e843b6a3"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { styles as default };
|
|
8
|
+
//# sourceMappingURL=ResourceCard.module.css.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceCard.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|