@vertesia/tools-admin-ui 1.0.0-dev.20260227.112605Z → 1.0.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 (65) hide show
  1. package/lib/AdminApp.d.ts +3 -1
  2. package/lib/AdminApp.d.ts.map +1 -1
  3. package/lib/components/AdminTopBar.d.ts +6 -0
  4. package/lib/components/AdminTopBar.d.ts.map +1 -0
  5. package/lib/components/CollectionCard.d.ts.map +1 -1
  6. package/lib/components/DetailPage.d.ts.map +1 -1
  7. package/lib/components/EndpointPanel.d.ts.map +1 -1
  8. package/lib/components/HeroSection.d.ts.map +1 -1
  9. package/lib/components/ResourceCard.d.ts.map +1 -1
  10. package/lib/components/ResourceSection.d.ts.map +1 -1
  11. package/lib/components/SearchBar.d.ts.map +1 -1
  12. package/lib/components/SummaryBadge.d.ts.map +1 -1
  13. package/lib/components/index.d.ts +6 -5
  14. package/lib/components/index.d.ts.map +1 -1
  15. package/lib/components/typeVariants.d.ts +5 -0
  16. package/lib/components/typeVariants.d.ts.map +1 -0
  17. package/lib/hooks.d.ts +1 -1
  18. package/lib/hooks.d.ts.map +1 -1
  19. package/lib/pages/ActivityCollection.d.ts +2 -0
  20. package/lib/pages/ActivityCollection.d.ts.map +1 -0
  21. package/lib/pages/HomePage.d.ts.map +1 -1
  22. package/lib/pages/InteractionCollection.d.ts.map +1 -1
  23. package/lib/pages/InteractionDetail.d.ts.map +1 -1
  24. package/lib/pages/SkillCollection.d.ts.map +1 -1
  25. package/lib/pages/SkillDetail.d.ts.map +1 -1
  26. package/lib/pages/TemplateCollection.d.ts.map +1 -1
  27. package/lib/pages/TemplateDetail.d.ts.map +1 -1
  28. package/lib/pages/ToolCollection.d.ts.map +1 -1
  29. package/lib/pages/TypeCollection.d.ts.map +1 -1
  30. package/lib/pages/TypeDetail.d.ts.map +1 -1
  31. package/lib/tools-admin-ui.js +481 -379
  32. package/lib/tools-admin-ui.js.map +1 -1
  33. package/lib/types.d.ts +8 -3
  34. package/lib/types.d.ts.map +1 -1
  35. package/package.json +8 -4
  36. package/src/AdminApp.tsx +23 -17
  37. package/src/components/AdminTopBar.tsx +39 -0
  38. package/src/components/CollectionCard.tsx +20 -14
  39. package/src/components/DetailPage.tsx +20 -11
  40. package/src/components/EndpointPanel.tsx +16 -7
  41. package/src/components/HeroSection.tsx +52 -45
  42. package/src/components/ResourceCard.tsx +23 -18
  43. package/src/components/ResourceSection.tsx +7 -5
  44. package/src/components/SearchBar.tsx +8 -6
  45. package/src/components/SummaryBadge.tsx +4 -3
  46. package/src/components/index.ts +6 -5
  47. package/src/components/typeVariants.ts +19 -0
  48. package/src/dev/env.ts +3 -3
  49. package/src/dev/index.css +13 -0
  50. package/src/dev/main.tsx +11 -5
  51. package/src/hooks.ts +5 -3
  52. package/src/pages/ActivityCollection.tsx +67 -0
  53. package/src/pages/HomePage.tsx +19 -15
  54. package/src/pages/InteractionCollection.tsx +25 -27
  55. package/src/pages/InteractionDetail.tsx +35 -34
  56. package/src/pages/SkillCollection.tsx +29 -32
  57. package/src/pages/SkillDetail.tsx +31 -41
  58. package/src/pages/TemplateCollection.tsx +25 -21
  59. package/src/pages/TemplateDetail.tsx +18 -17
  60. package/src/pages/ToolCollection.tsx +22 -16
  61. package/src/pages/TypeCollection.tsx +32 -24
  62. package/src/pages/TypeDetail.tsx +16 -18
  63. package/src/theme.css +12 -0
  64. package/src/types.ts +34 -1
  65. package/src/admin.css +0 -650
