@nospt/plugin-dev-ai-hub 0.1.0 โ†’ 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dev/index.tsx +17 -217
  2. package/dist/api/DevAiHubClient.esm.js +2 -0
  3. package/dist/api/DevAiHubClient.esm.js.map +1 -1
  4. package/dist/components/AssetCard/AssetCard.esm.js +60 -134
  5. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -1
  6. package/dist/components/AssetCard/AssetCard.module.css.esm.js +8 -0
  7. package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +1 -0
  8. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +175 -281
  9. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -1
  10. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +8 -0
  11. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +1 -0
  12. package/dist/components/AssetFilters/AssetFilters.esm.js +94 -134
  13. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -1
  14. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +8 -0
  15. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +1 -0
  16. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +70 -126
  17. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -1
  18. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +8 -0
  19. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +1 -0
  20. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +64 -189
  21. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -1
  22. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +8 -0
  23. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +1 -0
  24. package/dist/components/McpConfigPage/McpConfigPage.esm.js +200 -0
  25. package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +1 -0
  26. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +8 -0
  27. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +1 -0
  28. package/dist/components/McpConfigPage/index.esm.js +6 -0
  29. package/dist/components/McpConfigPage/index.esm.js.map +1 -0
  30. package/dist/components/ToolIcon/ToolIcon.esm.js +25 -13
  31. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  32. package/dist/index.d.ts +10 -5
  33. package/dist/index.esm.js +1 -1
  34. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  35. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  36. package/dist/plugin.esm.js +22 -4
  37. package/dist/plugin.esm.js.map +1 -1
  38. package/dist/routes.esm.js +2 -1
  39. package/dist/routes.esm.js.map +1 -1
  40. package/package.json +24 -14
  41. package/package.json-prepack +24 -14
  42. package/src/components/AssetCard/AssetCard.module.css +153 -0
  43. package/src/components/AssetCard/AssetCard.tsx +79 -148
  44. package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +207 -0
  45. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +228 -292
  46. package/src/components/AssetFilters/AssetFilters.module.css +67 -0
  47. package/src/components/AssetFilters/AssetFilters.tsx +136 -178
  48. package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +72 -0
  49. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +95 -135
  50. package/src/components/DevAiHubPage/DevAiHubPage.module.css +105 -0
  51. package/src/components/DevAiHubPage/DevAiHubPage.tsx +73 -222
  52. package/src/components/McpConfigDialog/McpConfigDialog.module.css +111 -0
  53. package/src/components/McpConfigDialog/McpConfigDialog.tsx +178 -208
  54. package/src/components/McpConfigPage/McpConfigPage.module.css +121 -0
  55. package/src/components/McpConfigPage/McpConfigPage.tsx +242 -0
  56. package/src/components/McpConfigPage/index.ts +1 -0
  57. package/src/components/ToolIcon/ToolIcon.tsx +31 -15
  58. package/src/css-modules.d.ts +4 -0
  59. package/src/index.ts +1 -1
  60. package/src/plugin.tsx +21 -1
  61. package/src/routes.ts +1 -0
  62. package/tests/e2e/.auth.json +14 -0
  63. package/tests/e2e/asset-detail.spec.ts +90 -0
  64. package/tests/e2e/asset-filtering.spec.ts +145 -0
  65. package/tests/e2e/asset-installation.spec.ts +74 -0
  66. package/tests/e2e/dev-ai-hub-page.spec.ts +89 -0
  67. package/tests/e2e/fixtures/base.ts +61 -0
  68. package/tests/e2e/fixtures/mock-api.ts +221 -0
  69. package/tests/e2e/mcp-config.spec.ts +88 -0
  70. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +0 -274
  71. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +0 -1
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@nospt/plugin-dev-ai-hub",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Frontend plugin for Dev AI Hub",
5
5
  "license": "Apache-2.0",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/nosportugal/backstage-plugin-dev-ai-hub"
11
+ },
8
12
  "publishConfig": {
9
13
  "access": "public"
10
14
  },
