@nospt/plugin-dev-ai-hub 0.1.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 (55) hide show
  1. package/.eslintrc.js +1 -0
  2. package/dev/index.tsx +232 -0
  3. package/dist/api/DevAiHubClient.esm.js +76 -0
  4. package/dist/api/DevAiHubClient.esm.js.map +1 -0
  5. package/dist/components/AssetCard/AssetCard.esm.js +194 -0
  6. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -0
  7. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +324 -0
  8. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -0
  9. package/dist/components/AssetFilters/AssetFilters.esm.js +216 -0
  10. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -0
  11. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +194 -0
  12. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -0
  13. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +320 -0
  14. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -0
  15. package/dist/components/DevAiHubPage/index.esm.js +6 -0
  16. package/dist/components/DevAiHubPage/index.esm.js.map +1 -0
  17. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +274 -0
  18. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +1 -0
  19. package/dist/components/ToolIcon/ToolIcon.esm.js +41 -0
  20. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -0
  21. package/dist/hooks/index.esm.js +114 -0
  22. package/dist/hooks/index.esm.js.map +1 -0
  23. package/dist/index.d.ts +104 -0
  24. package/dist/index.esm.js +5 -0
  25. package/dist/index.esm.js.map +1 -0
  26. package/dist/plugin.esm.js +40 -0
  27. package/dist/plugin.esm.js.map +1 -0
  28. package/dist/pluginLegacy.esm.js +27 -0
  29. package/dist/pluginLegacy.esm.js.map +1 -0
  30. package/dist/routes.esm.js +6 -0
  31. package/dist/routes.esm.js.map +1 -0
  32. package/package.json +80 -0
  33. package/package.json-prepack +75 -0
  34. package/src/api/DevAiHubClient.ts +110 -0
  35. package/src/components/AssetCard/AssetCard.test.tsx +182 -0
  36. package/src/components/AssetCard/AssetCard.tsx +221 -0
  37. package/src/components/AssetCard/index.ts +1 -0
  38. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +377 -0
  39. package/src/components/AssetDetailPanel/index.ts +1 -0
  40. package/src/components/AssetFilters/AssetFilters.tsx +261 -0
  41. package/src/components/AssetFilters/index.ts +2 -0
  42. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +216 -0
  43. package/src/components/AssetInstallDialog/index.ts +1 -0
  44. package/src/components/DevAiHubPage/DevAiHubPage.tsx +346 -0
  45. package/src/components/DevAiHubPage/index.ts +1 -0
  46. package/src/components/McpConfigDialog/McpConfigDialog.tsx +301 -0
  47. package/src/components/McpConfigDialog/index.ts +1 -0
  48. package/src/components/ToolIcon/ToolIcon.tsx +49 -0
  49. package/src/components/ToolIcon/index.ts +1 -0
  50. package/src/hooks/index.ts +152 -0
  51. package/src/index.ts +10 -0
  52. package/src/plugin.tsx +47 -0
  53. package/src/pluginLegacy.ts +42 -0
  54. package/src/routes.ts +3 -0
  55. package/tsconfig.json +10 -0