@@ -0,0 +1,67 @@
1
+ import type { RemoteActivityDefinition } from '@vertesia/common';
2
+ import { Card, CardContent, Spinner, useFetch } from '@vertesia/ui/core';
3
+ import { useParams } from '@vertesia/ui/router';
4
+
5
+ import { useAdminContext } from '../AdminContext.js';
6
+ import { DetailPage } from '../components/DetailPage.js';
7
+ import { TYPE_VARIANTS } from '../components/typeVariants.js';
8
+
9
+ interface ActivityCollectionResponse {
10
+ title: string;
11
+ description: string;
12
+ activities: RemoteActivityDefinition[];
13
+ }
14
+
15
+ export function ActivityCollection() {
16
+ const collection = useParams('collection');
17
+ const { baseUrl } = useAdminContext();
18
+
19
+ const { data, error } = useFetch<ActivityCollectionResponse>(
20
+ () => fetch(`${baseUrl}/activities/${collection}`).then(r => {
21
+ if (!r.ok) throw new Error(`Failed to load collection: ${r.statusText}`);
22
+ return r.json();
23
+ }),
24
+ [baseUrl, collection]
25
+ );
26
+
27
+ if (error) return <div className="p-6 text-destructive">Failed to load activity collection &ldquo;{collection}&rdquo;.</div>;
28
+ if (!data) return <div className="flex h-64 items-center justify-center text-muted-foreground"><Spinner /></div>;
29
+
30
+ return (
31
+ <DetailPage
32
+ type="activity"
33
+ title={data.title || collection}
34
+ description={data.description || `${data.activities.length} activit${data.activities.length !== 1 ? 'ies' : 'y'} in this collection.`}
35
+ >
36
+ {data.activities.map(activity => (
37
+ <Card key={activity.name} className="mb-4">
38
+ <CardContent className="p-5">
39
+ <div className="mb-2 flex items-center gap-2">
40
+ <span className={`inline-block rounded-full px-2 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wide ${TYPE_VARIANTS.activity}`}>
41
+ activity
42
+ </span>
43
+ <span className="font-semibold text-card-foreground">{activity.name}</span>
44
+ </div>
45
+ <div className="text-sm text-muted-foreground">{activity.description || 'No description'}</div>
46
+ {activity.input_schema && (
47
+ <div className="mt-3">
48
+ <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Input Schema</p>
49
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">
50
+ {JSON.stringify(activity.input_schema, null, 2)}
51
+ </pre>
52
+ </div>
53
+ )}
54
+ {activity.output_schema && (
55
+ <div className="mt-3">
56
+ <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Output Schema</p>
57
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">
58
+ {JSON.stringify(activity.output_schema, null, 2)}
59
+ </pre>
60
+ </div>
61
+ )}
62
+ </CardContent>
63
+ </Card>
64
+ ))}
65
+ </DetailPage>
66
+ );
67
+ }
@@ -1,11 +1,15 @@
1
+ import { Separator } from '@vertesia/ui/core';
1
2
  import { useMemo, useState } from 'react';
3
+
4
+ import { useAdminContext } from '../AdminContext.js';
5
+ import { CollectionCard, HeroSection, ResourceSection, SearchBar } from '../components/index.js';
6
+ import { TYPE_VARIANTS } from '../components/typeVariants.js';
2
7
  import type { ResourceType } from '../types.js';
3
8
  import { filterResources } from '../types.js';
4
- import { HeroSection, SearchBar, ResourceSection, CollectionCard } from '../components/index.js';
5
- import { useAdminContext } from '../AdminContext.js';
6
9
 
