@nospt/plugin-dev-ai-hub 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/api/DevAiHubResourceClient.esm.js +97 -0
  2. package/dist/api/DevAiHubResourceClient.esm.js.map +1 -0
  3. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +150 -0
  4. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -0
  5. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +8 -0
  6. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js.map +1 -0
  7. package/dist/components/AiResourcesPage/ResourceCard.esm.js +158 -0
  8. package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -0
  9. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +8 -0
  10. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js.map +1 -0
  11. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +234 -0
  12. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -0
  13. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +8 -0
  14. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js.map +1 -0
  15. package/dist/components/AiResourcesPage/ResourceFilters.esm.js +242 -0
  16. package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -0
  17. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js +8 -0
  18. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js.map +1 -0
  19. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +366 -0
  20. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -0
  21. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js +8 -0
  22. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js.map +1 -0
  23. package/dist/components/AiResourcesPage/StatTiles.esm.js +49 -0
  24. package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -0
  25. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +8 -0
  26. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js.map +1 -0
  27. package/dist/components/AiResourcesPage/index.esm.js +6 -0
  28. package/dist/components/AiResourcesPage/typeMeta.esm.js +35 -0
  29. package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -0
  30. package/dist/components/ToolIcon/ToolIcon.esm.js +26 -9
  31. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  32. package/dist/hooks/useResourceBody.esm.js +39 -0
  33. package/dist/hooks/useResourceBody.esm.js.map +1 -0
  34. package/dist/hooks/useResources.esm.js +26 -0
  35. package/dist/hooks/useResources.esm.js.map +1 -0
  36. package/dist/hooks/useTelemetryCounts.esm.js +24 -0
  37. package/dist/hooks/useTelemetryCounts.esm.js.map +1 -0
  38. package/dist/index.d.ts +1 -38
  39. package/dist/index.esm.js +1 -3
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/plugin.esm.js +15 -24
  42. package/dist/plugin.esm.js.map +1 -1
  43. package/dist/routes.esm.js +1 -2
  44. package/dist/routes.esm.js.map +1 -1
  45. package/package.json +17 -10
  46. package/.eslintrc.js +0 -1
  47. package/dev/index.tsx +0 -32
  48. package/dist/api/DevAiHubClient.esm.js +0 -78
  49. package/dist/api/DevAiHubClient.esm.js.map +0 -1
  50. package/dist/components/AssetCard/AssetCard.esm.js +0 -120
  51. package/dist/components/AssetCard/AssetCard.esm.js.map +0 -1
  52. package/dist/components/AssetCard/AssetCard.module.css.esm.js +0 -8
  53. package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +0 -1
  54. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +0 -218
  55. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +0 -1
  56. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +0 -8
  57. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +0 -1
  58. package/dist/components/AssetFilters/AssetFilters.esm.js +0 -176
  59. package/dist/components/AssetFilters/AssetFilters.esm.js.map +0 -1
  60. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +0 -8
  61. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +0 -1
  62. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +0 -138
  63. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +0 -1
  64. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +0 -8
  65. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +0 -1
  66. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +0 -195
  67. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +0 -1
  68. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +0 -8
  69. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +0 -1
  70. package/dist/components/DevAiHubPage/index.esm.js +0 -6
  71. package/dist/components/McpConfigPage/McpConfigPage.esm.js +0 -200
  72. package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +0 -1
  73. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +0 -8
  74. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +0 -1
  75. package/dist/components/McpConfigPage/index.esm.js +0 -6
  76. package/dist/components/McpConfigPage/index.esm.js.map +0 -1
  77. package/dist/hooks/index.esm.js +0 -114
  78. package/dist/hooks/index.esm.js.map +0 -1
  79. package/dist/pluginLegacy.esm.js +0 -27
  80. package/dist/pluginLegacy.esm.js.map +0 -1
  81. package/package.json-prepack +0 -85
  82. package/src/api/DevAiHubClient.ts +0 -110
  83. package/src/components/AssetCard/AssetCard.module.css +0 -153
  84. package/src/components/AssetCard/AssetCard.test.tsx +0 -182
  85. package/src/components/AssetCard/AssetCard.tsx +0 -152
  86. package/src/components/AssetCard/index.ts +0 -1
  87. package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +0 -207
  88. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +0 -313
  89. package/src/components/AssetDetailPanel/index.ts +0 -1
  90. package/src/components/AssetFilters/AssetFilters.module.css +0 -67
  91. package/src/components/AssetFilters/AssetFilters.tsx +0 -219
  92. package/src/components/AssetFilters/index.ts +0 -2
  93. package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +0 -72
  94. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +0 -176
  95. package/src/components/AssetInstallDialog/index.ts +0 -1
  96. package/src/components/DevAiHubPage/DevAiHubPage.module.css +0 -105
  97. package/src/components/DevAiHubPage/DevAiHubPage.tsx +0 -197
  98. package/src/components/DevAiHubPage/index.ts +0 -1
  99. package/src/components/McpConfigDialog/McpConfigDialog.module.css +0 -111
  100. package/src/components/McpConfigDialog/McpConfigDialog.tsx +0 -271
  101. package/src/components/McpConfigDialog/index.ts +0 -1
  102. package/src/components/McpConfigPage/McpConfigPage.module.css +0 -121
  103. package/src/components/McpConfigPage/McpConfigPage.tsx +0 -242
  104. package/src/components/McpConfigPage/index.ts +0 -1
  105. package/src/components/ToolIcon/ToolIcon.tsx +0 -65
  106. package/src/components/ToolIcon/index.ts +0 -1
  107. package/src/css-modules.d.ts +0 -4
  108. package/src/hooks/index.ts +0 -152
  109. package/src/index.ts +0 -10
  110. package/src/plugin.tsx +0 -60
  111. package/src/pluginLegacy.ts +0 -42
  112. package/src/routes.ts +0 -4
  113. package/tests/e2e/.auth.json +0 -14
  114. package/tests/e2e/asset-detail.spec.ts +0 -90
  115. package/tests/e2e/asset-filtering.spec.ts +0 -145
  116. package/tests/e2e/asset-installation.spec.ts +0 -74
  117. package/tests/e2e/dev-ai-hub-page.spec.ts +0 -89
  118. package/tests/e2e/fixtures/base.ts +0 -61
  119. package/tests/e2e/fixtures/mock-api.ts +0 -221
  120. package/tests/e2e/mcp-config.spec.ts +0 -88
  121. package/tsconfig.json +0 -10
  122. /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 counts for a resource (ADR-0007 — dedup for `view` is a follow-up). */\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\"), and cards already remount when they re-enter the\n // filtered set (a separate, pre-existing telemetry consideration).\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). NOS company palette — do not substitute other colours (see\n * docs/adr/0008; marketplace coral is NOS Coral from the brand reference).\n * The ONLY place raw hex may appear for type identity; components reference\n * the custom properties. Dark values keep the hue and tune lightness only.\n */\n:root,\n[data-theme-mode='light'] {\n --devaihub-type-skill: #6ab04c;\n --devaihub-type-agent: #ff6b9d;\n --devaihub-type-hook: #f9ca24;\n --devaihub-type-plugin: #54a0ff;\n --devaihub-type-mcp-config: #00d2d3;\n --devaihub-type-marketplace: #f26b43;\n\n --devaihub-type-skill-bg: rgba(106, 176, 76, 0.14);\n --devaihub-type-agent-bg: rgba(255, 107, 157, 0.14);\n --devaihub-type-hook-bg: rgba(249, 202, 36, 0.16);\n --devaihub-type-plugin-bg: rgba(84, 160, 255, 0.14);\n --devaihub-type-mcp-config-bg: rgba(0, 210, 211, 0.14);\n --devaihub-type-marketplace-bg: rgba(242, 107, 67, 0.14);\n}\n\n/*\n * Stat-tile gradient pairs (start → deep), identical in both themes — the\n * legacy boxes the tiles reproduce. Text on tiles is always white.\n */\n:root {\n --devaihub-tile-skill: #6ab04c;\n --devaihub-tile-skill-deep: #4a8f2e;\n --devaihub-tile-agent: #ff6b9d;\n --devaihub-tile-agent-deep: #e0507a;\n --devaihub-tile-hook: #f9ca24;\n --devaihub-tile-hook-deep: #d4a800;\n --devaihub-tile-plugin: #54a0ff;\n --devaihub-tile-plugin-deep: #2980ff;\n --devaihub-tile-mcp-config: #00d2d3;\n --devaihub-tile-mcp-config-deep: #00a8a9;\n --devaihub-tile-marketplace: #f26b43;\n --devaihub-tile-marketplace-deep: #dc4618;\n}\n\n[data-theme-mode='dark'] {\n --devaihub-type-skill: #83c368;\n --devaihub-type-agent: #ff85ae;\n --devaihub-type-hook: #fbd54f;\n --devaihub-type-plugin: #74b1ff;\n --devaihub-type-mcp-config: #2fdedf;\n --devaihub-type-marketplace: #f58f70;\n\n --devaihub-type-skill-bg: rgba(131, 195, 104, 0.18);\n --devaihub-type-agent-bg: rgba(255, 133, 174, 0.18);\n --devaihub-type-hook-bg: rgba(251, 213, 79, 0.18);\n --devaihub-type-plugin-bg: rgba(116, 177, 255, 0.18);\n --devaihub-type-mcp-config-bg: rgba(47, 222, 223, 0.18);\n --devaihub-type-marketplace-bg: rgba(245, 143, 112, 0.18);\n}\n\n/*\n * NOS corporate brand accent — the real NOS Teal, kept separate from the\n * per-type palette above (that palette is functional/informational, not\n * a brand statement; ADR-0008 still owns type identity). Used sparingly\n * as a \"touch of NOS\" on neutral plugin chrome (e.g. the detail drawer).\n * Source: NOS presentation brand reference\n * (.github/skills/nos-presenting-colors/references/branding.md).\n */\n:root,\n[data-theme-mode='light'] {\n --devaihub-brand-teal: #00b3ad;\n}\n\n[data-theme-mode='dark'] {\n /* Light Teal — the palette's own \"highlights\" shade, for dark backgrounds */\n --devaihub-brand-teal: #00e6e1;\n}\n\n@layer components {\n .AiResourcesPage_pageRoot__cf19202f36 {\n padding: var(--bui-space-6);\n }\n\n .AiResourcesPage_grid__cf19202f36 {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: var(--bui-space-4);\n }\n\n .AiResourcesPage_paginationRow__cf19202f36 {\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__cf19202f36 {\n animation: AiResourcesPage_card-entrance__cf19202f36 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__cf19202f36 {\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__cf19202f36 {\n animation: none;\n }\n }\n\n .AiResourcesPage_emptyState__cf19202f36 {\n text-align: center;\n padding: var(--bui-space-9) 0;\n }\n\n .AiResourcesPage_emptyEmoji__cf19202f36 {\n font-size: 40px;\n margin-bottom: var(--bui-space-2);\n animation: AiResourcesPage_empty-emoji-bounce__cf19202f36 700ms cubic-bezier(0.34, 1.56, 0.64, 1);\n }\n\n @keyframes AiResourcesPage_empty-emoji-bounce__cf19202f36 {\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__cf19202f36 {\n animation: none;\n }\n }\n}\n";