@@ -0,0 +1,221 @@
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';
16
+ import type { AiAssetSummary, AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
17
+ import { ToolIcon } from '../ToolIcon';
18
+
19
+ const POPULAR_THRESHOLD = 5;
20
+ const NEW_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
21
+
22
+ const TOOL_LABELS: Record<AiTool, string> = {
23
+ 'all': 'Universal',
24
+ 'claude-code': 'Claude Code',
25
+ 'github-copilot': 'GitHub Copilot',
26
+ 'google-gemini': 'Google Gemini',
27
+ 'cursor': 'Cursor',
28
+ };
29
+
30
+ 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 },
35
+ };
36
+
37
+ interface AssetCardProps {
38
+ asset: AiAssetSummary;
39
+ onView: (id: string) => void;
40
+ onInstall: (id: string) => void;
41
+ }
42
+
43
+ export function AssetCard({ asset, onView, onInstall }: AssetCardProps) {
44
+ const cfg = TYPE_CONFIG[asset.type];
45
+ const TypeIcon = cfg.Icon;
46
+ const isPopular = asset.installCount >= POPULAR_THRESHOLD;
47
+ const isNew = Date.now() - new Date(asset.updatedAt).getTime() < NEW_DAYS_MS;
48
+
49
+ return (
50
+ <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
+ }}
67
+ >
68
+ <CardContent sx={{ p: 1.5, pb: '0 !important', flex: 1 }}>
69
+ {/* Header */}
70
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 1 }}>
71
+ <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
+ }}
83
+ >
84
+ {asset.icon ? (
85
+ <Box
86
+ component="img"
87
+ src={asset.icon}
88
+ alt={asset.label ?? asset.name}
89
+ sx={{ width: 20, height: 20, objectFit: 'contain' }}
90
+ onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
91
+ />
92
+ ) : (
93
+ <TypeIcon sx={{ color: cfg.color, fontSize: '1.1rem' }} />
94
+ )}
95
+ </Box>
96
+
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 }}>
100
+ {asset.label ?? asset.name}
101
+ </Typography>
102
+ {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
+ />
117
+ )}
118
+ </Box>
119
+ <Typography variant="caption" sx={{ color: cfg.color, fontWeight: 600 }}>
120
+ {cfg.label}
121
+ </Typography>
122
+ </Box>
123
+ </Box>
124
+
125
+ {/* 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
+ >
138
+ {asset.description}
139
+ </Typography>
140
+
141
+ {/* Tools */}
142
+ <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: asset.tags.length > 0 ? 0.75 : 0 }}>
143
+ {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
+ />
159
+ ))}
160
+ </Box>
161
+
162
+ {/* Tags */}
163
+ {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
+ ))}
181
+ {asset.tags.length > 3 && (
182
+ <Typography variant="caption" color="text.disabled" sx={{ alignSelf: 'center' }}>
183
+ +{asset.tags.length - 3}
184
+ </Typography>
185
+ )}
186
+ </Box>
187
+ )}
188
+ </CardContent>
189
+
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' }}>
193
+ v{asset.version} · {asset.author}
194
+ </Typography>
195
+ {asset.installCount > 0 && (
196
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
197
+ <Typography sx={{ fontSize: '0.65rem', lineHeight: 1 }}>
198
+ {isPopular ? '🔥' : '↓'}
199
+ </Typography>
200
+ <Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.65rem' }}>
201
+ {asset.installCount}
202
+ </Typography>
203
+ </Box>
204
+ )}
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>
219
+ </Card>
220
+ );
221
+ }
@@ -0,0 +1 @@
1
+ export { AssetCard } from './AssetCard';
@@ -0,0 +1,377 @@
1
+ import { useState } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import Alert from '@mui/material/Alert';
5
+ import Box from '@mui/material/Box';
6
+ import Button from '@mui/material/Button';
7
+ import Chip from '@mui/material/Chip';
8
+ import CircularProgress from '@mui/material/CircularProgress';
9
+ import Divider from '@mui/material/Divider';
10
+ import Drawer from '@mui/material/Drawer';
11
+ import IconButton from '@mui/material/IconButton';
12
+ import Link from '@mui/material/Link';
13
+ import Snackbar from '@mui/material/Snackbar';
14
+ import Tab from '@mui/material/Tab';
15
+ import Tabs from '@mui/material/Tabs';
16
+ import Typography from '@mui/material/Typography';
17
+ import CloseIcon from '@mui/icons-material/Close';
18
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
19
+ import FolderZipIcon from '@mui/icons-material/FolderZip';
20
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
21
+ import type { AssetType } from '@nospt/plugin-dev-ai-hub-common';
22
+ import { useAssetDetail } from '../../hooks';
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ const SyntaxHighlighter = require('react-syntax-highlighter/dist/esm/prism').default;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const { oneLight } = require('react-syntax-highlighter/dist/esm/styles/prism');
28
+
29
+ const TYPE_COLORS: Record<AssetType, string> = {
30
+ instruction: '#1976d2',
31
+ agent: '#7b1fa2',
32
+ skill: '#388e3c',
33
+ workflow: '#f57c00',
34
+ };
35
+
36
+ interface AssetDetailPanelProps {
37
+ assetId: string | null;
38
+ onClose: () => void;
39
+ }
40
+
41
+ export function AssetDetailPanel({ assetId, onClose }: AssetDetailPanelProps) {
42
+ const [tab, setTab] = useState(0);
43
+ const [snackbar, setSnackbar] = useState<string | null>(null);
44
+ const { asset, loading } = useAssetDetail(assetId);
45
+
46
+ const handleCopy = () => {
47
+ if (!asset) return;
48
+ navigator.clipboard.writeText(asset.content).then(() =>
49
+ setSnackbar('Markdown copied to clipboard!'),
50
+ );
51
+ };
52
+
53
+ return (
54
+ <>
55
+ <Drawer
56
+ anchor="right"
57
+ open={!!assetId}
58
+ onClose={onClose}
59
+ PaperProps={{ sx: { width: { xs: '100vw', md: 640 } } }}
60
+ >
61
+ <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
62
+ {/* Header */}
63
+ <Box
64
+ sx={{
65
+ p: 2,
66
+ display: 'flex',
67
+ alignItems: 'flex-start',
68
+ gap: 2,
69
+ borderBottom: 1,
70
+ borderColor: 'divider',
71
+ }}
72
+ >
73
+ <Box sx={{ flex: 1 }}>
74
+ {loading || !asset ? (
75
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
76
+ <CircularProgress size={16} />
77
+ <Typography variant="body2" color="text.secondary">
78
+ Loading...
79
+ </Typography>
80
+ </Box>
81
+ ) : (
82
+ <>
83
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
84
+ <Typography variant="h6" fontWeight={700}>
85
+ {asset.name}
86
+ </Typography>
87
+ <Chip
88
+ label={asset.type}
89
+ size="small"
90
+ sx={{
91
+ backgroundColor: `${TYPE_COLORS[asset.type]}20`,
92
+ color: TYPE_COLORS[asset.type],
93
+ fontWeight: 600,
94
+ }}
95
+ />
96
+ </Box>
97
+ <Typography variant="body2" color="text.secondary">
98
+ {asset.description}
99
+ </Typography>
100
+ </>
101
+ )}
102
+ </Box>
103
+ <IconButton onClick={onClose} size="small">
104
+ <CloseIcon />
105
+ </IconButton>
106
+ </Box>
107
+
108
+ {/* Tabs */}
109
+ <Tabs
110
+ value={tab}
111
+ onChange={(_, v) => setTab(v)}
112
+ sx={{ borderBottom: 1, borderColor: 'divider', px: 2 }}
113
+ >
114
+ <Tab label="Preview" />
115
+ <Tab label="Metadata" />
116
+ <Tab label="Raw YAML" />
117
+ </Tabs>
118
+
119
+ {/* Content */}
120
+ <Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
121
+ {loading && (
122
+ <Box sx={{ display: 'flex', justifyContent: 'center', pt: 4 }}>
123
+ <CircularProgress />
124
+ </Box>
125
+ )}
126
+
127
+ {!loading && asset && (
128
+ <>
129
+ {tab === 0 && (
130
+ <Box
131
+ sx={{
132
+ '& h1,h2,h3,h4,h5,h6': { mt: 2, mb: 1, fontWeight: 700 },
133
+ '& p': { mb: 1, lineHeight: 1.7 },
134
+ '& ul, & ol': { pl: 2.5, mb: 1 },
135
+ '& li': { mb: 0.5 },
136
+ '& blockquote': {
137
+ borderLeft: '3px solid',
138
+ borderColor: 'primary.main',
139
+ pl: 1.5,
140
+ color: 'text.secondary',
141
+ my: 1,
142
+ ml: 0,
143
+ },
144
+ '& table': { width: '100%', borderCollapse: 'collapse', mb: 1 },
145
+ '& th, & td': {
146
+ border: '1px solid',
147
+ borderColor: 'divider',
148
+ px: 1.5,
149
+ py: 0.75,
150
+ fontSize: '0.875rem',
151
+ },
152
+ '& th': { backgroundColor: 'action.hover', fontWeight: 700 },
153
+ '& code': {
154
+ bgcolor: 'action.hover',
155
+ px: 0.5,
156
+ py: 0.2,
157
+ borderRadius: 0.5,
158
+ fontFamily: 'monospace',
159
+ fontSize: '0.875em',
160
+ },
161
+ '& pre code': { bgcolor: 'transparent', px: 0, py: 0 },
162
+ }}
163
+ >
164
+ <ReactMarkdown
165
+ remarkPlugins={[remarkGfm]}
166
+ components={{
167
+ pre: ({ children }) => <Box sx={{ my: 1 }}>{children}</Box>,
168
+ code({ className, children }) {
169
+ const match = /language-(\w+)/.exec(className || '');
170
+ const code = String(children).replace(/\n$/, '');
171
+ const isBlock = code.includes('\n') || !!match;
172
+ return isBlock ? (
173
+ <SyntaxHighlighter
174
+ style={oneLight}
175
+ language={match?.[1] ?? 'text'}
176
+ PreTag="div"
177
+ customStyle={{
178
+ borderRadius: 8,
179
+ fontSize: '0.8rem',
180
+ margin: 0,
181
+ border: '1px solid rgba(0,0,0,0.08)',
182
+ }}
183
+ >
184
+ {code}
185
+ </SyntaxHighlighter>
186
+ ) : (
187
+ <code className={className}>{children}</code>
188
+ );
189
+ },
190
+ }}
191
+ >
192
+ {asset.content}
193
+ </ReactMarkdown>
194
+ </Box>
195
+ )}
196
+
197
+ {tab === 1 && (
198
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
199
+ <MetaRow label="Author" value={asset.author} />
200
+ <MetaRow label="Version" value={asset.version} />
201
+ <MetaRow label="Provider" value={asset.providerId} />
202
+ {asset.commitSha && (
203
+ <MetaRow label="Commit" value={asset.commitSha.slice(0, 8)} />
204
+ )}
205
+ <MetaRow label="Last synced" value={new Date(asset.syncedAt).toLocaleString()} />
206
+ <MetaRow label="Branch" value={asset.branch} />
207
+
208
+ <Divider />
209
+
210
+ <Box>
211
+ <Typography variant="caption" color="text.secondary">
212
+ Compatible tools
213
+ </Typography>
214
+ <Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
215
+ {asset.tools.map(t => (
216
+ <Chip key={t} label={t} size="small" />
217
+ ))}
218
+ </Box>
219
+ </Box>
220
+
221
+ {asset.tags.length > 0 && (
222
+ <Box>
223
+ <Typography variant="caption" color="text.secondary">
224
+ Tags
225
+ </Typography>
226
+ <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
227
+ {asset.tags.map(t => (
228
+ <Chip key={t} label={t} size="small" variant="outlined" />
229
+ ))}
230
+ </Box>
231
+ </Box>
232
+ )}
233
+
234
+ {asset.type === 'skill' && (
235
+ <>
236
+ <Divider />
237
+ <Box>
238
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
239
+ <FolderZipIcon sx={{ fontSize: '0.85rem', color: 'text.secondary' }} />
240
+ <Typography variant="caption" color="text.secondary">
241
+ Bundled files
242
+ </Typography>
243
+ </Box>
244
+ {asset.resourcesContent && Object.keys(asset.resourcesContent).length > 0 ? (
245
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 0.5 }}>
246
+ <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
247
+ <Chip
248
+ label="SKILL.md"
249
+ size="small"
250
+ sx={{ fontFamily: 'monospace', fontSize: '0.7rem', height: 20 }}
251
+ />
252
+ {Object.keys(asset.resourcesContent).map(p => (
253
+ <Chip
254
+ key={p}
255
+ label={p}
256
+ size="small"
257
+ variant="outlined"
258
+ sx={{ fontFamily: 'monospace', fontSize: '0.7rem', height: 20 }}
259
+ />
260
+ ))}
261
+ </Box>
262
+ <Typography variant="caption" color="text.disabled">
263
+ Downloads as .zip containing all files above.
264
+ </Typography>
265
+ </Box>
266
+ ) : (
267
+ <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
268
+ <Chip
269
+ label="SKILL.md"
270
+ size="small"
271
+ sx={{ fontFamily: 'monospace', fontSize: '0.7rem', height: 20 }}
272
+ />
273
+ </Box>
274
+ )}
275
+ </Box>
276
+ </>
277
+ )}
278
+
279
+ <Divider />
280
+
281
+ <Box>
282
+ <Typography variant="caption" color="text.secondary">
283
+ Repository
284
+ </Typography>
285
+ <Box>
286
+ <Link
287
+ href={asset.repoUrl}
288
+ target="_blank"
289
+ rel="noopener noreferrer"
290
+ variant="body2"
291
+ >
292
+ {asset.repoUrl} <OpenInNewIcon sx={{ fontSize: 12 }} />
293
+ </Link>
294
+ </Box>
295
+ </Box>
296
+ </Box>
297
+ )}
298
+
299
+ {tab === 2 && (
300
+ <Box
301
+ component="pre"
302
+ sx={{
303
+ bgcolor: 'action.hover',
304
+ p: 2,
305
+ borderRadius: 1,
306
+ overflow: 'auto',
307
+ fontSize: '0.8rem',
308
+ fontFamily: 'monospace',
309
+ whiteSpace: 'pre-wrap',
310
+ wordBreak: 'break-word',
311
+ border: '1px solid',
312
+ borderColor: 'divider',
313
+ }}
314
+ >
315
+ {asset.yamlRaw}
316
+ </Box>
317
+ )}
318
+ </>
319
+ )}
320
+ </Box>
321
+
322
+ {/* Actions */}
323
+ <Box
324
+ sx={{
325
+ p: 2,
326
+ borderTop: 1,
327
+ borderColor: 'divider',
328
+ display: 'flex',
329
+ gap: 1,
330
+ flexWrap: 'wrap',
331
+ }}
332
+ >
333
+ <Button
334
+ variant="contained"
335
+ startIcon={<ContentCopyIcon />}
336
+ onClick={handleCopy}
337
+ disabled={!asset}
338
+ >
339
+ Copy Markdown
340
+ </Button>
341
+
342
+ <Button
343
+ variant="outlined"
344
+ startIcon={<OpenInNewIcon />}
345
+ onClick={() => asset && window.open(asset.repoUrl, '_blank')}
346
+ disabled={!asset}
347
+ >
348
+ Open in Repo
349
+ </Button>
350
+ </Box>
351
+ </Box>
352
+ </Drawer>
353
+
354
+ <Snackbar
355
+ open={!!snackbar}
356
+ autoHideDuration={2500}
357
+ onClose={() => setSnackbar(null)}
358
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
359
+ >
360
+ <Alert severity="success" onClose={() => setSnackbar(null)}>
361
+ {snackbar}
362
+ </Alert>
363
+ </Snackbar>
364
+ </>
365
+ );
366
+ }
367
+
368
+ function MetaRow({ label, value }: { label: string; value: string }) {
369
+ return (
370
+ <Box>
371
+ <Typography variant="caption" color="text.secondary">
372
+ {label}
373
+ </Typography>
374
+ <Typography variant="body2">{value}</Typography>
375
+ </Box>
376
+ );
377
+ }
@@ -0,0 +1 @@
1
+ export { AssetDetailPanel } from './AssetDetailPanel';