7
10
  const sections: { type: ResourceType; title: string; subtitle: string }[] = [
8
11
  { type: 'tool', title: 'Tools', subtitle: 'Remote tools available to agents via Vertesia.' },
12
+ { type: 'activity', title: 'Activities', subtitle: 'Remote activities for DSL workflows, invoked via HTTP.' },
9
13
  { type: 'skill', title: 'Skills', subtitle: 'Reusable instructions and scripts packaged as tools.' },
10
14
  { type: 'interaction', title: 'Interactions', subtitle: 'Conversation blueprints surfaced in the Vertesia UI.' },
11
15
  { type: 'type', title: 'Content Types', subtitle: 'Schema definitions for structured content in the data store.' },
@@ -25,7 +29,7 @@ export function HomePage() {
25
29
  const isSearching = search.trim().length > 0;
26
30
 
27
31
  return (
28
- <div className="vta-root">
32
+ <div className="mx-auto max-w-5xl px-7 py-10">
29
33
  <HeroSection
30
34
  title={serverInfo.message.replace('Vertesia Tools API', 'Tools Server')}
31
35
  version={serverInfo.version}
@@ -41,7 +45,6 @@ export function HomePage() {
41
45
  />
42
46
 
43
47
  {isSearching ? (
44
- /* Search mode: show individual resource cards */
45
48
  sections.map((section, i) => {
46
49
  const sectionItems = filtered.filter(r => r.type === section.type);
47
50
  return (
@@ -55,7 +58,6 @@ export function HomePage() {
55
58
  );
56
59
  })
57
60
  ) : (
58
- /* Browse mode: show collection cards grouped by type */
59
61
  sections.map((section, i) => {
60
62
  const sectionCollections = collections.filter(c => c.type === section.type);
61
63
  const mcpResources = section.type === 'mcp'
@@ -66,27 +68,29 @@ export function HomePage() {
66
68
 
67
69
  return (
68
70
  <section key={section.type}>
69
- {i > 0 && <hr className="vta-divider" />}
71
+ {i > 0 && <Separator className="my-8" />}
70
72
  <div>
71
- <h2 className="vta-section-title">
73
+ <h2 className="text-xl font-semibold text-foreground">
72
74
  {section.title}
73
- <span className="vta-section-count">
75
+ <span className="ml-2 text-sm font-normal text-muted-foreground">
74
76
  ({sectionCollections.length}{sectionCollections.length === 1
75
77
  ? ' collection' : ' collections'})
76
78
  </span>
77
79
  </h2>
78
- <p className="vta-section-subtitle">{section.subtitle}</p>
80
+ <p className="mb-4 text-sm text-muted-foreground">{section.subtitle}</p>
79
81
  </div>
80
- <div className="vta-card-grid">
82
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
81
83
  {sectionCollections.map(col => (
82
84
  <CollectionCard key={`${col.type}:${col.name}`} collection={col} />
83
85
  ))}
84
86
  {mcpResources.map(r => (
85
- <div key={r.name} className="vta-card">
86
- <span className="vta-card-type vta-card-type--mcp">mcp</span>
87
- <div className="vta-card-title">{r.title}</div>
88
- <div className="vta-card-desc">{r.description || 'No description'}</div>
89
- {r.url && <div className="vta-card-url">{r.url}</div>}
87
+ <div key={r.name} className="rounded-xl border border-border bg-card p-5 shadow-sm">
88
+ <span className={`mb-2 inline-block rounded-full px-2 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wide ${TYPE_VARIANTS.mcp}`}>
89
+ mcp
90
+ </span>
91
+ <div className="font-semibold text-card-foreground">{r.title}</div>
92
+ <div className="mt-1 text-sm text-muted-foreground">{r.description || 'No description'}</div>
93
+ {r.url && <div className="mt-2 truncate font-mono text-xs text-muted-foreground">{r.url}</div>}
90
94
  </div>
91
95
  ))}
92
96
  </div>
@@ -1,14 +1,16 @@
1
- import { useFetch } from '@vertesia/ui/core';
2
- import { useParams, NavLink } from '@vertesia/ui/router';
3
1
  import type { CatalogInteractionRef } from '@vertesia/common';
2
+ import { Badge, Card, CardContent, Spinner, useFetch } from '@vertesia/ui/core';
3
+ import { NavLink, useParams } from '@vertesia/ui/router';
4
+
4
5
  import { useAdminContext } from '../AdminContext.js';
5
6
  import { DetailPage } from '../components/DetailPage.js';
7
+ import { TYPE_VARIANTS } from '../components/typeVariants.js';
6
8
 
7
9
  export function InteractionCollection() {
8
10
  const collection = useParams('collection');
9
11
  const { baseUrl } = useAdminContext();
10
12
 
11
- const { data: interactions, isLoading, error } = useFetch<CatalogInteractionRef[]>(
13
+ const { data: interactions, error } = useFetch<CatalogInteractionRef[]>(
12
14
  () => fetch(`${baseUrl}/interactions/${collection}`).then(r => {
13
15
  if (!r.ok) throw new Error(`Failed to load collection: ${r.statusText}`);
14
16
  return r.json();
@@ -16,12 +18,12 @@ export function InteractionCollection() {
16
18
  [baseUrl, collection]
17
19
  );
18
20
 
19
- if (isLoading) {
20
- return <div className="vta-loading">Loading collection...</div>;
21
+ if (error) {
22
+ return <div className="p-6 text-destructive">Failed to load collection &ldquo;{collection}&rdquo;.</div>;
21
23
  }
22
24
 
23
- if (error || !interactions) {
24
- return <div className="vta-error">Failed to load collection &ldquo;{collection}&rdquo;.</div>;
25
+ if (!interactions) {
26
+ return <div className="flex h-64 items-center justify-center text-muted-foreground"><Spinner /></div>;
25
27
  }
26
28
 
27
29
  return (
@@ -30,27 +32,23 @@ export function InteractionCollection() {
30
32
  title={collection}
31
33
  description={`${interactions.length} interaction${interactions.length !== 1 ? 's' : ''} in this collection.`}
32
34
  >
33
- <div className="vta-card-grid">
35
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
34
36
  {interactions.map(inter => (
35
- <NavLink
36
- key={inter.id}
37
- href={`/interactions/${collection}/${inter.name}`}
38
- className="vta-card-link"
39
- >
40
- <div className="vta-card vta-card--link">
41
- <span className="vta-card-type vta-card-type--interaction">interaction</span>
42
- <div className="vta-card-title">{inter.title || inter.name}</div>
43
- <div className="vta-card-desc">
44
- {inter.description || 'No description'}
45
- </div>
46
- {inter.tags && inter.tags.length > 0 && (
47
- <div className="vta-card-tags">
48
- {inter.tags.map(tag => (
49
- <span key={tag} className="vta-tag">{tag}</span>
50
- ))}
51
- </div>
52
- )}
53
- </div>
37
+ <NavLink key={inter.id} href={`/interactions/${collection}/${inter.name}`} className="block no-underline">
38
+ <Card className="cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md">
39
+ <CardContent className="p-5">
40
+ <span className={`mb-2 inline-block rounded-full px-2 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wide ${TYPE_VARIANTS.interaction}`}>
41
+ interaction
42
+ </span>
43
+ <div className="font-semibold text-card-foreground">{inter.title || inter.name}</div>
44
+ <div className="mt-1 text-sm text-muted-foreground">{inter.description || 'No description'}</div>
45
+ {inter.tags && inter.tags.length > 0 && (
46
+ <div className="mt-2 flex flex-wrap gap-1">
47
+ {inter.tags.map(tag => <Badge key={tag}>{tag}</Badge>)}
48
+ </div>
49
+ )}
50
+ </CardContent>
51
+ </Card>
54
52
  </NavLink>
55
53
  ))}
56
54
  </div>
@@ -1,9 +1,11 @@
1
- import { useFetch } from '@vertesia/ui/core';
2
- import { useParams } from '@vertesia/ui/router';
3
1
  import type { InteractionSpec } from '@vertesia/common';
2
+ import { Badge, Card, CardContent, Spinner, useFetch } from '@vertesia/ui/core';
3
+ import { useParams } from '@vertesia/ui/router';
4
+ import { useUserSession } from '@vertesia/ui/session';
5
+
4
6
  import { useAdminContext } from '../AdminContext.js';
5
7
  import { DetailPage } from '../components/DetailPage.js';
6
- import { useUserSession } from '@vertesia/ui/session';
8
+ import { ROLE_VARIANTS } from '../components/typeVariants.js';
7
9
 
8
10
  type InteractionResponse = InteractionSpec & { id: string };
9
11
 
@@ -14,7 +16,7 @@ export function InteractionDetail() {
14
16
  const name = params.name;
15
17
  const { baseUrl } = useAdminContext();
16
18
 
17
- const { data: interaction, isLoading, error } = useFetch<InteractionResponse>(
19
+ const { data: interaction, error } = useFetch<InteractionResponse>(
18
20
  () => client.getRawJWT().then(token => fetch(`${baseUrl}/interactions/${collection}/${name}`, {
19
21
  headers: {
20
22
  Authorization: `Bearer ${token}`,
@@ -26,12 +28,12 @@ export function InteractionDetail() {
26
28
  [baseUrl, collection, name]
27
29
  );
28
30
 
29
- if (isLoading) {
30
- return <div className="vta-loading">Loading interaction...</div>;
31
+ if (error) {
32
+ return <div className="p-6 text-destructive">Failed to load interaction &ldquo;{name}&rdquo;.</div>;
31
33
  }
32
34
 
33
- if (error || !interaction) {
34
- return <div className="vta-error">Failed to load interaction &ldquo;{name}&rdquo;.</div>;
35
+ if (!interaction) {
36
+ return <div className="flex h-64 items-center justify-center text-muted-foreground"><Spinner /></div>;
35
37
  }
36
38
 
37
39
  const { agent_runner_options } = interaction;
@@ -46,44 +48,43 @@ export function InteractionDetail() {
46
48
  tags={interaction.tags}
47
49
  backHref={`/interactions/${collection}`}
48
50
  >
49
- {/* Prompts */}
50
51
  {interaction.prompts && interaction.prompts.length > 0 && (
51
- <div className="vta-detail-section">
52
- <h2>Prompts</h2>
53
- {interaction.prompts.map((prompt, i) => (
54
- <div key={i} className="vta-detail-card">
55
- <div className="vta-detail-card-header">
56
- <span className={`vta-detail-role vta-detail-role--${prompt.role}`}>
57
- {prompt.role}
58
- </span>
59
- {prompt.name && (
60
- <span className="vta-detail-prompt-name">{prompt.name}</span>
61
- )}
62
- </div>
63
- <pre className="vta-detail-code">{prompt.content}</pre>
64
- </div>
52
+ <div className="mb-8">
53
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Prompts</h2>
54
+ {interaction.prompts.map((prompt) => (
55
+ <Card key={`${prompt.role}-${prompt.name ?? ''}`} className="mb-3">
56
+ <CardContent className="p-4">
57
+ <div className="mb-2 flex items-center gap-2">
58
+ <span className={`rounded-full px-2 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wide ${ROLE_VARIANTS[prompt.role] ?? ''}`}>
59
+ {prompt.role}
60
+ </span>
61
+ {prompt.name && (
62
+ <span className="text-sm italic text-muted-foreground">{prompt.name}</span>
63
+ )}
64
+ </div>
65
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">{prompt.content}</pre>
66
+ </CardContent>
67
+ </Card>
65
68
  ))}
66
69
  </div>
67
70
  )}
68
71
 
69
- {/* Result schema */}
70
72
  {interaction.result_schema && (
71
- <div className="vta-detail-section">
72
- <h2>Result Schema</h2>
73
- <pre className="vta-detail-code">
73
+ <div className="mb-8">
74
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Result Schema</h2>
75
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">
74
76
  {JSON.stringify(interaction.result_schema, null, 2)}
75
77
  </pre>
76
78
  </div>
77
79
  )}
78
80
 
79
- {/* Agent runner options */}
80
81
  {hasAgentFlags && (
81
- <div className="vta-detail-section">
82
- <h2>Agent Runner</h2>
83
- <div className="vta-detail-flags">
84
- {agent_runner_options.is_agent && <span className="vta-detail-flag">Agent</span>}
85
- {agent_runner_options.is_tool && <span className="vta-detail-flag">Tool</span>}
86
- {agent_runner_options.is_skill && <span className="vta-detail-flag">Skill</span>}
82
+ <div className="mb-8">
83
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Agent Runner</h2>
84
+ <div className="flex flex-wrap gap-2">
85
+ {agent_runner_options.is_agent && <Badge variant="success">Agent</Badge>}
86
+ {agent_runner_options.is_tool && <Badge variant="success">Tool</Badge>}
87
+ {agent_runner_options.is_skill && <Badge variant="success">Skill</Badge>}
87
88
  </div>
88
89
  </div>
89
90
  )}
@@ -1,8 +1,10 @@
1
+ import { Badge, Card, CardContent, Spinner, useFetch } from '@vertesia/ui/core';
2
+ import { NavLink, useParams } from '@vertesia/ui/router';
1
3
  import { useMemo } from 'react';
2
- import { useFetch } from '@vertesia/ui/core';
3
- import { useParams, NavLink } from '@vertesia/ui/router';
4
+
4
5
  import { useAdminContext } from '../AdminContext.js';
5
6
  import { DetailPage } from '../components/DetailPage.js';
7
+ import { TYPE_VARIANTS } from '../components/typeVariants.js';
6
8
 
7
9
  interface SkillToolDef {
8
10
  name: string;
@@ -35,7 +37,7 @@ export function SkillCollection() {
35
37
  const collection = useParams('collection');
36
38
  const { baseUrl } = useAdminContext();
37
39
 
38
- const { data, isLoading, error } = useFetch<SkillCollectionResponse>(
40
+ const { data, error } = useFetch<SkillCollectionResponse>(
39
41
  () => fetch(`${baseUrl}/skills/${collection}`).then(r => {
40
42
  if (!r.ok) throw new Error(`Failed to load collection: ${r.statusText}`);
41
43
  return r.json();
@@ -55,8 +57,8 @@ export function SkillCollection() {
55
57
  .map(([name, w]) => ({ name, ...w }));
56
58
  }, [widgetsData, collection]);
57
59
 
58
- if (isLoading) return <div className="vta-loading">Loading collection...</div>;
59
- if (error || !data) return <div className="vta-error">Failed to load skill collection &ldquo;{collection}&rdquo;.</div>;
60
+ if (error) return <div className="p-6 text-destructive">Failed to load skill collection &ldquo;{collection}&rdquo;.</div>;
61
+ if (!data) return <div className="flex h-64 items-center justify-center text-muted-foreground"><Spinner /></div>;
60
62
 
61
63
  return (
62
64
  <DetailPage
@@ -64,44 +66,39 @@ export function SkillCollection() {
64
66
  title={data.title || collection}
65
67
  description={data.description || `${data.tools.length} skill${data.tools.length !== 1 ? 's' : ''} in this collection.`}
66
68
  >
67
- {/* Widgets provided by this collection */}
68
69
  {collectionWidgets.length > 0 && (
69
- <div className="vta-detail-section">
70
- <h2>Widgets</h2>
71
- <div className="vta-detail-flags">
70
+ <div className="mb-8">
71
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Widgets</h2>
72
+ <div className="flex flex-wrap gap-2">
72
73
  {collectionWidgets.map(w => (
73
- <span key={w.name} className="vta-detail-flag">
74
+ <Badge key={w.name} variant="success">
74
75
  {w.name}
75
- <span className="vta-card-url" style={{ marginLeft: '0.5rem' }}>
76
- (skill: {w.skill})
77
- </span>
78
- </span>
76
+ <span className="ml-2 font-mono text-xs opacity-70">(skill: {w.skill})</span>
77
+ </Badge>
79
78
  ))}
80
79
  </div>
81
80
  </div>
82
81
  )}
83
82
 
84
- <div className="vta-card-grid">
83
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
85
84
  {data.tools.map(skill => {
86
85
  const displayName = skillDisplayName(skill.name);
87
86
  return (
88
- <NavLink
89
- key={skill.name}
90
- href={`/skills/${collection}/${displayName}`}
91
- className="vta-card-link"
92
- >
93
- <div className="vta-card vta-card--link">
94
- <span className="vta-card-type vta-card-type--skill">skill</span>
95
- <div className="vta-card-title">{displayName}</div>
96
- <div className="vta-card-desc">{skill.description || 'No description'}</div>
97
- {skill.related_tools && skill.related_tools.length > 0 && (
98
- <div className="vta-card-tags">
99
- {skill.related_tools.map(t => (
100
- <span key={t} className="vta-tag">{t}</span>
101
- ))}
102
- </div>
103
- )}
104
- </div>
87
+ <NavLink key={skill.name} href={`/skills/${collection}/${displayName}`} className="block no-underline">
88
+ <Card className="cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md">
89
+ <CardContent className="p-5">
90
+ <span className={`mb-2 inline-block rounded-full px-2 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wide ${TYPE_VARIANTS.skill}`}>
91
+ skill
92
+ </span>
93
+ <div className="font-semibold text-card-foreground">{displayName}</div>
94
+ <div className="mt-1 text-sm text-muted-foreground">{skill.description || 'No description'}</div>
95
+ {skill.related_tools && skill.related_tools.length > 0 && (
96
+ <div className="mt-2 flex flex-wrap gap-1">
97
+ {skill.related_tools.map(t => <Badge key={t}>{t}</Badge>)}
98
+ </div>
99
+ )}
100
+ </CardContent>
101
+ </Card>
105
102
  </NavLink>
106
103
  );
107
104
  })}
@@ -1,5 +1,6 @@
1
- import { useFetch } from '@vertesia/ui/core';
1
+ import { Badge, Spinner, useFetch } from '@vertesia/ui/core';
2
2
  import { useParams } from '@vertesia/ui/router';
3
+
3
4
  import { useAdminContext } from '../AdminContext.js';
4
5
  import { DetailPage } from '../components/DetailPage.js';
5
6
 
@@ -25,7 +26,7 @@ export function SkillDetail() {
25
26
  const name = params.name;
26
27
  const { baseUrl } = useAdminContext();
27
28
 
28
- const { data: skill, isLoading, error } = useFetch<SkillDefinitionResponse>(
29
+ const { data: skill, error } = useFetch<SkillDefinitionResponse>(
29
30
  () => fetch(`${baseUrl}/skills/${collection}/${name}`).then(r => {
30
31
  if (!r.ok) throw new Error(`Failed to load skill: ${r.statusText}`);
31
32
  return r.json();
@@ -33,8 +34,8 @@ export function SkillDetail() {
33
34
  [baseUrl, collection, name]
34
35
  );
35
36
 
36
- if (isLoading) return <div className="vta-loading">Loading skill...</div>;
37
- if (error || !skill) return <div className="vta-error">Failed to load skill &ldquo;{name}&rdquo;.</div>;
37
+ if (error) return <div className="p-6 text-destructive">Failed to load skill &ldquo;{name}&rdquo;.</div>;
38
+ if (!skill) return <div className="flex h-64 items-center justify-center text-muted-foreground"><Spinner /></div>;
38
39
 
39
40
  return (
40
41
  <DetailPage
@@ -43,66 +44,55 @@ export function SkillDetail() {
43
44
  description={skill.description}
44
45
  backHref={`/skills/${collection}`}
45
46
  >
46
- {/* Widgets */}
47
47
  {skill.widgets && skill.widgets.length > 0 && (
48
- <div className="vta-detail-section">
49
- <h2>Widgets</h2>
50
- <div className="vta-detail-flags">
51
- {skill.widgets.map(w => (
52
- <span key={w} className="vta-detail-flag">{w}</span>
53
- ))}
48
+ <div className="mb-8">
49
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Widgets</h2>
50
+ <div className="flex flex-wrap gap-2">
51
+ {skill.widgets.map(w => <Badge key={w} variant="success">{w}</Badge>)}
54
52
  </div>
55
53
  </div>
56
54
  )}
57
55
 
58
- {/* Scripts */}
59
56
  {skill.scripts && skill.scripts.length > 0 && (
60
- <div className="vta-detail-section">
61
- <h2>Scripts</h2>
62
- <div className="vta-detail-flags">
63
- {skill.scripts.map(s => (
64
- <span key={s} className="vta-detail-flag">{s}</span>
65
- ))}
57
+ <div className="mb-8">
58
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Scripts</h2>
59
+ <div className="flex flex-wrap gap-2">
60
+ {skill.scripts.map(s => <Badge key={s} variant="success">{s}</Badge>)}
66
61
  </div>
67
62
  </div>
68
63
  )}
69
64
 
70
- {/* Related tools */}
71
65
  {skill.related_tools && skill.related_tools.length > 0 && (
72
- <div className="vta-detail-section">
73
- <h2>Related Tools</h2>
74
- <div className="vta-detail-flags">
75
- {skill.related_tools.map(t => (
76
- <span key={t} className="vta-detail-flag">{t}</span>
77
- ))}
66
+ <div className="mb-8">
67
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Related Tools</h2>
68
+ <div className="flex flex-wrap gap-2">
69
+ {skill.related_tools.map(t => <Badge key={t} variant="success">{t}</Badge>)}
78
70
  </div>
79
71
  </div>
80
72
  )}
81
73
 
82
- {/* Execution environment */}
83
74
  {skill.execution && (
84
- <div className="vta-detail-section">
85
- <h2>Execution</h2>
86
- <div className="vta-detail-flags">
87
- <span className="vta-detail-flag">{skill.execution.language}</span>
88
- {skill.execution.packages?.map(p => (
89
- <span key={p} className="vta-detail-flag">{p}</span>
90
- ))}
75
+ <div className="mb-8">
76
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Execution</h2>
77
+ <div className="flex flex-wrap gap-2">
78
+ <Badge variant="success">{skill.execution.language}</Badge>
79
+ {skill.execution.packages?.map(p => <Badge key={p} variant="success">{p}</Badge>)}
91
80
  </div>
92
81
  </div>
93
82
  )}
94
83
 
95
- {/* Instructions */}
96
- <div className="vta-detail-section">
97
- <h2>Instructions {skill.content_type === 'jst' && <span className="vta-tag">JST template</span>}</h2>
98
- <pre className="vta-detail-code">{skill.instructions}</pre>
84
+ <div className="mb-8">
85
+ <h2 className="mb-3 text-lg font-semibold text-foreground">
86
+ Instructions
87
+ {skill.content_type === 'jst' && <Badge className="ml-2">JST template</Badge>}
88
+ </h2>
89
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">{skill.instructions}</pre>
99
90
  </div>
100
91
 
101
- {/* Input schema */}
102
92
  {skill.input_schema && (
103
- <div className="vta-detail-section">
104
- <h2>Input Schema</h2>
105
- <pre className="vta-detail-code">
93
+ <div className="mb-8">
94
+ <h2 className="mb-3 text-lg font-semibold text-foreground">Input Schema</h2>
95
+ <pre className="whitespace-pre-wrap wrap-break-word rounded-lg border border-border bg-muted-background p-4 font-mono text-sm text-foreground">
106
96
  {JSON.stringify(skill.input_schema, null, 2)}
107
97
  </pre>
108
98
  </div>