@@ -37,35 +41,41 @@
37
41
  "lint": "backstage-cli package lint",
38
42
  "prepack": "backstage-cli package prepack",
39
43
  "postpack": "backstage-cli package postpack",
40
- "test": "backstage-cli package test --passWithNoTests",
44
+ "test": "backstage-cli package test --passWithNoTests --watchAll=false",
45
+ "test:e2e": "playwright test",
46
+ "test:e2e:ui": "playwright test --ui",
47
+ "test:e2e:debug": "playwright test --debug",
41
48
  "tsc": "tsc --noEmit"
42
49
  },
43
50
  "dependencies": {
44
- "@backstage/core-components": "*",
45
- "@backstage/core-plugin-api": "*",
46
- "@backstage/frontend-plugin-api": "*",
51
+ "@backstage/core-components": "^0.18.9",
52
+ "@backstage/core-plugin-api": "^1.12.5",
53
+ "@backstage/frontend-plugin-api": "^0.16.2",
54
+ "@backstage/ui": "^0.14.3",
47
55
  "@mui/icons-material": "^5.15.0",
48
56
  "@mui/material": "^5.15.0",
49
- "@nospt/plugin-dev-ai-hub-common": "*",
57
+ "@nospt/plugin-dev-ai-hub-common": "workspace:^",
58
+ "@remixicon/react": "^4.6.0",
50
59
  "react-markdown": "^9.0.0",
51
60
  "react-syntax-highlighter": "^16.1.1",
52
61
  "remark-gfm": "^4.0.1",
53
62
  "simple-icons": "^13.0.0"
54
63
  },
55
64
  "devDependencies": {
56
- "@backstage/cli": "*",
57
- "@backstage/dev-utils": "*",
58
- "@backstage/test-utils": "*",
65
+ "@backstage/cli": "^0.36.1",
66
+ "@backstage/dev-utils": "^1.1.22",
67
+ "@playwright/test": "^1.60.0",
68
+ "@testing-library/dom": "^10.0.0",
59
69
  "@testing-library/jest-dom": "^6.0.0",
60
- "@testing-library/react": "^14.0.0",
61
- "@types/jest": "^29.0.0",
70
+ "@testing-library/react": "^16.3.1",
71
+ "@types/jest": "^30.0.0",
62
72
  "@types/react": "^18.0.0",
63
73
  "@types/react-syntax-highlighter": "^15.0.0",
64
- "jest": "^29.0.0",
65
- "jest-environment-jsdom": "^29.0.0",
74
+ "jest": "^30.2.0",
75
+ "jsdom": "^27.1.0",
66
76
  "react": "^18.0.0",
67
77
  "react-dom": "^18.0.0",
68
- "react-router-dom": "^6.0.0"
78
+ "react-router-dom": "^6.30.2"
69
79
  },