4
+ var styles = {"pageRoot":"AiResourcesPage_pageRoot__cf19202f36","grid":"AiResourcesPage_grid__cf19202f36","paginationRow":"AiResourcesPage_paginationRow__cf19202f36","cardEntrance":"AiResourcesPage_cardEntrance__cf19202f36","emptyState":"AiResourcesPage_emptyState__cf19202f36","emptyEmoji":"AiResourcesPage_emptyEmoji__cf19202f36"};
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,158 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useEffect } from 'react';
3
+ import { useApi } from '@backstage/core-plugin-api';
4
+ import { Text } from '@backstage/ui';
5
+ import { RiExternalLinkLine, RiEyeLine } from '@remixicon/react';
6
+ import { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient.esm.js';
7
+ import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
8
+ import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
9
+ import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
10
+ import styles from './ResourceCard.module.css.esm.js';
11
+
12
+ const MAX_VISIBLE_TAGS = 3;
13
+ const MAX_VISIBLE_FRAMEWORKS = 2;
14
+ const POPULAR_THRESHOLD = 5;
15
+ function ResourceCard({
16
+ resource,
17
+ onView,
18
+ className,
19
+ style
20
+ }) {
21
+ const meta = getTypeMeta(resource.type);
22
+ const api = useApi(devAiHubResourceApiRef);
23
+ const counts = useTelemetryCounts(resource.entityRef);
24
+ useEffect(() => {
25
+ api.track(resource.entityRef, "view");
26
+ }, [resource.entityRef]);
27
+ const handleSourceClick = (e) => e.stopPropagation();
28
+ const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);
29
+ const hiddenTagCount = resource.tags.length - visibleTags.length;
30
+ const visibleFrameworks = resource.frameworks.slice(
31
+ 0,
32
+ MAX_VISIBLE_FRAMEWORKS
33
+ );
34
+ const hiddenFrameworkCount = resource.frameworks.length - visibleFrameworks.length;
35
+ const ownerLabel = resource.owner?.replace(/^(group|user):(default\/)?/, "");
36
+ return /* @__PURE__ */ jsxs(
37
+ "div",
38
+ {
39
+ className: className ? `${styles.card} ${className}` : styles.card,
40
+ style: {
41
+ "--card-accent": meta.color,
42
+ "--card-accent-bg": meta.colorBg,
43
+ ...style
44
+ },
45
+ onClick: () => onView(resource.entityRef),
46
+ onKeyDown: (e) => {
47
+ if (e.key === "Enter") onView(resource.entityRef);
48
+ },
49
+ role: "button",
50
+ tabIndex: 0,
51
+ "aria-label": `View ${resource.title ?? resource.name}`,
52
+ children: [
53
+ /* @__PURE__ */ jsxs("div", { className: styles.header, children: [
54
+ /* @__PURE__ */ jsx("div", { className: styles.iconBox, children: /* @__PURE__ */ jsx(meta.Icon, { size: 18 }) }),
55
+ /* @__PURE__ */ jsxs("div", { className: styles.headerText, children: [
56
+ /* @__PURE__ */ jsx(
57
+ Text,
58
+ {
59
+ variant: "title-x-small",
60
+ weight: "bold",
61
+ as: "h3",
62
+ className: styles.title,
63
+ children: resource.title ?? resource.name
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", className: styles.typeLabel, children: meta.label })
67
+ ] }),
68
+ resource.sourceLocation && /* @__PURE__ */ jsx(
69
+ "a",
70
+ {
71
+ className: styles.sourceLink,
72
+ href: resource.sourceLocation.replace(/^url:/, ""),
73
+ target: "_blank",
74
+ rel: "noopener noreferrer",
75
+ onClick: handleSourceClick,
76
+ "aria-label": "View source",
77
+ title: "View source",
78
+ children: /* @__PURE__ */ jsx(RiExternalLinkLine, { size: 16 })
79
+ }
80
+ )
81
+ ] }),
82
+ /* @__PURE__ */ jsxs("div", { className: styles.body, children: [
83
+ resource.description && /* @__PURE__ */ jsx(
84
+ Text,
85
+ {
86
+ variant: "body-small",
87
+ color: "secondary",
88
+ className: styles.description,
89
+ children: resource.description
90
+ }
91
+ ),
92
+ resource.frameworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.badges, children: [
93
+ visibleFrameworks.map((f) => /* @__PURE__ */ jsxs("span", { className: styles.frameworkBadge, children: [
94
+ /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 14 }),
95
+ frameworkLabel(f)
96
+ ] }, f)),
97
+ hiddenFrameworkCount > 0 && /* @__PURE__ */ jsxs(
98
+ "span",
99
+ {
100
+ className: styles.frameworkBadge,
101
+ title: resource.frameworks.slice(MAX_VISIBLE_FRAMEWORKS).map(frameworkLabel).join(", "),
102
+ children: [
103
+ "+",
104
+ hiddenFrameworkCount
105
+ ]
106
+ }
107
+ )
108
+ ] }),
109
+ resource.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.tags, children: [
110
+ visibleTags.map((t) => /* @__PURE__ */ jsxs("span", { className: styles.tag, children: [
111
+ "#",
112
+ t
113
+ ] }, t)),
114
+ hiddenTagCount > 0 && /* @__PURE__ */ jsxs(
115
+ "span",
116
+ {
117
+ className: styles.tag,
118
+ title: resource.tags.slice(MAX_VISIBLE_TAGS).join(", "),
119
+ children: [
120
+ "+",
121
+ hiddenTagCount
122
+ ]
123
+ }
124
+ )
125
+ ] }),
126
+ (resource.version || ownerLabel || !!counts?.install || !!counts?.view) && /* @__PURE__ */ jsxs("div", { className: styles.footer, children: [
127
+ /* @__PURE__ */ jsx(Text, { variant: "body-x-small", color: "secondary", children: [
128
+ resource.version && `v${resource.version.replace(/^v/, "")}`,
129
+ ownerLabel
130
+ ].filter(Boolean).join(" \xB7 ") }),
131
+ /* @__PURE__ */ jsxs("span", { className: styles.countsRow, children: [
132
+ !!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.count, title: `${counts.view} views`, children: [
133
+ /* @__PURE__ */ jsx(RiEyeLine, { size: 13 }),
134
+ " ",
135
+ counts.view
136
+ ] }),
137
+ !!counts?.install && /* @__PURE__ */ jsxs(
138
+ "span",
139
+ {
140
+ className: styles.count,
141
+ title: `${counts.install} installs`,
142
+ children: [
143
+ counts.install >= POPULAR_THRESHOLD ? "\u{1F525}" : "\u2193",
144
+ " ",
145
+ counts.install
146
+ ]
147
+ }
148
+ )
149
+ ] })
150
+ ] })
151
+ ] })
152
+ ]
153
+ }
154
+ );
155
+ }
156
+
157
+ export { ResourceCard };
158
+ //# sourceMappingURL=ResourceCard.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceCard.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceCard.tsx"],"sourcesContent":["import { useEffect, type MouseEvent } from 'react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport { Text } from '@backstage/ui';\nimport { RiExternalLinkLine, RiEyeLine } from '@remixicon/react';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\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. Mirrors the legacy card layout: type identity\n * (colour/icon/label from the registry), title, description, framework\n * badges, tags, version · owner footer, View source link.\n */\nconst MAX_VISIBLE_TAGS = 3;\nconst MAX_VISIBLE_FRAMEWORKS = 2;\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 api = useApi(devAiHubResourceApiRef);\n const counts = useTelemetryCounts(resource.entityRef);\n\n useEffect(() => {\n api.track(resource.entityRef, 'view');\n // Fires once per card mount, not per re-render (ADR-0007 anti-inflation intent).\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [resource.entityRef]);\n\n const handleSourceClick = (e: MouseEvent) => e.stopPropagation();\n\n const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);\n const hiddenTagCount = resource.tags.length - visibleTags.length;\n const visibleFrameworks = resource.frameworks.slice(\n 0,\n MAX_VISIBLE_FRAMEWORKS,\n );\n const hiddenFrameworkCount =\n resource.frameworks.length - visibleFrameworks.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 <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 <Text variant=\"body-x-small\" className={styles.typeLabel}>\n {meta.label}\n </Text>\n </div>\n {resource.sourceLocation && (\n <a\n className={styles.sourceLink}\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={handleSourceClick}\n aria-label=\"View source\"\n title=\"View source\"\n >\n <RiExternalLinkLine size={16} />\n </a>\n )}\n </div>\n\n <div className={styles.body}>\n {resource.description && (\n <Text\n variant=\"body-small\"\n color=\"secondary\"\n className={styles.description}\n >\n {resource.description}\n </Text>\n )}\n\n {resource.frameworks.length > 0 && (\n <div className={styles.badges}>\n {visibleFrameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n {hiddenFrameworkCount > 0 && (\n <span\n className={styles.frameworkBadge}\n title={resource.frameworks\n .slice(MAX_VISIBLE_FRAMEWORKS)\n .map(frameworkLabel)\n .join(', ')}\n >\n +{hiddenFrameworkCount}\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 <span className={styles.countsRow}>\n {!!counts?.view && (\n <span className={styles.count} title={`${counts.view} views`}>\n <RiEyeLine size={13} /> {counts.view}\n </span>\n )}\n {!!counts?.install && (\n <span\n className={styles.count}\n title={`${counts.install} installs`}\n >\n {counts.install >= POPULAR_THRESHOLD ? '🔥' : '↓'}{' '}\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 .ResourceCard_card__7e1619a1f6 {\n background: var(--bui-bg-neutral-1);\n border: 1px solid var(--bui-border-1);\n border-left: 3px solid var(--card-accent);\n border-radius: var(--bui-radius-2);\n /* Clips .header's own background to the card's rounded top corners. */\n overflow: hidden;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n /* Every card reserves room for the tallest realistic combination (2-line\n title, 2-line description, a badges row, a tags row, and the footer)\n so cards read as a uniform grid instead of a ragged one. The footer's\n margin-top: auto (below) absorbs any slack on lighter cards. */\n min-height: 250px;\n transition: box-shadow 120ms ease, transform 120ms ease,\n border-color 120ms ease;\n }\n\n .ResourceCard_card__7e1619a1f6: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 box-shadow: 0 4px 16px\n color-mix(in srgb, var(--card-accent) 15%, transparent);\n transform: translateY(-1px);\n }\n\n .ResourceCard_header__7e1619a1f6 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n padding: var(--bui-space-3) var(--bui-space-4);\n /* BUI's neutral-2..4 tiers render identically in this theme, so blend\n the (theme-aware) foreground colour into the card bg instead — this\n guarantees a visibly darker band in both light and dark mode. */\n background: color-mix(\n in srgb,\n var(--bui-fg-primary) 10%,\n var(--bui-bg-neutral-1)\n );\n border-bottom: 1px solid var(--bui-border-1);\n flex-shrink: 0;\n /* Fixed so every header band is the same height regardless of whether\n the title wraps to one line or two — shorter titles just centre\n (align-items: center, above) within the reserved space. */\n height: 84px;\n }\n\n .ResourceCard_body__7e1619a1f6 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-3);\n padding: var(--bui-space-4);\n flex: 1;\n }\n\n .ResourceCard_iconBox__7e1619a1f6 {\n background: var(--card-accent-bg);\n color: var(--card-accent);\n border-radius: var(--bui-radius-2);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .ResourceCard_headerText__7e1619a1f6 {\n flex: 1;\n min-width: 0;\n }\n\n .ResourceCard_title__7e1619a1f6 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n line-height: 1.3;\n }\n\n .ResourceCard_typeLabel__7e1619a1f6 {\n color: var(--card-accent);\n font-weight: 600;\n font-size: 0.75rem;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n }\n\n .ResourceCard_sourceLink__7e1619a1f6 {\n color: var(--bui-fg-secondary);\n display: flex;\n padding: var(--bui-space-1);\n border-radius: var(--bui-radius-1);\n align-self: flex-start;\n }\n\n .ResourceCard_sourceLink__7e1619a1f6:hover {\n color: var(--card-accent);\n background: var(--card-accent-bg);\n }\n\n .ResourceCard_description__7e1619a1f6 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n line-height: 1.5;\n }\n\n .ResourceCard_badges__7e1619a1f6 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n .ResourceCard_frameworkBadge__7e1619a1f6 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-full, 999px);\n padding: 0 var(--bui-space-2);\n font-size: 0.8125rem;\n line-height: 1.9;\n color: var(--bui-fg-primary);\n }\n\n .ResourceCard_tags__7e1619a1f6 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1) var(--bui-space-2);\n }\n\n .ResourceCard_tag__7e1619a1f6 {\n font-size: 0.75rem;\n line-height: 1.7;\n color: var(--bui-fg-secondary);\n opacity: 0.85;\n }\n\n .ResourceCard_footer__7e1619a1f6 {\n margin-top: auto;\n padding-top: var(--bui-space-2);\n border-top: 1px solid var(--bui-border-1);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--bui-space-2);\n }\n\n .ResourceCard_countsRow__7e1619a1f6 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n flex-shrink: 0;\n }\n\n .ResourceCard_count__7e1619a1f6 {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n font-size: 0.8125rem;\n color: var(--bui-fg-secondary);\n white-space: nowrap;\n }\n}\n";
4
+ var styles = {"card":"ResourceCard_card__7e1619a1f6","header":"ResourceCard_header__7e1619a1f6","body":"ResourceCard_body__7e1619a1f6","iconBox":"ResourceCard_iconBox__7e1619a1f6","headerText":"ResourceCard_headerText__7e1619a1f6","title":"ResourceCard_title__7e1619a1f6","typeLabel":"ResourceCard_typeLabel__7e1619a1f6","sourceLink":"ResourceCard_sourceLink__7e1619a1f6","description":"ResourceCard_description__7e1619a1f6","badges":"ResourceCard_badges__7e1619a1f6","frameworkBadge":"ResourceCard_frameworkBadge__7e1619a1f6","tags":"ResourceCard_tags__7e1619a1f6","tag":"ResourceCard_tag__7e1619a1f6","footer":"ResourceCard_footer__7e1619a1f6","countsRow":"ResourceCard_countsRow__7e1619a1f6","count":"ResourceCard_count__7e1619a1f6"};
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":";;;;;;;;"}