@nospt/plugin-dev-ai-hub 0.3.0 → 1.0.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/api/DevAiHubResourceClient.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +2 -2
- package/dist/components/AiResourcesPage/ResourceCard.esm.js +15 -61
- package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +2 -2
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +4 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +2 -2
- 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 +44 -11
- package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -1
- package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +3 -3
- package/dist/components/AiResourcesPage/typeMeta.esm.js +1 -2
- package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -1
- package/dist/components/ToolIcon/ToolIcon.esm.js +2 -1
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
- package/package.json +14 -6
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @nospt/plugin-dev-ai-hub
|
|
2
|
+
|
|
3
|
+
The frontend half of **Dev AI Hub** — a catalog-backed browser for AI assets usable by GitHub Copilot, Claude Code, Google Gemini, Cursor and other AI coding tools.
|
|
4
|
+
|
|
5
|
+
> **Requires Backstage 1.51 or later.** The `AiResource` kind comes from Backstage's own alpha catalog module, which is not available before that.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
Teams accumulate AI assets — a prompt that writes good commit messages, an agent that reviews code, an MCP server config for Grafana — and they end up scattered across repos, wikis and chat threads. Dev AI Hub gives them one page in Backstage, with the catalog as the source of truth.
|
|
12
|
+
|
|
13
|
+
Six kinds of resource, each with its own install path:
|
|
14
|
+
|
|
15
|
+
| Type | What it is |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `skill` | Reusable instructions an AI tool loads on demand |
|
|
18
|
+
| `agent` | A specialised assistant with its own system prompt |
|
|
19
|
+
| `hook` | Something that runs on a tool event, e.g. lint after an edit |
|
|
20
|
+
| `mcp-config` | An MCP server configuration to merge into your tool |
|
|
21
|
+
| `plugin` | A bundle of the above, installed from a marketplace |
|
|
22
|
+
| `marketplace` | A catalog of plugins you register with your tool once |
|
|
23
|
+
|
|
24
|
+
## What a user does with it
|
|
25
|
+
|
|
26
|
+
1. **Browses** — type tiles double as filters; search covers name, description and tags; further filters narrow by AI tool and tag.
|
|
27
|
+
2. **Opens a resource** — a drawer shows the metadata plus the body, resolved on demand from wherever it lives in Git. Markdown is rendered; an `mcp-config` is shown as copyable JSON.
|
|
28
|
+
3. **Installs it** — the dialog gives per-framework install paths, a copy button, a download where the body *is* the artifact, and one-click launchers where the tool has a URL handler.
|
|
29
|
+
|
|
30
|
+
Every resource opened or installed is counted, so the cards can show what colleagues actually use.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn --cwd packages/app add @nospt/plugin-dev-ai-hub
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This plugin uses the Backstage **New Frontend System**. In `packages/app/src/App.tsx`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { devAiHubPlugin } from '@nospt/plugin-dev-ai-hub';
|
|
42
|
+
|
|
43
|
+
export const app = createApp({
|
|
44
|
+
features: [
|
|
45
|
+
// ...existing features
|
|
46
|
+
devAiHubPlugin,
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The sidebar item and the `/dev-ai-hub` route register themselves.
|
|
52
|
+
|
|
53
|
+
### You also need
|
|
54
|
+
|
|
55
|
+
- **[`@nospt/plugin-dev-ai-hub-backend`](https://www.npmjs.com/package/@nospt/plugin-dev-ai-hub-backend)** — the page has nothing to show without it.
|
|
56
|
+
- **`@backstage/plugin-catalog-backend-module-ai-model`** — provides the `AiResource` kind.
|
|
57
|
+
- **`AiResource` allow-listed** in your `catalog.rules`, and at least one entity registered.
|
|
58
|
+
|
|
59
|
+
All three are covered in the [repository README](https://github.com/nosportugal/backstage-plugin-dev-ai-hub#readme), which is the single place kept current.
|
|
60
|
+
|
|
61
|
+
## Troubleshooting
|
|
62
|
+
|
|
63
|
+
| Symptom | Usually means |
|
|
64
|
+
|---|---|
|
|
65
|
+
| The page loads but says "Register AiResource entities…" | The catalog has no `AiResource` entities — or they were rejected because the kind is not in `catalog.rules` |
|
|
66
|
+
| "Could not load resources" | The backend plugin is not registered, or the request is unauthenticated |
|
|
67
|
+
| Components render unstyled | `@backstage/ui/css/styles.css` is not imported in `packages/app/src/index.tsx` — apps scaffolded by a current `create-app` already do this |
|
|
68
|
+
| A resource is listed but has no install actions | It publishes no `backstage.io/source-location`, so it is browsable but not installable |
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Apache-2.0
|
|
@@ -1 +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
|
|
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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"}
|
|
@@ -1 +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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"}
|
|
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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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
|
|
2
2
|
|
|
3
|
-
var css_248z = "/*\n * Plugin-owned colour tokens for the six ResourceType roles (ADR-0008,\n * ADR-0010)
|
|
4
|
-
var styles = {"pageRoot":"
|
|
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 /*\n * A uniform ink veil composited over the fill — appearance, not contrast.\n * At 8% the published hex is still plainly itself (yellow #fcd200 renders\n * as #e8c100) while the row loses the glare it had as six blocks of\n * full-strength brand colour, which read hardest against the light theme's\n * off-white page.\n *\n * It buys almost no legibility and is not claimed to: white on yellow goes\n * 1.46:1 -> 1.74:1, against a 3:1 floor. The trade recorded in ADR-0008 is\n * unchanged. Reaching that floor everywhere needs ~30%, and AA ~45%, by\n * which point yellow is #8c7500 and no longer a NOS colour — the outcome\n * that ADR records as rejected twice on sight.\n *\n * The veil is a separate token rather than baked into the hexes below so\n * the fills stay the brand values as published, and so the strength is one\n * number to revisit.\n */\n --devaihub-tile-veil: rgba(0, 0, 0, 0.08);\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__da942a1d83 {\n padding: var(--bui-space-6);\n }\n\n .AiResourcesPage_grid__da942a1d83 {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: var(--bui-space-4);\n }\n\n .AiResourcesPage_paginationRow__da942a1d83 {\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__da942a1d83 {\n animation: AiResourcesPage_card-entrance__da942a1d83 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__da942a1d83 {\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__da942a1d83 {\n animation: none;\n }\n }\n\n .AiResourcesPage_emptyState__da942a1d83 {\n text-align: center;\n padding: var(--bui-space-9) 0;\n }\n\n .AiResourcesPage_emptyEmoji__da942a1d83 {\n font-size: 40px;\n margin-bottom: var(--bui-space-2);\n animation: AiResourcesPage_empty-emoji-bounce__da942a1d83 700ms cubic-bezier(0.34, 1.56, 0.64, 1);\n }\n\n @keyframes AiResourcesPage_empty-emoji-bounce__da942a1d83 {\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__da942a1d83 {\n animation: none;\n }\n }\n}\n";
|
|
4
|
+
var styles = {"pageRoot":"AiResourcesPage_pageRoot__da942a1d83","grid":"AiResourcesPage_grid__da942a1d83","paginationRow":"AiResourcesPage_paginationRow__da942a1d83","cardEntrance":"AiResourcesPage_cardEntrance__da942a1d83","emptyState":"AiResourcesPage_emptyState__da942a1d83","emptyEmoji":"AiResourcesPage_emptyEmoji__da942a1d83"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { styles as default };
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { useEffect } from 'react';
|
|
3
|
-
import { useApi } from '@backstage/core-plugin-api';
|
|
4
2
|
import { Text } from '@backstage/ui';
|
|
5
|
-
import {
|
|
6
|
-
import { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient.esm.js';
|
|
3
|
+
import { RiEyeLine, RiDownloadLine } from '@remixicon/react';
|
|
7
4
|
import { useTelemetryCounts } from '../../hooks/useTelemetryCounts.esm.js';
|
|
8
5
|
import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
|
|
9
6
|
import { getTypeMeta, frameworkLabel } from './typeMeta.esm.js';
|
|
10
7
|
import styles from './ResourceCard.module.css.esm.js';
|
|
11
8
|
|
|
12
9
|
const MAX_VISIBLE_TAGS = 3;
|
|
13
|
-
const MAX_VISIBLE_FRAMEWORKS = 2;
|
|
14
10
|
const POPULAR_THRESHOLD = 5;
|
|
15
11
|
function ResourceCard({
|
|
16
12
|
resource,
|
|
@@ -19,19 +15,9 @@ function ResourceCard({
|
|
|
19
15
|
style
|
|
20
16
|
}) {
|
|
21
17
|
const meta = getTypeMeta(resource.type);
|
|
22
|
-
const api = useApi(devAiHubResourceApiRef);
|
|
23
18
|
const counts = useTelemetryCounts(resource.entityRef);
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
api.track(resource.entityRef, "view");
|
|
26
|
-
}, [resource.entityRef]);
|
|
27
|
-
const handleSourceClick = (e) => e.stopPropagation();
|
|
28
19
|
const visibleTags = resource.tags.slice(0, MAX_VISIBLE_TAGS);
|
|
29
20
|
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
21
|
const ownerLabel = resource.owner?.replace(/^(group|user):(default\/)?/, "");
|
|
36
22
|
return /* @__PURE__ */ jsxs(
|
|
37
23
|
"div",
|
|
@@ -53,6 +39,7 @@ function ResourceCard({
|
|
|
53
39
|
/* @__PURE__ */ jsxs("div", { className: styles.header, children: [
|
|
54
40
|
/* @__PURE__ */ jsx("div", { className: styles.iconBox, children: /* @__PURE__ */ jsx(meta.Icon, { size: 18 }) }),
|
|
55
41
|
/* @__PURE__ */ jsxs("div", { className: styles.headerText, children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Text, { variant: "body-x-small", className: styles.typeLabel, children: meta.label }),
|
|
56
43
|
/* @__PURE__ */ jsx(
|
|
57
44
|
Text,
|
|
58
45
|
{
|
|
@@ -62,50 +49,19 @@ function ResourceCard({
|
|
|
62
49
|
className: styles.title,
|
|
63
50
|
children: resource.title ?? resource.name
|
|
64
51
|
}
|
|
65
|
-
)
|
|
66
|
-
|
|
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
|
-
)
|
|
52
|
+
)
|
|
53
|
+
] })
|
|
81
54
|
] }),
|
|
82
55
|
/* @__PURE__ */ jsxs("div", { className: styles.body, children: [
|
|
83
|
-
resource.
|
|
84
|
-
|
|
56
|
+
resource.frameworks.length > 0 && /* @__PURE__ */ jsx("div", { className: styles.badges, children: resource.frameworks.map((f) => /* @__PURE__ */ jsx(
|
|
57
|
+
"span",
|
|
85
58
|
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
] }),
|
|
59
|
+
className: styles.frameworkBadge,
|
|
60
|
+
title: frameworkLabel(f),
|
|
61
|
+
children: /* @__PURE__ */ jsx(ToolIcon, { tool: f, size: 16 })
|
|
62
|
+
},
|
|
63
|
+
f
|
|
64
|
+
)) }),
|
|
109
65
|
resource.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles.tags, children: [
|
|
110
66
|
visibleTags.map((t) => /* @__PURE__ */ jsxs("span", { className: styles.tag, children: [
|
|
111
67
|
"#",
|
|
@@ -130,18 +86,16 @@ function ResourceCard({
|
|
|
130
86
|
].filter(Boolean).join(" \xB7 ") }),
|
|
131
87
|
/* @__PURE__ */ jsxs("span", { className: styles.countsRow, children: [
|
|
132
88
|
!!counts?.view && /* @__PURE__ */ jsxs("span", { className: styles.count, title: `${counts.view} views`, children: [
|
|
133
|
-
/* @__PURE__ */ jsx(RiEyeLine, { size:
|
|
134
|
-
" ",
|
|
89
|
+
/* @__PURE__ */ jsx(RiEyeLine, { size: 14 }),
|
|
135
90
|
counts.view
|
|
136
91
|
] }),
|
|
137
92
|
!!counts?.install && /* @__PURE__ */ jsxs(
|
|
138
93
|
"span",
|
|
139
94
|
{
|
|
140
|
-
className: styles.count,
|
|
95
|
+
className: counts.install >= POPULAR_THRESHOLD ? styles.countPopular : styles.count,
|
|
141
96
|
title: `${counts.install} installs`,
|
|
142
97
|
children: [
|
|
143
|
-
counts.install >= POPULAR_THRESHOLD ? "\u{1F525}" :
|
|
144
|
-
" ",
|
|
98
|
+
counts.install >= POPULAR_THRESHOLD ? "\u{1F525}" : /* @__PURE__ */ jsx(RiDownloadLine, { size: 14 }),
|
|
145
99
|
counts.install
|
|
146
100
|
]
|
|
147
101
|
}
|
|
@@ -1 +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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"}
|
|
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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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@layer components {\n .
|
|
4
|
-
var styles = {"card":"
|
|
3
|
+
var css_248z = "@layer components {\n /*\n * One continuous surface, generous radius, hairline edges — the macOS card\n * idiom. What made the previous card read as clunky was structural rather\n * than stylistic: a two-tone header band with its own background and bottom\n * rule, a second rule above the footer, and 4px corners. Three hard lines\n * cutting a small card into three boxes.\n *\n * Separation is now done with space and type weight instead of lines and\n * fills, which is the same trade the rest of the plugin makes.\n */\n .ResourceCard_card__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
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { styles as default };
|
|
@@ -23,6 +23,10 @@ function ResourceDetailPanel({
|
|
|
23
23
|
if (resourceProp) setDisplayResource(resourceProp);
|
|
24
24
|
}, [resourceProp]);
|
|
25
25
|
const api = useApi(devAiHubResourceApiRef);
|
|
26
|
+
const openedRef = resourceProp?.entityRef;
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (openedRef) api.track(openedRef, "view");
|
|
29
|
+
}, [openedRef]);
|
|
26
30
|
const actionable = !!displayResource?.sourceLocation;
|
|
27
31
|
const bodyState = useResourceBody(displayResource?.entityRef, actionable);
|
|
28
32
|
const counts = useTelemetryCounts(displayResource?.entityRef);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceDetailPanel.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceDetailPanel.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Box,\n Button,\n ButtonIcon,\n Flex,\n Link,\n Skeleton,\n Text,\n} from '@backstage/ui';\nimport {\n RiCloseLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiEyeLine,\n RiFileCopyLine,\n RiInstallLine,\n} from '@remixicon/react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n getBodyShape,\n hasCopyableBody,\n hasDownloadableArtifact,\n type FrameworkToken,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n}\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n}: ResourceDetailPanelProps) {\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n const bodyShape = getBodyShape(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n await navigator.clipboard.writeText(bodyState.body.content);\n api.track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = () => {\n api.track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n const handleInstall = () => {\n api.track(resource.entityRef, 'install');\n setInstallOpen(true);\n };\n\n const metadataRows: [string, string | undefined][] = [\n ['Type', meta.label],\n ['Lifecycle', resource.lifecycle || undefined],\n ['Owner', resource.owner],\n ['Version', resource.version],\n ['Entity ref', resource.entityRef],\n ];\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={{ '--drawer-accent': meta.color } as React.CSSProperties}\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n style={{ color: meta.color, fontWeight: 600 }}\n >\n {meta.label}\n </Text>\n </Box>\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onClick={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n )}\n\n {(!!counts?.view || !!counts?.install) && (\n <div className={styles.statsRow}>\n {!!counts?.view && (\n <span className={styles.statItem}>\n <RiEyeLine size={14} /> {counts.view} views\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.statItem}>\n <RiInstallLine size={14} /> {counts.install} installs\n </span>\n )}\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content\n </Text>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not\n have access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button\n size=\"small\"\n variant=\"tertiary\"\n onPress={bodyState.retry}\n >\n Retry\n </Button>\n </Flex>\n )}\n {bodyState.body && bodyShape === 'json' && (\n <pre className={styles.codeBlock}>\n <code>{bodyState.body.content}</code>\n </pre>\n )}\n {bodyState.body && bodyShape === 'markdown' && (\n <div className={styles.markdown}>\n <ReactMarkdown>{bodyState.body.content}</ReactMarkdown>\n </div>\n )}\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>{v}</dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ResourceDetailPanel.esm.js","sources":["../../../src/components/AiResourcesPage/ResourceDetailPanel.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport {\n Box,\n Button,\n ButtonIcon,\n Flex,\n Link,\n Skeleton,\n Text,\n} from '@backstage/ui';\nimport {\n RiCloseLine,\n RiDownloadLine,\n RiExternalLinkLine,\n RiEyeLine,\n RiFileCopyLine,\n RiInstallLine,\n} from '@remixicon/react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n getBodyShape,\n hasCopyableBody,\n hasDownloadableArtifact,\n type FrameworkToken,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { devAiHubResourceApiRef } from '../../api/DevAiHubResourceClient';\nimport { useResourceBody } from '../../hooks/useResourceBody';\nimport { useTelemetryCounts } from '../../hooks/useTelemetryCounts';\nimport { ToolIcon } from '../ToolIcon';\nimport { ResourceInstallDialog } from './ResourceInstallDialog';\nimport { frameworkLabel, getTypeMeta } from './typeMeta';\nimport styles from './ResourceDetailPanel.module.css';\n\ninterface ResourceDetailPanelProps {\n resource: ResourceSummary | undefined;\n onClose: () => void;\n}\n\n/**\n * The detail drawer (legacy pattern, URL-param driven). Metadata renders from\n * the catalog `ResourceSummary`; the body is fetched lazily on open from the\n * body resolver (issue #30) and rendered by shape. Resources without a\n * source-location are browsable but not actionable — no action buttons.\n */\nexport function ResourceDetailPanel({\n resource: resourceProp,\n onClose,\n}: ResourceDetailPanelProps) {\n const isOpen = !!resourceProp;\n // The drawer slides out on close (issue: \"add some flowers\"), which needs\n // content to stay rendered during the exit transition — the `resource` prop\n // itself goes undefined the instant the URL param clears. `displayResource`\n // mirrors the last real resource and is only ever updated while one is\n // present, so the drawer keeps showing it while animating out instead of\n // blanking.\n const [displayResource, setDisplayResource] = useState(resourceProp);\n useEffect(() => {\n if (resourceProp) setDisplayResource(resourceProp);\n }, [resourceProp]);\n\n const api = useApi(devAiHubResourceApiRef);\n\n // A `view` is a deliberate look at one resource, so it is recorded here —\n // when the drawer opens — and not on card render, where merely loading the\n // page counted a view for every card in the grid. Keyed on the *live* prop,\n // not `displayResource`: that one stays set through the exit animation, and\n // closing must not count a second view.\n const openedRef = resourceProp?.entityRef;\n useEffect(() => {\n if (openedRef) api.track(openedRef, 'view');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [openedRef]);\n\n const actionable = !!displayResource?.sourceLocation;\n const bodyState = useResourceBody(displayResource?.entityRef, actionable);\n const counts = useTelemetryCounts(displayResource?.entityRef);\n const [installOpen, setInstallOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n\n if (!displayResource) return null;\n\n const resource = displayResource;\n const meta = getTypeMeta(resource.type);\n const bodyShape = getBodyShape(resource.type);\n\n const handleCopy = async () => {\n if (!bodyState.body) return;\n await navigator.clipboard.writeText(bodyState.body.content);\n api.track(resource.entityRef, 'copy');\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleDownload = () => {\n api.track(resource.entityRef, 'download');\n return api.downloadEntityBody(resource.entityRef);\n };\n\n const handleInstall = () => {\n api.track(resource.entityRef, 'install');\n setInstallOpen(true);\n };\n\n const metadataRows: [string, string | undefined][] = [\n ['Type', meta.label],\n ['Lifecycle', resource.lifecycle || undefined],\n ['Owner', resource.owner],\n ['Version', resource.version],\n ['Entity ref', resource.entityRef],\n ];\n\n return (\n <>\n <div\n className={styles.overlay}\n data-open={isOpen}\n onClick={onClose}\n role=\"presentation\"\n aria-hidden={!isOpen}\n />\n <div\n className={styles.drawer}\n data-open={isOpen}\n style={{ '--drawer-accent': meta.color } as React.CSSProperties}\n role=\"dialog\"\n aria-label={resource.title ?? resource.name}\n aria-hidden={!isOpen}\n >\n <Flex className={styles.header}>\n <div\n className={styles.iconBox}\n style={\n {\n '--card-accent': meta.color,\n '--card-accent-bg': meta.colorBg,\n } as React.CSSProperties\n }\n >\n <meta.Icon size={20} />\n </div>\n <Box className={styles.headerText}>\n <Text variant=\"title-small\" weight=\"bold\" as=\"h2\">\n {resource.title ?? resource.name}\n </Text>\n <Text\n variant=\"body-x-small\"\n style={{ color: meta.color, fontWeight: 600 }}\n >\n {meta.label}\n </Text>\n </Box>\n <ButtonIcon\n icon={<RiCloseLine />}\n variant=\"tertiary\"\n onClick={onClose}\n aria-label=\"Close\"\n />\n </Flex>\n\n <div className={styles.body}>\n {resource.description && (\n <Text variant=\"body-small\" as=\"p\">\n {resource.description}\n </Text>\n )}\n\n {(!!counts?.view || !!counts?.install) && (\n <div className={styles.statsRow}>\n {!!counts?.view && (\n <span className={styles.statItem}>\n <RiEyeLine size={14} /> {counts.view} views\n </span>\n )}\n {!!counts?.install && (\n <span className={styles.statItem}>\n <RiInstallLine size={14} /> {counts.install} installs\n </span>\n )}\n </div>\n )}\n\n {actionable && (\n <div className={styles.actions}>\n {hasCopyableBody(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiFileCopyLine />}\n isDisabled={!bodyState.body}\n onPress={handleCopy}\n >\n {copied ? 'Copied!' : 'Copy'}\n </Button>\n )}\n {hasDownloadableArtifact(resource.type) && (\n <Button\n size=\"small\"\n variant=\"secondary\"\n iconStart={<RiDownloadLine />}\n onPress={handleDownload}\n >\n Download\n </Button>\n )}\n <Button\n size=\"small\"\n variant=\"primary\"\n iconStart={<RiInstallLine />}\n onPress={handleInstall}\n >\n Install\n </Button>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Content\n </Text>\n {!actionable && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n No content location published for this resource.\n </Text>\n )}\n {actionable && bodyState.loading && (\n <div className={styles.bodyLoading}>\n <Skeleton width=\"100%\" height={14} />\n <Skeleton width=\"85%\" height={14} />\n <Skeleton width=\"60%\" height={14} />\n </div>\n )}\n {actionable && bodyState.error === 'not-found' && (\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Content not available — it may have been removed, or you may not\n have access to it.\n </Text>\n )}\n {actionable && bodyState.error === 'upstream' && (\n <Flex align=\"center\" gap=\"2\">\n <Text variant=\"body-small\" as=\"p\" color=\"secondary\">\n Couldn’t fetch the content from its source.\n </Text>\n <Button\n size=\"small\"\n variant=\"tertiary\"\n onPress={bodyState.retry}\n >\n Retry\n </Button>\n </Flex>\n )}\n {bodyState.body && bodyShape === 'json' && (\n <pre className={styles.codeBlock}>\n <code>{bodyState.body.content}</code>\n </pre>\n )}\n {bodyState.body && bodyShape === 'markdown' && (\n <div className={styles.markdown}>\n <ReactMarkdown>{bodyState.body.content}</ReactMarkdown>\n </div>\n )}\n </div>\n\n {resource.frameworks.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Works with\n </Text>\n <div className={styles.badges}>\n {resource.frameworks.map(f => (\n <span key={f} className={styles.frameworkBadge}>\n <ToolIcon tool={f as FrameworkToken} size={14} />\n {frameworkLabel(f)}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {resource.tags.length > 0 && (\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Tags\n </Text>\n <div className={styles.badges}>\n {resource.tags.map(t => (\n <span key={t} className={styles.tag}>\n #{t}\n </span>\n ))}\n </div>\n </div>\n )}\n\n <div className={styles.section}>\n <Text variant=\"body-x-small\" color=\"secondary\" weight=\"bold\">\n Metadata\n </Text>\n <dl className={styles.metaList}>\n {metadataRows\n .filter(([, v]) => v)\n .map(([k, v]) => (\n <div key={k} className={styles.metaRow}>\n <dt>{k}</dt>\n <dd>{v}</dd>\n </div>\n ))}\n </dl>\n </div>\n\n {resource.sourceLocation && (\n <div className={styles.section}>\n <Link\n href={resource.sourceLocation.replace(/^url:/, '')}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={styles.sourceLinkContent}>\n <RiExternalLinkLine size={14} /> View source\n </span>\n </Link>\n </div>\n )}\n </div>\n </div>\n\n <ResourceInstallDialog\n resource={resource}\n body={bodyState.body}\n isOpen={installOpen}\n onOpenChange={setInstallOpen}\n />\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@layer components {\n .
|
|
4
|
-
var styles = {"overlay":"
|
|
3
|
+
var css_248z = "@layer components {\n .ResourceDetailPanel_overlay__cf1a969bfb {\n position: fixed;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.3);\n z-index: 1299;\n opacity: 0;\n pointer-events: none;\n transition: opacity 220ms ease;\n }\n\n .ResourceDetailPanel_overlay__cf1a969bfb[data-open='true'] {\n opacity: 1;\n pointer-events: auto;\n }\n\n .ResourceDetailPanel_drawer__cf1a969bfb {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: 520px;\n max-width: 100vw;\n /* An opaque surface token, not `--bui-bg-neutral-1`: that token is a\n translucent 10% white in dark mode, meant to layer over a solid\n background. The drawer floats over the page, so it must bring its own. */\n background-color: var(--bui-bg-app);\n /* The accent mirrors the open resource's type colour (set inline by the\n component). The fallback is BUI's own accent, which the host app points\n at its brand colour — a plugin-owned brand hex here would be a second,\n competing accent on the same screen (ADR-0008, 2026-08-03 amendment). */\n border-top: 3px solid var(--drawer-accent, var(--bui-fg-info));\n box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);\n z-index: 1300;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n pointer-events: none;\n transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n .ResourceDetailPanel_drawer__cf1a969bfb[data-open='true'] {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .ResourceDetailPanel_overlay__cf1a969bfb,\n .ResourceDetailPanel_drawer__cf1a969bfb {\n transition: none;\n }\n }\n\n .ResourceDetailPanel_header__cf1a969bfb {\n display: flex;\n align-items: flex-start;\n gap: var(--bui-space-3);\n padding: var(--bui-space-4);\n border-bottom: 1px solid var(--bui-border-1);\n }\n\n .ResourceDetailPanel_actions__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_bodyLoading__cf1a969bfb {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n margin-top: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_codeBlock__cf1a969bfb {\n margin: var(--bui-space-2) 0 0;\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-2);\n font-size: 0.8125rem;\n overflow-x: auto;\n white-space: pre;\n }\n\n .ResourceDetailPanel_markdown__cf1a969bfb {\n margin-top: var(--bui-space-2);\n font-size: 0.875rem;\n line-height: 1.55;\n overflow-wrap: anywhere;\n }\n\n .ResourceDetailPanel_markdown__cf1a969bfb pre {\n padding: var(--bui-space-3);\n background-color: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-2);\n overflow-x: auto;\n }\n\n .ResourceDetailPanel_markdown__cf1a969bfb h1,\n .ResourceDetailPanel_markdown__cf1a969bfb h2,\n .ResourceDetailPanel_markdown__cf1a969bfb h3 {\n font-size: 1rem;\n margin: var(--bui-space-3) 0 var(--bui-space-1);\n }\n\n .ResourceDetailPanel_iconBox__cf1a969bfb {\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 .ResourceDetailPanel_headerText__cf1a969bfb {\n flex: 1;\n min-width: 0;\n }\n\n .ResourceDetailPanel_body__cf1a969bfb {\n padding: var(--bui-space-4);\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-4);\n }\n\n .ResourceDetailPanel_section__cf1a969bfb {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n }\n\n .ResourceDetailPanel_badges__cf1a969bfb {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_statsRow__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-3);\n }\n\n .ResourceDetailPanel_statItem__cf1a969bfb {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.8125rem;\n color: var(--bui-fg-secondary);\n }\n\n .ResourceDetailPanel_frameworkBadge__cf1a969bfb {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-full, 999px);\n padding: 0 var(--bui-space-2);\n font-size: 0.75rem;\n line-height: 1.8;\n }\n\n .ResourceDetailPanel_tag__cf1a969bfb {\n background: var(--bui-bg-neutral-2);\n border-radius: var(--bui-radius-1);\n padding: 0 var(--bui-space-2);\n font-size: 0.75rem;\n line-height: 1.7;\n color: var(--bui-fg-secondary);\n }\n\n .ResourceDetailPanel_metaList__cf1a969bfb {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-1);\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb {\n display: flex;\n gap: var(--bui-space-2);\n font-size: 0.8rem;\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb dt {\n color: var(--bui-fg-secondary);\n min-width: 90px;\n flex-shrink: 0;\n }\n\n .ResourceDetailPanel_metaRow__cf1a969bfb dd {\n margin: 0;\n word-break: break-all;\n }\n\n .ResourceDetailPanel_sourceLinkContent__cf1a969bfb {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n color: var(--bui-fg-info);\n font-weight: 600;\n }\n}\n";
|
|
4
|
+
var styles = {"overlay":"ResourceDetailPanel_overlay__cf1a969bfb","drawer":"ResourceDetailPanel_drawer__cf1a969bfb","header":"ResourceDetailPanel_header__cf1a969bfb","actions":"ResourceDetailPanel_actions__cf1a969bfb","bodyLoading":"ResourceDetailPanel_bodyLoading__cf1a969bfb","codeBlock":"ResourceDetailPanel_codeBlock__cf1a969bfb","markdown":"ResourceDetailPanel_markdown__cf1a969bfb","iconBox":"ResourceDetailPanel_iconBox__cf1a969bfb","headerText":"ResourceDetailPanel_headerText__cf1a969bfb","body":"ResourceDetailPanel_body__cf1a969bfb","section":"ResourceDetailPanel_section__cf1a969bfb","badges":"ResourceDetailPanel_badges__cf1a969bfb","statsRow":"ResourceDetailPanel_statsRow__cf1a969bfb","statItem":"ResourceDetailPanel_statItem__cf1a969bfb","frameworkBadge":"ResourceDetailPanel_frameworkBadge__cf1a969bfb","tag":"ResourceDetailPanel_tag__cf1a969bfb","metaList":"ResourceDetailPanel_metaList__cf1a969bfb","metaRow":"ResourceDetailPanel_metaRow__cf1a969bfb","sourceLinkContent":"ResourceDetailPanel_sourceLinkContent__cf1a969bfb"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { styles as default };
|
|
@@ -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 * EXPERIMENT — tile variant \"aurora\" (see TILE_VARIANT in StatTiles.tsx).\n *\n * Frosted glass panes floating over a blurred colour field made from the six\n * brand hues. Each pane picks its type's colour up *through* the glass rather\n * than being filled with it, so — as in the console variant — colour is light\n * and text never sits on a saturated field.\n *\n * The material is deliberate: nosportugal/backstage already uses acrylic with\n * backdrop blur for its sidebar submenu, so this is the host app's own glass\n * turned expressive. Distinct from the rest of Backstage without looking like\n * a third-party widget bolted on.\n */\n@layer components {\n .StatTiles.aurora_grid__93817f37ed {\n position: relative;\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: var(--bui-space-3);\n margin-bottom: var(--bui-space-5);\n padding: var(--bui-space-3);\n border-radius: var(--bui-radius-3);\n isolation: isolate;\n /* The field is clipped to the row: unclipped, a 36px blur bleeds across\n the page and washes out everything around it. */\n overflow: hidden;\n background: var(--bui-bg-neutral-1);\n }\n\n /* The aurora: the six brand hues as soft overlapping lights, blurred into a\n field. Sits behind the panes and is what they refract. */\n .StatTiles.aurora_grid__93817f37ed::before {\n content: '';\n position: absolute;\n inset: -20%;\n z-index: -1;\n background: radial-gradient(28% 60% at 8% 40%, #6ea514 0%, transparent 70%),\n radial-gradient(26% 60% at 26% 60%, #eb84cd 0%, transparent 70%),\n radial-gradient(26% 60% at 44% 35%, #fcd200 0%, transparent 70%),\n radial-gradient(26% 60% at 62% 65%, #4bdbc5 0%, transparent 70%),\n radial-gradient(26% 60% at 80% 38%, #4f60d2 0%, transparent 70%),\n radial-gradient(28% 60% at 96% 60%, #f26b43 0%, transparent 70%);\n filter: blur(44px) saturate(150%);\n opacity: 0.5;\n pointer-events: none;\n }\n\n .StatTiles.aurora_tile__93817f37ed {\n position: relative;\n /* Glass is transparent, so the text is the theme's own foreground rather\n than white — the only variant where that is true, and the reason these\n are tokens. */\n --devaihub-tile-fg: var(--bui-fg-primary);\n --devaihub-tile-fg-muted: var(--bui-fg-secondary);\n /* Frosted pane: a translucent tint of the page surface, blurred behind. */\n background: color-mix(in srgb, var(--bui-bg-app) 62%, transparent);\n backdrop-filter: blur(16px) saturate(150%);\n -webkit-backdrop-filter: blur(16px) saturate(150%);\n border: 1px solid rgba(255, 255, 255, 0.22);\n border-radius: var(--bui-radius-3);\n /* Tighter than it looks like it should be: the pane sits inside the grid's\n own padding, so the two paddings stack and \"MCP Configs\" wraps. */\n padding: var(--bui-space-3);\n padding-bottom: var(--bui-space-5);\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n cursor: pointer;\n overflow: hidden;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28),\n 0 4px 20px -8px rgba(0, 0, 0, 0.35);\n transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1),\n border-color 180ms ease, background-color 180ms ease;\n }\n\n /* The type's own colour, caught in the glass. */\n .StatTiles.aurora_tile__93817f37ed::before {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(\n 120% 100% at 0% 0%,\n color-mix(in srgb, var(--tile-fill) 34%, transparent),\n transparent 72%\n );\n pointer-events: none;\n transition: opacity 180ms ease;\n opacity: 0.9;\n }\n\n .StatTiles.aurora_tile__93817f37ed:hover {\n transform: translateY(-2px);\n border-color: color-mix(in srgb, var(--tile-fill) 55%, transparent);\n }\n\n .StatTiles.aurora_tile__93817f37ed:hover::before {\n opacity: 1;\n }\n\n .StatTiles.aurora_tile__93817f37ed:focus-visible {\n outline: 2px solid var(--bui-fg-primary);\n outline-offset: 3px;\n }\n\n .StatTiles.aurora_tileActive__93817f37ed {\n transform: translateY(-2px);\n border-color: color-mix(in srgb, var(--tile-fill) 75%, transparent);\n background: color-mix(in srgb, var(--bui-bg-app) 45%, transparent);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35),\n 0 0 0 1px color-mix(in srgb, var(--tile-fill) 40%, transparent),\n 0 8px 28px -10px color-mix(in srgb, var(--tile-fill) 70%, transparent);\n }\n\n .StatTiles.aurora_tileIconBox__93817f37ed {\n position: relative;\n color: var(--tile-fill);\n background: color-mix(in srgb, var(--tile-fill) 16%, transparent);\n border-radius: var(--bui-radius-2);\n padding: var(--bui-space-1);\n display: flex;\n flex-shrink: 0;\n }\n\n .StatTiles.aurora_tileText__93817f37ed {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n }\n\n .StatTiles.aurora_tile__93817f37ed .StatTiles.aurora_tileValue__93817f37ed {\n color: var(--bui-fg-primary);\n font-size: 1.75rem;\n font-weight: 700;\n line-height: 1.1;\n font-variant-numeric: tabular-nums;\n letter-spacing: -0.02em;\n display: block;\n }\n\n .StatTiles.aurora_tile__93817f37ed .StatTiles.aurora_tileLabel__93817f37ed {\n color: var(--bui-fg-secondary);\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n line-height: 1.2;\n display: block;\n }\n\n .StatTiles.aurora_shareTrack__93817f37ed {\n position: absolute;\n left: var(--bui-space-3);\n right: var(--bui-space-3);\n bottom: var(--bui-space-3);\n height: 2px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--bui-fg-primary) 12%, transparent);\n overflow: hidden;\n }\n\n .StatTiles.aurora_shareFill__93817f37ed {\n display: block;\n height: 100%;\n width: var(--tile-share);\n border-radius: inherit;\n background: var(--tile-fill);\n transition: width 320ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .StatTiles.aurora_tile__93817f37ed,\n .StatTiles.aurora_tile__93817f37ed::before,\n .StatTiles.aurora_shareFill__93817f37ed {\n transition: none;\n }\n\n .StatTiles.aurora_tile__93817f37ed:hover,\n .StatTiles.aurora_tileActive__93817f37ed {\n transform: none;\n }\n }\n}\n";
|
|
4
|
+
var auroraStyles = {"grid":"StatTiles.aurora_grid__93817f37ed","tile":"StatTiles.aurora_tile__93817f37ed","tileActive":"StatTiles.aurora_tileActive__93817f37ed StatTiles.aurora_tile__93817f37ed","tileIconBox":"StatTiles.aurora_tileIconBox__93817f37ed","tileText":"StatTiles.aurora_tileText__93817f37ed","tileValue":"StatTiles.aurora_tileValue__93817f37ed","tileLabel":"StatTiles.aurora_tileLabel__93817f37ed","shareTrack":"StatTiles.aurora_shareTrack__93817f37ed","shareFill":"StatTiles.aurora_shareFill__93817f37ed"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { auroraStyles as default };
|
|
8
|
+
//# sourceMappingURL=StatTiles.aurora.module.css.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatTiles.aurora.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -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 * EXPERIMENT — tile variant \"console\" (see TILE_VARIANT in StatTiles.tsx).\n *\n * The six types as one instrument panel rather than six boxes: a single dark\n * recessed strip, each type a segment lit by its own colour.\n *\n * The central move is that colour is *light*, not paint — it appears as a glow,\n * a light bar and the icon, and never as a field behind text. Text is always\n * near-white on the same dark panel, so every type is equally legible and the\n * white-on-yellow problem that dogged the filled tiles cannot arise. The panel\n * is dark in both themes on purpose: it is the page's signature element.\n */\n@layer components {\n .StatTiles.console_grid__051b968612 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 1px;\n /* The 1px gap plus this background paints the divider lines, so the row\n reads as one instrument with segments rather than six objects. */\n background: rgba(255, 255, 255, 0.07);\n padding: 1px;\n border-radius: var(--bui-radius-3);\n margin-bottom: var(--bui-space-5);\n overflow: hidden;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06),\n 0 8px 24px -12px rgba(0, 0, 0, 0.5);\n }\n\n .StatTiles.console_tile__051b968612 {\n position: relative;\n --devaihub-tile-fg: #f2f4f8;\n --devaihub-tile-fg-muted: rgba(242, 244, 248, 0.62);\n background: #12141c;\n padding: var(--bui-space-4);\n padding-top: var(--bui-space-5);\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n cursor: pointer;\n isolation: isolate;\n transition: background-color 180ms ease, box-shadow 180ms ease;\n }\n\n /* The segment's light: a bar across the top edge in the type's colour, with\n the glow it casts down into the panel. */\n .StatTiles.console_tile__051b968612::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 2px;\n background: var(--tile-fill);\n box-shadow: 0 0 12px 0 var(--tile-fill);\n opacity: 0.75;\n transition: opacity 180ms ease, box-shadow 180ms ease;\n }\n\n /* The colour bleeding into the segment from that light source. */\n .StatTiles.console_tile__051b968612::after {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(\n 120% 90% at 50% 0%,\n color-mix(in srgb, var(--tile-fill) 30%, transparent),\n transparent 70%\n );\n opacity: 0.55;\n pointer-events: none;\n z-index: -1;\n transition: opacity 180ms ease;\n }\n\n .StatTiles.console_tile__051b968612:hover {\n background: #171a24;\n }\n\n .StatTiles.console_tile__051b968612:hover::before {\n opacity: 1;\n box-shadow: 0 0 18px 1px var(--tile-fill);\n }\n\n .StatTiles.console_tile__051b968612:hover::after {\n opacity: 0.8;\n }\n\n .StatTiles.console_tile__051b968612:focus-visible {\n outline: 2px solid var(--tile-fill);\n outline-offset: -2px;\n }\n\n .StatTiles.console_tileActive__051b968612 {\n background: #1b1f2b;\n }\n\n .StatTiles.console_tileActive__051b968612::before {\n opacity: 1;\n height: 3px;\n box-shadow: 0 0 22px 2px var(--tile-fill);\n }\n\n .StatTiles.console_tileActive__051b968612::after {\n opacity: 1;\n }\n\n .StatTiles.console_tileIconBox__051b968612 {\n color: var(--tile-fill);\n display: flex;\n flex-shrink: 0;\n /* The glyph is a light source too, so it gets the same bloom. */\n filter: drop-shadow(\n 0 0 6px color-mix(in srgb, var(--tile-fill) 60%, transparent)\n );\n }\n\n .StatTiles.console_tileText__051b968612 {\n display: flex;\n flex-direction: column;\n min-width: 0;\n }\n\n .StatTiles.console_tile__051b968612 .StatTiles.console_tileValue__051b968612 {\n color: #f2f4f8;\n font-size: 1.625rem;\n font-weight: 700;\n line-height: 1.1;\n /* Tabular figures: the counts sit in a fixed column like an instrument\n readout instead of shifting as values change. */\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.01em;\n display: block;\n }\n\n .StatTiles.console_tile__051b968612 .StatTiles.console_tileLabel__051b968612 {\n color: rgba(242, 244, 248, 0.62);\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n line-height: 1.2;\n display: block;\n }\n\n /* Share readout, drawn in the segment's own light. */\n .StatTiles.console_shareTrack__051b968612 {\n position: absolute;\n left: var(--bui-space-4);\n right: var(--bui-space-4);\n bottom: var(--bui-space-3);\n height: 2px;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.09);\n overflow: hidden;\n }\n\n .StatTiles.console_shareFill__051b968612 {\n display: block;\n height: 100%;\n width: var(--tile-share);\n border-radius: inherit;\n background: var(--tile-fill);\n box-shadow: 0 0 8px 0 var(--tile-fill);\n transition: width 320ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .StatTiles.console_tile__051b968612,\n .StatTiles.console_tile__051b968612::before,\n .StatTiles.console_tile__051b968612::after,\n .StatTiles.console_shareFill__051b968612 {\n transition: none;\n }\n }\n}\n";
|
|
4
|
+
var consoleStyles = {"grid":"StatTiles.console_grid__051b968612","tile":"StatTiles.console_tile__051b968612","tileActive":"StatTiles.console_tileActive__051b968612 StatTiles.console_tile__051b968612","tileIconBox":"StatTiles.console_tileIconBox__051b968612","tileText":"StatTiles.console_tileText__051b968612","tileValue":"StatTiles.console_tileValue__051b968612","tileLabel":"StatTiles.console_tileLabel__051b968612","shareTrack":"StatTiles.console_shareTrack__051b968612","shareFill":"StatTiles.console_shareFill__051b968612"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { consoleStyles as default };
|
|
8
|
+
//# sourceMappingURL=StatTiles.console.module.css.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatTiles.console.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from '@backstage/ui';
|
|
3
3
|
import { RESOURCE_TYPES } from '@nospt/plugin-dev-ai-hub-common';
|
|
4
4
|
import { getTypeMeta } from './typeMeta.esm.js';
|
|
5
|
-
import
|
|
5
|
+
import solidStyles from './StatTiles.module.css.esm.js';
|
|
6
|
+
import consoleStyles from './StatTiles.console.module.css.esm.js';
|
|
7
|
+
import auroraStyles from './StatTiles.aurora.module.css.esm.js';
|
|
6
8
|
|
|
9
|
+
const TILE_VARIANT = "solid";
|
|
10
|
+
const VARIANT_STYLES = {
|
|
11
|
+
solid: solidStyles,
|
|
12
|
+
console: consoleStyles,
|
|
13
|
+
aurora: auroraStyles
|
|
14
|
+
};
|
|
15
|
+
const styles = VARIANT_STYLES[TILE_VARIANT];
|
|
7
16
|
function StatTiles({ counts, activeType, onToggle }) {
|
|
17
|
+
const total = RESOURCE_TYPES.reduce((sum, type) => sum + counts[type], 0);
|
|
8
18
|
return /* @__PURE__ */ jsx("div", { className: styles.grid, children: RESOURCE_TYPES.map((type) => {
|
|
9
19
|
const meta = getTypeMeta(type);
|
|
10
20
|
const isActive = activeType === type;
|
|
11
|
-
|
|
21
|
+
const share = total > 0 ? counts[type] / total : 0;
|
|
22
|
+
const percent = Math.round(share * 100);
|
|
23
|
+
return /* @__PURE__ */ jsxs(
|
|
12
24
|
"div",
|
|
13
25
|
{
|
|
14
26
|
className: isActive ? styles.tileActive : styles.tile,
|
|
15
27
|
style: {
|
|
16
|
-
"--tile-
|
|
17
|
-
"--tile-
|
|
28
|
+
"--tile-fill": meta.tileFill,
|
|
29
|
+
"--tile-share": `${share * 100}%`
|
|
18
30
|
},
|
|
31
|
+
title: `${counts[type]} of ${total} resources (${percent}%)`,
|
|
19
32
|
onClick: () => onToggle(type),
|
|
20
33
|
onKeyDown: (e) => {
|
|
21
34
|
if (e.key === "Enter" || e.key === " ") onToggle(type);
|
|
@@ -24,21 +37,41 @@ function StatTiles({ counts, activeType, onToggle }) {
|
|
|
24
37
|
tabIndex: 0,
|
|
25
38
|
"aria-pressed": isActive,
|
|
26
39
|
"aria-label": `Filter by ${meta.pluralLabel}`,
|
|
27
|
-
children:
|
|
28
|
-
/* @__PURE__ */
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ jsx("div", { className: styles.tileIconBox, "aria-hidden": true, children: /* @__PURE__ */ jsx(meta.Icon, { size: 20 }) }),
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: styles.tileText, children: [
|
|
29
43
|
/* @__PURE__ */ jsx(
|
|
30
44
|
Text,
|
|
31
45
|
{
|
|
32
|
-
variant: "title-
|
|
46
|
+
variant: "title-medium",
|
|
33
47
|
weight: "bold",
|
|
34
48
|
className: styles.tileValue,
|
|
49
|
+
style: {
|
|
50
|
+
color: "var(--devaihub-tile-fg)",
|
|
51
|
+
fontSize: "1.75rem",
|
|
52
|
+
lineHeight: 1.1
|
|
53
|
+
},
|
|
35
54
|
children: counts[type]
|
|
36
55
|
}
|
|
37
56
|
),
|
|
38
|
-
/* @__PURE__ */ jsx(
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
Text,
|
|
59
|
+
{
|
|
60
|
+
variant: "body-small",
|
|
61
|
+
className: styles.tileLabel,
|
|
62
|
+
style: {
|
|
63
|
+
color: "var(--devaihub-tile-fg-muted, var(--devaihub-tile-fg))",
|
|
64
|
+
fontSize: "0.75rem",
|
|
65
|
+
fontWeight: 600,
|
|
66
|
+
letterSpacing: "0.07em",
|
|
67
|
+
textTransform: "uppercase"
|
|
68
|
+
},
|
|
69
|
+
children: meta.pluralLabel
|
|
70
|
+
}
|
|
71
|
+
)
|
|
39
72
|
] }),
|
|
40
|
-
/* @__PURE__ */ jsx("div", { className: styles.
|
|
41
|
-
]
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: styles.shareTrack, "aria-hidden": true, children: /* @__PURE__ */ jsx("span", { className: styles.shareFill }) })
|
|
74
|
+
]
|
|
42
75
|
},
|
|
43
76
|
type
|
|
44
77
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatTiles.esm.js","sources":["../../../src/components/AiResourcesPage/StatTiles.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"StatTiles.esm.js","sources":["../../../src/components/AiResourcesPage/StatTiles.tsx"],"sourcesContent":["import { Text } from '@backstage/ui';\nimport {\n RESOURCE_TYPES,\n type ResourceType,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { getTypeMeta } from './typeMeta';\nimport solidStyles from './StatTiles.module.css';\nimport consoleStyles from './StatTiles.console.module.css';\nimport auroraStyles from './StatTiles.aurora.module.css';\n\n/**\n * EXPERIMENT (2026-08-04) — three visual treatments of the type row, chosen at\n * build time. Every variant renders the same DOM and the same behaviour; they\n * differ only in stylesheet, so switching between them cannot change what the\n * component does.\n *\n * - `solid` — shipped design: brand-coloured blocks, white content.\n * - `console` — one dark instrument panel, each type a segment lit by its hue.\n * - `aurora` — frosted glass panes over a blurred field of the six hues.\n *\n * TO REVERT THE EXPERIMENT ENTIRELY: set this to `solid`, delete\n * `StatTiles.console.module.css` and `StatTiles.aurora.module.css`, and drop\n * their imports above. Nothing else in the plugin references them.\n *\n * Once a variant is chosen, collapse this: the winner's rules move into\n * `StatTiles.module.css` and the losers are deleted. Leaving three stylesheets\n * in the bundle is the cost of the experiment, not a design.\n */\nconst TILE_VARIANT: 'solid' | 'console' | 'aurora' = 'solid';\n\nconst VARIANT_STYLES = {\n solid: solidStyles,\n console: consoleStyles,\n aurora: auroraStyles,\n};\n\nconst styles = VARIANT_STYLES[TILE_VARIANT];\n\ninterface StatTilesProps {\n counts: Record<ResourceType, number>;\n activeType?: ResourceType;\n onToggle: (type: ResourceType) => void;\n}\n\n/**\n * The six clickable type tiles that act as the type filter (legacy pattern):\n * click filters to that type, click again clears.\n *\n * A share bar under each tile carries that type's proportion of the catalog,\n * so the row reads as a distribution (\"mostly skills, few hooks\") rather than\n * six identical objects — repetition is what makes attention fall off across a\n * row, and the count alone does nothing to break it.\n */\nexport function StatTiles({ counts, activeType, onToggle }: StatTilesProps) {\n const total = RESOURCE_TYPES.reduce((sum, type) => sum + counts[type], 0);\n\n return (\n <div className={styles.grid}>\n {RESOURCE_TYPES.map(type => {\n const meta = getTypeMeta(type);\n const isActive = activeType === type;\n const share = total > 0 ? counts[type] / total : 0;\n const percent = Math.round(share * 100);\n return (\n <div\n key={type}\n className={isActive ? styles.tileActive : styles.tile}\n style={\n {\n '--tile-fill': meta.tileFill,\n '--tile-share': `${share * 100}%`,\n } as React.CSSProperties\n }\n title={`${counts[type]} of ${total} resources (${percent}%)`}\n onClick={() => onToggle(type)}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') onToggle(type);\n }}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isActive}\n aria-label={`Filter by ${meta.pluralLabel}`}\n >\n {/* Echoes the card's tinted icon square, but inverted: a\n translucent white panel on the tile's own colour. Hidden from\n assistive tech — the tile is already labelled. */}\n <div className={styles.tileIconBox} aria-hidden>\n <meta.Icon size={20} />\n </div>\n <div className={styles.tileText}>\n {/* Typography is set inline as well as in the stylesheet: BUI's\n Text carries its own colour and size, and which of the two\n wins depends on the order BUI's styles land in the host app\n relative to this plugin's. In an app where BUI came last,\n these fell back to BUI's own values — which is how the tile\n labels once silently changed colour between themes. */}\n <Text\n variant=\"title-medium\"\n weight=\"bold\"\n className={styles.tileValue}\n style={{\n color: 'var(--devaihub-tile-fg)',\n fontSize: '1.75rem',\n lineHeight: 1.1,\n }}\n >\n {counts[type]}\n </Text>\n <Text\n variant=\"body-small\"\n className={styles.tileLabel}\n style={{\n color:\n 'var(--devaihub-tile-fg-muted, var(--devaihub-tile-fg))',\n fontSize: '0.75rem',\n fontWeight: 600,\n letterSpacing: '0.07em',\n textTransform: 'uppercase',\n }}\n >\n {meta.pluralLabel}\n </Text>\n </div>\n {/* The share bar. Decorative for assistive tech — the same figure\n is already in the tile's title and derivable from the count. */}\n <div className={styles.shareTrack} aria-hidden>\n <span className={styles.shareFill} />\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import styleInject from '../../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@layer components {\n
|
|
4
|
-
var
|
|
3
|
+
var css_248z = "@layer components {\n /*\n * The type filter: a row of solid brand-coloured blocks, each carrying a\n * share bar along its bottom edge — the type's proportion of the catalog.\n *\n * The bar is the data-ink. It answers \"what is this catalog made of?\" in the\n * space the tile already occupied, and it breaks the uniformity of six\n * identical tiles, which is what makes attention drop off across a repeated\n * row (eye-tracking: users barely register the last two of five).\n *\n * A full-height proportional fill was built first — the saturated part being\n * the share, the remainder the same hue dimmed — and rejected on sight: the\n * dimmed remainder is the muddy tone this palette has repeatedly failed on,\n * and it covered most of every tile. The bar keeps the encoding and leaves\n * the colour alone.\n *\n * The fill is `--devaihub-tile-*` (the brand hex, identical in both themes),\n * NOT `--devaihub-type-*`: those are darkened in light mode to read against\n * a card surface, and filling with them turns the row olive. See the token\n * block in AiResourcesPage.module.css.\n */\n .StatTiles_grid__e83ca2277a {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));\n gap: var(--bui-space-3);\n margin-bottom: var(--bui-space-5);\n }\n\n .StatTiles_tile__e83ca2277a {\n position: relative;\n /* The variant owns its foreground: the components reference these tokens\n inline, which is what lets one DOM serve three treatments. */\n --devaihub-tile-fg: #ffffff;\n --devaihub-tile-fg-muted: rgba(255, 255, 255, 0.92);\n /* Ink veil over the published hex — see --devaihub-tile-veil for why it is\n a veil and not a darker set of fills. The gradient is a flat fill: two\n identical stops is the shortest way to composite a colour over another\n in one background shorthand. The shadows below stay keyed to the raw\n --tile-fill, so the hue-tinted glow keeps its full-strength colour. */\n background: linear-gradient(\n var(--devaihub-tile-veil),\n var(--devaihub-tile-veil)\n ),\n var(--tile-fill);\n border-radius: var(--bui-radius-3);\n padding: var(--bui-space-4);\n display: flex;\n align-items: center;\n gap: var(--bui-space-3);\n cursor: pointer;\n /* A lit top edge and a shadow tinted with the tile's own hue, so the row\n sits on the page rather than being pasted onto it. */\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),\n 0 1px 2px rgba(0, 0, 0, 0.12),\n 0 8px 18px -10px color-mix(in srgb, var(--tile-fill) 60%, transparent);\n /* Room for the share bar under the label. */\n padding-bottom: var(--bui-space-5);\n transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 160ms ease;\n }\n\n .StatTiles_tile__e83ca2277a:hover {\n transform: translateY(-2px);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32),\n 0 2px 4px rgba(0, 0, 0, 0.14),\n 0 14px 26px -12px color-mix(in srgb, var(--tile-fill) 70%, transparent);\n }\n\n .StatTiles_tile__e83ca2277a:focus-visible {\n outline: 2px solid var(--bui-fg-primary);\n outline-offset: 3px;\n }\n\n /* The ring sits OUTSIDE, on the page background, and takes the theme's\n foreground. A tint or an inset white ring cannot read on a solid fill —\n white is invisible on the yellow and turquoise tiles, which is exactly\n where selection has to be legible. */\n .StatTiles_tileActive__e83ca2277a {\n transform: translateY(-2px);\n outline: 2px solid var(--bui-fg-primary);\n outline-offset: 3px;\n }\n\n /* Share bar: a track across the full width with the fill sized to this\n type's proportion of the catalog. Drawn in the tile's own foreground so it\n works on every hue without a second colour decision. */\n .StatTiles_shareTrack__e83ca2277a {\n position: absolute;\n left: var(--bui-space-4);\n right: var(--bui-space-4);\n bottom: var(--bui-space-3);\n height: 3px;\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.28);\n overflow: hidden;\n }\n\n .StatTiles_shareFill__e83ca2277a {\n display: block;\n height: 100%;\n width: var(--tile-share);\n border-radius: inherit;\n background: var(--devaihub-tile-fg);\n transition: width 320ms cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n .StatTiles_tileIconBox__e83ca2277a {\n background: rgba(255, 255, 255, 0.22);\n color: var(--devaihub-tile-fg);\n border-radius: var(--bui-radius-2);\n padding: var(--bui-space-2);\n display: flex;\n flex-shrink: 0;\n }\n\n .StatTiles_tileText__e83ca2277a {\n display: flex;\n flex-direction: column;\n min-width: 0;\n }\n\n /* Colour and size are set inline on the components too: BUI's Text carries\n its own, and which wins depends on the order BUI's styles land in the host\n app relative to this plugin's. These rules cover hosts where the plugin\n lands last. */\n .StatTiles_tile__e83ca2277a .StatTiles_tileValue__e83ca2277a {\n color: var(--devaihub-tile-fg);\n font-size: 1.75rem;\n font-weight: 700;\n line-height: 1.1;\n letter-spacing: -0.02em;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);\n display: block;\n }\n\n .StatTiles_tile__e83ca2277a .StatTiles_tileLabel__e83ca2277a {\n color: var(--devaihub-tile-fg);\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.07em;\n text-transform: uppercase;\n line-height: 1.2;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);\n display: block;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .StatTiles_tile__e83ca2277a {\n transition: none;\n }\n\n .StatTiles_tile__e83ca2277a:hover,\n .StatTiles_tileActive__e83ca2277a {\n transform: none;\n }\n\n .StatTiles_shareFill__e83ca2277a {\n transition: none;\n }\n }\n}\n";
|
|
4
|
+
var solidStyles = {"grid":"StatTiles_grid__e83ca2277a","tile":"StatTiles_tile__e83ca2277a","tileActive":"StatTiles_tileActive__e83ca2277a StatTiles_tile__e83ca2277a","shareTrack":"StatTiles_shareTrack__e83ca2277a","shareFill":"StatTiles_shareFill__e83ca2277a","tileIconBox":"StatTiles_tileIconBox__e83ca2277a","tileText":"StatTiles_tileText__e83ca2277a","tileValue":"StatTiles_tileValue__e83ca2277a","tileLabel":"StatTiles_tileLabel__e83ca2277a"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export { solidStyles as default };
|
|
8
8
|
//# sourceMappingURL=StatTiles.module.css.esm.js.map
|
|
@@ -16,8 +16,7 @@ function getTypeMeta(type) {
|
|
|
16
16
|
Icon: ICONS[info.icon],
|
|
17
17
|
color: `var(--devaihub-type-${info.colorRole})`,
|
|
18
18
|
colorBg: `var(--devaihub-type-${info.colorRole}-bg)`,
|
|
19
|
-
|
|
20
|
-
tileDeep: `var(--devaihub-tile-${info.colorRole}-deep)`
|
|
19
|
+
tileFill: `var(--devaihub-tile-${info.colorRole})`
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
22
|
const FRAMEWORK_LABELS = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typeMeta.esm.js","sources":["../../../src/components/AiResourcesPage/typeMeta.tsx"],"sourcesContent":["import type { ElementType } from 'react';\nimport {\n RiFlashlightLine,\n RiPlugLine,\n RiPuzzleLine,\n RiRobot2Line,\n RiStore2Line,\n RiToolsLine,\n} from '@remixicon/react';\nimport {\n RESOURCE_TYPE_REGISTRY,\n type ResourceType,\n type ResourceTypeIcon,\n type ResourceTypeInfo,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Maps the registry's React-free icon identifiers to components. */\nconst ICONS: Record<ResourceTypeIcon, ElementType> = {\n tools: RiToolsLine,\n robot: RiRobot2Line,\n flash: RiFlashlightLine,\n plug: RiPlugLine,\n puzzle: RiPuzzleLine,\n store: RiStore2Line,\n};\n\nexport interface TypeMeta extends ResourceTypeInfo {\n Icon: ElementType;\n /** The plugin-owned colour tokens for this role (ADR-0008). */\n color: string;\n colorBg: string;\n
|
|
1
|
+
{"version":3,"file":"typeMeta.esm.js","sources":["../../../src/components/AiResourcesPage/typeMeta.tsx"],"sourcesContent":["import type { ElementType } from 'react';\nimport {\n RiFlashlightLine,\n RiPlugLine,\n RiPuzzleLine,\n RiRobot2Line,\n RiStore2Line,\n RiToolsLine,\n} from '@remixicon/react';\nimport {\n RESOURCE_TYPE_REGISTRY,\n type ResourceType,\n type ResourceTypeIcon,\n type ResourceTypeInfo,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Maps the registry's React-free icon identifiers to components. */\nconst ICONS: Record<ResourceTypeIcon, ElementType> = {\n tools: RiToolsLine,\n robot: RiRobot2Line,\n flash: RiFlashlightLine,\n plug: RiPlugLine,\n puzzle: RiPuzzleLine,\n store: RiStore2Line,\n};\n\nexport interface TypeMeta extends ResourceTypeInfo {\n Icon: ElementType;\n /** The plugin-owned colour tokens for this role (ADR-0008). */\n color: string;\n colorBg: string;\n /**\n * The stat tile's solid fill: the brand hex as published, which is NOT\n * `color` — that one is darkened in light mode to read against a card.\n */\n tileFill: string;\n}\n\nexport function getTypeMeta(type: ResourceType): TypeMeta {\n const info = RESOURCE_TYPE_REGISTRY[type];\n return {\n ...info,\n Icon: ICONS[info.icon],\n color: `var(--devaihub-type-${info.colorRole})`,\n colorBg: `var(--devaihub-type-${info.colorRole}-bg)`,\n tileFill: `var(--devaihub-tile-${info.colorRole})`,\n };\n}\n\n/** Display labels for the known framework tokens; unknown pass through. */\nconst FRAMEWORK_LABELS: Record<string, string> = {\n 'claude-code': 'Claude Code',\n 'github-copilot': 'GitHub Copilot',\n 'google-gemini': 'Google Gemini',\n cursor: 'Cursor',\n all: 'All tools',\n};\n\nexport function frameworkLabel(token: string): string {\n return FRAMEWORK_LABELS[token] ?? token;\n}\n"],"names":[],"mappings":";;;AAiBA,MAAM,KAAA,GAA+C;AAAA,EACnD,KAAA,EAAO,WAAA;AAAA,EACP,KAAA,EAAO,YAAA;AAAA,EACP,KAAA,EAAO,gBAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,MAAA,EAAQ,YAAA;AAAA,EACR,KAAA,EAAO;AACT,CAAA;AAcO,SAAS,YAAY,IAAA,EAA8B;AACxD,EAAA,MAAM,IAAA,GAAO,uBAAuB,IAAI,CAAA;AACxC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAAA,IACrB,KAAA,EAAO,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA,CAAA;AAAA,IAC5C,OAAA,EAAS,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,IAAA,CAAA;AAAA,IAC9C,QAAA,EAAU,CAAA,oBAAA,EAAuB,IAAA,CAAK,SAAS,CAAA,CAAA;AAAA,GACjD;AACF;AAGA,MAAM,gBAAA,GAA2C;AAAA,EAC/C,aAAA,EAAe,aAAA;AAAA,EACf,gBAAA,EAAkB,gBAAA;AAAA,EAClB,eAAA,EAAiB,eAAA;AAAA,EACjB,MAAA,EAAQ,QAAA;AAAA,EACR,GAAA,EAAK;AACP,CAAA;AAEO,SAAS,eAAe,KAAA,EAAuB;AACpD,EAAA,OAAO,gBAAA,CAAiB,KAAK,CAAA,IAAK,KAAA;AACpC;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolIcon.esm.js","sources":["../../../src/components/ToolIcon/ToolIcon.tsx"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { RiInfinityLine } from '@remixicon/react';\nimport { siAnthropic, siGithub, siGooglegemini } from 'simple-icons';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\n\n/* Cursor's cube mark isn't in simple-icons; path from Boxicons Brands\n (`bxl:cursor-ai`, MIT). Monochrome brand — no branded hex. */\nconst CURSOR_PATH =\n 'm20.42 6.73l-8-4.62a.82.82 0 0 0-.83 0L3.58 6.73c-.22.12-.35.36-.35.61v9.32c0 .25.13.48.35.61l8.01 4.62c.26.15.57.15.83 0l8.01-4.62c.22-.12.35-.36.35-.61V7.34c0-.25-.13-.48-.35-.61Zm-.5.98L12.19 21.1c-.05.09-.19.05-.19-.05v-8.77c0-.18-.09-.34-.25-.43L4.16 7.47c-.09-.05-.05-.19.05-.19h15.46c.22 0 .36.24.25.43';\n\ntype SvgTool = Exclude<FrameworkToken, 'all' | 'cursor'>;\n\n/**\n * Anthropic's and GitHub's marks are monochrome brand assets (near-black,\n * `#191919` / `#181717`) meant to be manually inverted per surface — used\n * literally, they're invisible on the dark theme's dark backgrounds. Treat\n * them like `cursor` below (theme-aware `--bui-fg-primary`) instead of their\n * literal hex. Google's mark is an actual mid-tone brand colour (`#8E75B2`)\n * that reads fine on both themes, so it keeps its literal hex.\n */\nconst TOOL_ICON: Record<\n SvgTool,\n { path: string; hex: string; label: string; monochrome?: boolean }\n> = {\n 'claude-code': { ...siAnthropic, label: 'Claude Code', monochrome: true },\n 'github-copilot': { ...siGithub, label: 'GitHub Copilot', monochrome: true },\n 'google-gemini': { ...siGooglegemini, label: 'Google Gemini' },\n};\n\ninterface ToolIconProps {\n tool: FrameworkToken;\n /** Use the brand's official color. Defaults to true. */\n branded?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Icon size in pixels. Defaults to 20. */\n size?: number;\n}\n\nexport function ToolIcon({\n tool,\n branded = true,\n className,\n style,\n size = 20,\n}: ToolIconProps) {\n if (tool === 'all') {\n return (\n <RiInfinityLine\n size={size}\n className={className}\n style={{ color: 'var(--bui-fg-secondary)', ...style }}\n aria-label=\"Universal\"\n />\n );\n }\n\n if (tool === 'cursor') {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{\n color: branded ? 'var(--bui-fg-primary)' : 'inherit',\n ...style,\n }}\n aria-label=\"Cursor\"\n role=\"img\"\n >\n <path d={CURSOR_PATH} />\n </svg>\n );\n }\n\n const cfg = TOOL_ICON[tool as SvgTool];\n if (!cfg) return null;\n\n const brandedColor = cfg.monochrome ? 'var(--bui-fg-primary)' : `#${cfg.hex}`;\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{ color: branded ? brandedColor : 'inherit', ...style }}\n aria-label={cfg.label}\n role=\"img\"\n >\n <path d={cfg.path} />\n </svg>\n );\n}\n"],"names":[],"mappings":";;;;AAOA,MAAM,WAAA,GACJ,uTAAA;AAYF,MAAM,SAAA,GAGF;AAAA,EACF,eAAe,EAAE,GAAG,aAAa,KAAA,EAAO,aAAA,EAAe,YAAY,IAAA,EAAK;AAAA,EACxE,kBAAkB,EAAE,GAAG,UAAU,KAAA,EAAO,gBAAA,EAAkB,YAAY,IAAA,EAAK;AAAA,EAC3E,eAAA,EAAiB,EAAE,GAAG,cAAA,EAAgB,OAAO,eAAA;AAC/C,CAAA;AAYO,SAAS,QAAA,CAAS;AAAA,EACvB,IAAA;AAAA,EACA,OAAA,GAAU,IAAA;AAAA,EACV,SAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA,GAAO;AACT,CAAA,EAAkB;AAChB,EAAA,IAAI,SAAS,KAAA,EAAO;AAClB,IAAA,uBACE,GAAA;AAAA,MAAC,cAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,SAAA;AAAA,QACA,KAAA,EAAO,EAAE,KAAA,EAAO,yBAAA,EAA2B,GAAG,KAAA,EAAM;AAAA,QACpD,YAAA,EAAW;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"ToolIcon.esm.js","sources":["../../../src/components/ToolIcon/ToolIcon.tsx"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { RiInfinityLine } from '@remixicon/react';\nimport { siAnthropic, siGithub, siGooglegemini } from 'simple-icons';\nimport type { FrameworkToken } from '@nospt/plugin-dev-ai-hub-common';\n\n/* Cursor's cube mark isn't in simple-icons; path from Boxicons Brands\n (`bxl:cursor-ai`, MIT). Monochrome brand — no branded hex. */\nconst CURSOR_PATH =\n 'm20.42 6.73l-8-4.62a.82.82 0 0 0-.83 0L3.58 6.73c-.22.12-.35.36-.35.61v9.32c0 .25.13.48.35.61l8.01 4.62c.26.15.57.15.83 0l8.01-4.62c.22-.12.35-.36.35-.61V7.34c0-.25-.13-.48-.35-.61Zm-.5.98L12.19 21.1c-.05.09-.19.05-.19-.05v-8.77c0-.18-.09-.34-.25-.43L4.16 7.47c-.09-.05-.05-.19.05-.19h15.46c.22 0 .36.24.25.43';\n\ntype SvgTool = Exclude<FrameworkToken, 'all' | 'cursor'>;\n\n/**\n * Anthropic's and GitHub's marks are monochrome brand assets (near-black,\n * `#191919` / `#181717`) meant to be manually inverted per surface — used\n * literally, they're invisible on the dark theme's dark backgrounds. Treat\n * them like `cursor` below (theme-aware `--bui-fg-primary`) instead of their\n * literal hex. Google's mark is an actual mid-tone brand colour (`#8E75B2`)\n * that reads fine on both themes, so it keeps its literal hex.\n */\nconst TOOL_ICON: Record<\n SvgTool,\n { path: string; hex: string; label: string; monochrome?: boolean }\n> = {\n 'claude-code': { ...siAnthropic, label: 'Claude Code', monochrome: true },\n 'github-copilot': { ...siGithub, label: 'GitHub Copilot', monochrome: true },\n 'google-gemini': { ...siGooglegemini, label: 'Google Gemini' },\n};\n\ninterface ToolIconProps {\n tool: FrameworkToken;\n /** Use the brand's official color. Defaults to true. */\n branded?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Icon size in pixels. Defaults to 20. */\n size?: number;\n}\n\nexport function ToolIcon({\n tool,\n branded = true,\n className,\n style,\n size = 20,\n}: ToolIconProps) {\n if (tool === 'all') {\n return (\n <RiInfinityLine\n size={size}\n className={className}\n style={{ color: 'var(--bui-fg-secondary)', ...style }}\n aria-label=\"Universal\"\n // Without an explicit role the label is not exposed as an image name,\n // which the other two branches already set. It matters wherever the\n // icon stands alone (resource cards), where it is the only thing\n // naming the tool.\n role=\"img\"\n />\n );\n }\n\n if (tool === 'cursor') {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{\n color: branded ? 'var(--bui-fg-primary)' : 'inherit',\n ...style,\n }}\n aria-label=\"Cursor\"\n role=\"img\"\n >\n <path d={CURSOR_PATH} />\n </svg>\n );\n }\n\n const cfg = TOOL_ICON[tool as SvgTool];\n if (!cfg) return null;\n\n const brandedColor = cfg.monochrome ? 'var(--bui-fg-primary)' : `#${cfg.hex}`;\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n fill=\"currentColor\"\n className={className}\n style={{ color: branded ? brandedColor : 'inherit', ...style }}\n aria-label={cfg.label}\n role=\"img\"\n >\n <path d={cfg.path} />\n </svg>\n );\n}\n"],"names":[],"mappings":";;;;AAOA,MAAM,WAAA,GACJ,uTAAA;AAYF,MAAM,SAAA,GAGF;AAAA,EACF,eAAe,EAAE,GAAG,aAAa,KAAA,EAAO,aAAA,EAAe,YAAY,IAAA,EAAK;AAAA,EACxE,kBAAkB,EAAE,GAAG,UAAU,KAAA,EAAO,gBAAA,EAAkB,YAAY,IAAA,EAAK;AAAA,EAC3E,eAAA,EAAiB,EAAE,GAAG,cAAA,EAAgB,OAAO,eAAA;AAC/C,CAAA;AAYO,SAAS,QAAA,CAAS;AAAA,EACvB,IAAA;AAAA,EACA,OAAA,GAAU,IAAA;AAAA,EACV,SAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA,GAAO;AACT,CAAA,EAAkB;AAChB,EAAA,IAAI,SAAS,KAAA,EAAO;AAClB,IAAA,uBACE,GAAA;AAAA,MAAC,cAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,SAAA;AAAA,QACA,KAAA,EAAO,EAAE,KAAA,EAAO,yBAAA,EAA2B,GAAG,KAAA,EAAM;AAAA,QACpD,YAAA,EAAW,WAAA;AAAA,QAKX,IAAA,EAAK;AAAA;AAAA,KACP;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,uBACE,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAM,4BAAA;AAAA,QACN,OAAA,EAAQ,WAAA;AAAA,QACR,KAAA,EAAO,IAAA;AAAA,QACP,MAAA,EAAQ,IAAA;AAAA,QACR,IAAA,EAAK,cAAA;AAAA,QACL,SAAA;AAAA,QACA,KAAA,EAAO;AAAA,UACL,KAAA,EAAO,UAAU,uBAAA,GAA0B,SAAA;AAAA,UAC3C,GAAG;AAAA,SACL;AAAA,QACA,YAAA,EAAW,QAAA;AAAA,QACX,IAAA,EAAK,KAAA;AAAA,QAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,WAAA,EAAa;AAAA;AAAA,KACxB;AAAA,EAEJ;AAEA,EAAA,MAAM,GAAA,GAAM,UAAU,IAAe,CAAA;AACrC,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,MAAM,eAAe,GAAA,CAAI,UAAA,GAAa,uBAAA,GAA0B,CAAA,CAAA,EAAI,IAAI,GAAG,CAAA,CAAA;AAE3E,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAM,4BAAA;AAAA,MACN,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAO,IAAA;AAAA,MACP,MAAA,EAAQ,IAAA;AAAA,MACR,IAAA,EAAK,cAAA;AAAA,MACL,SAAA;AAAA,MACA,OAAO,EAAE,KAAA,EAAO,UAAU,YAAA,GAAe,SAAA,EAAW,GAAG,KAAA,EAAM;AAAA,MAC7D,cAAY,GAAA,CAAI,KAAA;AAAA,MAChB,IAAA,EAAK,KAAA;AAAA,MAEL,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAG,GAAA,CAAI,IAAA,EAAM;AAAA;AAAA,GACrB;AAEJ;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nospt/plugin-dev-ai-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc1",
|
|
4
4
|
"description": "Frontend plugin for Dev AI Hub",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"pluginPackages": [
|
|
34
34
|
"@nospt/plugin-dev-ai-hub",
|
|
35
35
|
"@nospt/plugin-dev-ai-hub-backend",
|
|
36
|
-
"@nospt/plugin-dev-ai-hub-common"
|
|
37
|
-
"@nospt/plugin-dev-ai-hub-node"
|
|
36
|
+
"@nospt/plugin-dev-ai-hub-common"
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
39
|
"sideEffects": false,
|
|
41
40
|
"files": [
|
|
42
|
-
"dist"
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
45
|
"start": "backstage-cli package start --config ../../app-config.dev.yaml",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@backstage/core-plugin-api": "^1.12.6",
|
|
60
60
|
"@backstage/frontend-plugin-api": "^0.17.0",
|
|
61
61
|
"@backstage/ui": "^0.15.0",
|
|
62
|
-
"@nospt/plugin-dev-ai-hub-common": "^0.
|
|
62
|
+
"@nospt/plugin-dev-ai-hub-common": "^1.0.0-rc1",
|
|
63
63
|
"@remixicon/react": "^4.6.0",
|
|
64
64
|
"react-markdown": "^9.0.0",
|
|
65
65
|
"react-syntax-highlighter": "^16.1.1",
|
|
@@ -86,12 +86,20 @@
|
|
|
86
86
|
"jsdom": "^27.1.0",
|
|
87
87
|
"react": "^18.0.0",
|
|
88
88
|
"react-dom": "^18.0.0",
|
|
89
|
-
"react-router-dom": "^6.30.2"
|
|
89
|
+
"react-router-dom": "^6.30.2",
|
|
90
|
+
"yaml": "^2.9.0"
|
|
90
91
|
},
|
|
91
92
|
"peerDependencies": {
|
|
92
93
|
"react": "^18.0.0",
|
|
93
94
|
"react-dom": "^18.0.0",
|
|
94
95
|
"react-router-dom": "^6.0.0"
|
|
95
96
|
},
|
|
97
|
+
"jest": {
|
|
98
|
+
"coveragePathIgnorePatterns": [
|
|
99
|
+
"/node_modules/",
|
|
100
|
+
"plugin\\.tsx$",
|
|
101
|
+
"routes\\.ts$"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
96
104
|
"module": "./dist/index.esm.js"
|
|
97
105
|
}
|