70
80
  "peerDependencies": {
71
81
  "react": "^18.0.0",
@@ -0,0 +1,153 @@
1
+ @layer components {
2
+ .card {
3
+ height: 100%;
4
+ display: flex;
5
+ flex-direction: column;
6
+ border-radius: var(--bui-radius-2);
7
+ border: 1px solid var(--bui-border-1);
8
+ border-left: 3px solid var(--card-accent);
9
+ background-color: var(--bui-bg-neutral-1);
10
+ transition: all 0.18s ease;
11
+ }
12
+
13
+ .card:hover {
14
+ box-shadow: 0 6px 24px color-mix(in srgb, var(--card-accent) 19%, transparent);
15
+ border-color: var(--card-accent);
16
+ transform: translateY(-2px);
17
+ }
18
+
19
+ .cardContent {
20
+ padding: var(--bui-space-3);
21
+ padding-bottom: 0;
22
+ flex: 1;
23
+ }
24
+
25
+ .header {
26
+ display: flex;
27
+ align-items: flex-start;
28
+ gap: var(--bui-space-2);
29
+ margin-bottom: var(--bui-space-2);
30
+ }
31
+
32
+ .iconBox {
33
+ width: 36px;
34
+ height: 36px;
35
+ border-radius: var(--bui-radius-2);
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ flex-shrink: 0;
40
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--card-accent) 15%, transparent);
41
+ }
42
+
43
+ .iconImage {
44
+ width: 20px;
45
+ height: 20px;
46
+ object-fit: contain;
47
+ }
48
+
49
+ .headerInfo {
50
+ flex: 1;
51
+ min-width: 0;
52
+ }
53
+
54
+ .titleRow {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: var(--bui-space-1);
58
+ margin-bottom: 2px;
59
+ }
60
+
61
+ .title {
62
+ line-height: 1.2;
63
+ flex: 1;
64
+ overflow: hidden;
65
+ white-space: nowrap;
66
+ text-overflow: ellipsis;
67
+ }
68
+
69
+ .newBadge {
70
+ height: 16px;
71
+ flex-shrink: 0;
72
+ font-size: 0.58rem;
73
+ font-weight: 700;
74
+ background-color: #6AB04C;
75
+ color: #fff;
76
+ }
77
+
78
+ .description {
79
+ margin-bottom: var(--bui-space-2);
80
+ display: -webkit-box;
81
+ -webkit-line-clamp: 2;
82
+ -webkit-box-orient: vertical;
83
+ overflow: hidden;
84
+ line-height: 1.4;
85
+ }
86
+
87
+ .toolsRow :global(.bui-TagList) {
88
+ display: flex;
89
+ gap: var(--bui-space-1);
90
+ flex-wrap: wrap;
91
+ }
92
+
93
+ .toolsRowWithTags {
94
+ margin-bottom: 6px;
95
+ }
96
+
97
+ .toolChip {
98
+ height: 18px;
99
+ font-size: 0.65rem;
100
+ font-weight: 600;
101
+ background-color: var(--bui-bg-neutral-1-hover);
102
+ color: var(--bui-fg-secondary);
103
+ }
104
+
105
+ .tagsRow :global(.bui-TagList) {
106
+ display: flex;
107
+ gap: var(--bui-space-1);
108
+ flex-wrap: wrap;
109
+ }
110
+
111
+ .tagChip {
112
+ height: 16px;
113
+ font-size: 0.6rem;
114
+ color: var(--bui-fg-secondary);
115
+ background-color: transparent;
116
+ border: 1px solid var(--bui-border-1);
117
+ }
118
+
119
+ .cardActions {
120
+ padding: var(--bui-space-2) var(--bui-space-3);
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ margin-top: auto;
125
+ border-top: 1px solid var(--bui-border-1);
126
+ }
127
+
128
+ .metaRow {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: var(--bui-space-2);
132
+ }
133
+
134
+ .metaText {
135
+ font-size: 0.65rem;
136
+ }
137
+
138
+ .installCountRow {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 2px;
142
+ }
143
+
144
+ .installEmoji {
145
+ font-size: 0.65rem;
146
+ line-height: 1;
147
+ }
148
+
149
+ .actionsRow {
150
+ display: flex;
151
+ gap: 2px;
152
+ }
153
+ }
@@ -1,20 +1,10 @@
1
1
  import type { ElementType } from 'react';
2
- import Box from '@mui/material/Box';
3
- import Card from '@mui/material/Card';
4
- import CardActions from '@mui/material/CardActions';
5
- import CardContent from '@mui/material/CardContent';
6
- import Chip from '@mui/material/Chip';
7
- import IconButton from '@mui/material/IconButton';
8
- import Tooltip from '@mui/material/Tooltip';
9
- import Typography from '@mui/material/Typography';
10
- import OpenInNewIcon from '@mui/icons-material/OpenInNew';
11
- import DownloadIcon from '@mui/icons-material/Download';
12
- import ArticleIcon from '@mui/icons-material/Article';
13
- import SmartToyIcon from '@mui/icons-material/SmartToy';
14
- import BuildIcon from '@mui/icons-material/Build';
15
- import AccountTreeIcon from '@mui/icons-material/AccountTree';
2
+ import { Box, Flex, Text, Card, CardBody, CardFooter, Tag, TagGroup, ButtonIcon, Tooltip, TooltipTrigger } from '@backstage/ui';
3
+ import { RiDownloadLine, RiExternalLinkLine } from '@remixicon/react';
4
+ import { RiArticleLine, RiRobot2Line, RiToolsLine, RiGitBranchLine } from '@remixicon/react';
16
5
  import type { AiAssetSummary, AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
17
6
  import { ToolIcon } from '../ToolIcon';
7
+ import styles from './AssetCard.module.css';
18
8
 
19
9
  const POPULAR_THRESHOLD = 5;
20
10
  const NEW_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
@@ -28,10 +18,10 @@ const TOOL_LABELS: Record<AiTool, string> = {
28
18
  };
29
19
 
30
20
  const TYPE_CONFIG: Record<AssetType, { label: string; color: string; bg: string; Icon: ElementType }> = {
31
- instruction: { label: 'Instruction', color: '#2563EB', bg: '#EFF6FF', Icon: ArticleIcon },
32
- agent: { label: 'Agent', color: '#7C3AED', bg: '#F5F3FF', Icon: SmartToyIcon },
33
- skill: { label: 'Skill', color: '#059669', bg: '#ECFDF5', Icon: BuildIcon },
34
- workflow: { label: 'Workflow', color: '#D97706', bg: '#FFFBEB', Icon: AccountTreeIcon },
21
+ instruction: { label: 'Instruction', color: '#54A0FF', bg: 'rgba(84, 160, 255, 0.15)', Icon: RiArticleLine },
22
+ agent: { label: 'Agent', color: '#FF6B9D', bg: 'rgba(255, 107, 157, 0.15)', Icon: RiRobot2Line },
23
+ skill: { label: 'Skill', color: '#6AB04C', bg: 'rgba(106, 176, 76, 0.15)', Icon: RiToolsLine },
24
+ workflow: { label: 'Workflow', color: '#F9CA24', bg: 'rgba(249, 202, 36, 0.15)', Icon: RiGitBranchLine },
35
25
  };
36
26
 
37
27
  interface AssetCardProps {
@@ -48,174 +38,115 @@ export function AssetCard({ asset, onView, onInstall }: AssetCardProps) {
48
38
 
49
39
  return (
50
40
  <Card
51
- variant="outlined"
52
- sx={{
53
- height: '100%',
54
- display: 'flex',
55
- flexDirection: 'column',
56
- borderRadius: 2,
57
- border: '1px solid',
58
- borderColor: 'divider',
59
- borderLeft: `3px solid ${cfg.color}`,
60
- transition: 'all 0.18s ease',
61
- '&:hover': {
62
- boxShadow: `0 6px 24px ${cfg.color}30`,
63
- borderColor: cfg.color,
64
- transform: 'translateY(-2px)',
65
- },
66
- }}
41
+ className={styles.card}
42
+ style={{ '--card-accent': cfg.color } as React.CSSProperties}
67
43
  >
68
- <CardContent sx={{ p: 1.5, pb: '0 !important', flex: 1 }}>
44
+ <CardBody className={styles.cardContent}>
69
45
  {/* Header */}
70
- <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 1 }}>
46
+ <Flex className={styles.header}>
71
47
  <Box
72
- sx={{
73
- width: 36,
74
- height: 36,
75
- borderRadius: 1.5,
76
- backgroundColor: cfg.bg,
77
- display: 'flex',
78
- alignItems: 'center',
79
- justifyContent: 'center',
80
- flexShrink: 0,
81
- boxShadow: `0 2px 8px ${cfg.color}25`,
82
- }}
48
+ className={styles.iconBox}
49
+ style={{ backgroundColor: cfg.bg }}
83
50
  >
84
51
  {asset.icon ? (
85
- <Box
86
- component="img"
52
+ <img
87
53
  src={asset.icon}
88
54
  alt={asset.label ?? asset.name}
89
- sx={{ width: 20, height: 20, objectFit: 'contain' }}
55
+ className={styles.iconImage}
90
56
  onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
91
57
  />
92
58
  ) : (
93
- <TypeIcon sx={{ color: cfg.color, fontSize: '1.1rem' }} />
59
+ <TypeIcon size={18} style={{ color: cfg.color }} />
94
60
  )}
95
61
  </Box>
96
62
 
97
- <Box sx={{ flex: 1, minWidth: 0 }}>
98
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.2 }}>
99
- <Typography variant="body2" fontWeight={700} noWrap title={asset.label ?? asset.name} sx={{ lineHeight: 1.2, flex: 1 }}>
63
+ <Box className={styles.headerInfo}>
64
+ <Flex className={styles.titleRow}>
65
+ <Text variant="body-medium" weight="bold" className={styles.title} title={asset.label ?? asset.name}>
100
66
  {asset.label ?? asset.name}
101
- </Typography>
67
+ </Text>
102
68
  {isNew && (
103
- <Chip
104
- label="New"
105
- size="small"
106
- sx={{
107
- height: 16,
108
- fontSize: '0.58rem',
109
- fontWeight: 700,
110
- backgroundColor: '#059669',
111
- color: '#fff',
112
- borderRadius: 1,
113
- flexShrink: 0,
114
- '& .MuiChip-label': { px: '5px' },
115
- }}
116
- />
69
+ <TagGroup aria-label="Status">
70
+ <Tag id="new" size="small" className={styles.newBadge}>New</Tag>
71
+ </TagGroup>
117
72
  )}
118
- </Box>
119
- <Typography variant="caption" sx={{ color: cfg.color, fontWeight: 600 }}>
73
+ </Flex>
74
+ <Text variant="body-x-small" style={{ color: cfg.color, fontWeight: 600 }}>
120
75
  {cfg.label}
121
- </Typography>
76
+ </Text>
122
77
  </Box>
123
- </Box>
78
+ </Flex>
124
79
 
125
80
  {/* Description */}
126
- <Typography
127
- variant="caption"
128
- color="text.secondary"
129
- sx={{
130
- mb: 1,
131
- display: '-webkit-box',
132
- WebkitLineClamp: 2,
133
- WebkitBoxOrient: 'vertical',
134
- overflow: 'hidden',
135
- lineHeight: 1.4,
136
- }}
137
- >
81
+ <Text variant="body-x-small" color="secondary" className={styles.description}>
138
82
  {asset.description}
139
- </Typography>
83
+ </Text>
140
84
 
141
85
  {/* Tools */}
142
- <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: asset.tags.length > 0 ? 0.75 : 0 }}>
86
+ <TagGroup aria-label="Compatible tools" className={`${styles.toolsRow} ${asset.tags.length > 0 ? styles.toolsRowWithTags : ''}`}>
143
87
  {asset.tools.map(tool => (
144
- <Chip
145
- key={tool}
146
- icon={<ToolIcon tool={tool as AiTool} sx={{ fontSize: '0.75rem !important' }} />}
147
- label={TOOL_LABELS[tool as AiTool] ?? tool}
148
- size="small"
149
- sx={{
150
- height: 18,
151
- fontSize: '0.65rem',
152
- fontWeight: 600,
153
- backgroundColor: 'action.hover',
154
- color: 'text.secondary',
155
- borderRadius: 1,
156
- '& .MuiChip-icon': { ml: '4px' },
157
- }}
158
- />
88
+ <Tag key={tool} id={tool} size="small" className={styles.toolChip} icon={<ToolIcon tool={tool as AiTool} size={12} />}>
89
+ {TOOL_LABELS[tool as AiTool] ?? tool}
90
+ </Tag>
159
91
  ))}
160
- </Box>
92
+ </TagGroup>
161
93
 
162
94
  {/* Tags */}
163
95
  {asset.tags.length > 0 && (
164
- <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
165
- {asset.tags.slice(0, 3).map(tag => (
166
- <Chip
167
- key={tag}
168
- label={`#${tag}`}
169
- size="small"
170
- sx={{
171
- height: 16,
172
- fontSize: '0.6rem',
173
- color: 'text.disabled',
174
- backgroundColor: 'transparent',
175
- border: '1px solid',
176
- borderColor: 'divider',
177
- borderRadius: 1,
178
- }}
179
- />
180
- ))}
96
+ <>
97
+ <TagGroup aria-label="Tags" className={styles.tagsRow}>
98
+ {asset.tags.slice(0, 3).map(tag => (
99
+ <Tag key={tag} id={tag} size="small" className={styles.tagChip}>
100
+ #{tag}
101
+ </Tag>
102
+ ))}
103
+ </TagGroup>
181
104
  {asset.tags.length > 3 && (
182
- <Typography variant="caption" color="text.disabled" sx={{ alignSelf: 'center' }}>
105
+ <Text variant="body-x-small" color="secondary" style={{ alignSelf: 'center' }}>
183
106
  +{asset.tags.length - 3}
184
- </Typography>
107
+ </Text>
185
108
  )}
186
- </Box>
109
+ </>
187
110
  )}
188
- </CardContent>
111
+ </CardBody>
189
112
 
190
- <CardActions sx={{ px: 1.5, py: 1, justifyContent: 'space-between', mt: 'auto', borderTop: '1px solid', borderColor: 'divider' }}>
191
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
192
- <Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.65rem' }}>
113
+ <CardFooter className={styles.cardActions}>
114
+ <Flex className={styles.metaRow}>
115
+ <Text variant="body-x-small" color="secondary" className={styles.metaText}>
193
116
  v{asset.version} ยท {asset.author}
194
- </Typography>
117
+ </Text>
195
118
  {asset.installCount > 0 && (
196
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
197
- <Typography sx={{ fontSize: '0.65rem', lineHeight: 1 }}>
119
+ <Flex className={styles.installCountRow}>
120
+ <Text className={styles.installEmoji}>
198
121
  {isPopular ? '๐Ÿ”ฅ' : 'โ†“'}
199
- </Typography>
200
- <Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.65rem' }}>
122
+ </Text>
123
+ <Text variant="body-x-small" color="secondary" className={styles.metaText}>
201
124
  {asset.installCount}
202
- </Typography>
203
- </Box>
125
+ </Text>
126
+ </Flex>
204
127
  )}
205
- </Box>
206
- <Box sx={{ display: 'flex', gap: 0.25 }}>
207
- <Tooltip title="Install in editor">
208
- <IconButton size="small" onClick={() => onInstall(asset.id)} color="primary">
209
- <DownloadIcon fontSize="small" />
210
- </IconButton>
211
- </Tooltip>
212
- <Tooltip title="View details">
213
- <IconButton size="small" onClick={() => onView(asset.id)}>
214
- <OpenInNewIcon fontSize="small" />
215
- </IconButton>
216
- </Tooltip>
217
- </Box>
218
- </CardActions>
128
+ </Flex>
129
+ <Flex className={styles.actionsRow}>
130
+ <TooltipTrigger>
131
+ <ButtonIcon
132
+ aria-label="Install in editor"
133
+ icon={<RiDownloadLine size={16} />}
134
+ variant="tertiary"
135
+ onPress={() => onInstall(asset.id)}
136
+ />
137
+ <Tooltip>Install in editor</Tooltip>
138
+ </TooltipTrigger>
139
+ <TooltipTrigger>
140
+ <ButtonIcon
141
+ aria-label="View details"
142
+ icon={<RiExternalLinkLine size={16} />}
143
+ variant="tertiary"
144
+ onPress={() => onView(asset.id)}
145
+ />
146
+ <Tooltip>View details</Tooltip>
147
+ </TooltipTrigger>
148
+ </Flex>
149
+ </CardFooter>
219
150
  </Card>
220
151
  );
221
152
  }