fix(skills): refactor skills section to use design system tokens

- Replace hardcoded purple/blue colors with primary/10 backgrounds
- Use text-primary for icons instead of hardcoded colors
- Use error tokens for error states instead of red-*
- Use ProgressBar component instead of custom div progress bars
- Use EmptyState component for empty activity feed
- Use Card components consistently with proper padding
- Fix TypeScript types in integration tests
This commit is contained in:
Ajax Davis 2026-01-25 03:52:20 +10:00
parent 234548c6b2
commit 9f0d7a2b17
5 changed files with 102 additions and 92 deletions

View file

@ -2,6 +2,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { EmptyState } from '@tpmjs/ui/EmptyState/EmptyState';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
@ -88,9 +89,12 @@ export function SkillsActivityFeed({
if (error) {
return (
<Card variant="default" className="border-red-200 bg-red-50">
<Card variant="default" className="border-error/20 bg-error/5">
<CardContent padding="md">
<p className="text-sm text-red-600">{error}</p>
<div className="flex items-center gap-2">
<Icon icon="alertCircle" size="sm" className="text-error" />
<p className="text-sm text-error">{error}</p>
</div>
</CardContent>
</Card>
);
@ -99,11 +103,13 @@ export function SkillsActivityFeed({
if (questions.length === 0) {
return (
<Card variant="default" className="border-dashed">
<CardContent padding="lg" className="text-center">
<Icon icon="message" size="lg" className="mx-auto text-foreground-tertiary mb-2" />
<p className="text-foreground-secondary text-sm">
No questions yet. Be the first to ask!
</p>
<CardContent padding="lg">
<EmptyState
icon="message"
title="No questions yet"
description="Be the first to ask a question about this collection's tools."
size="sm"
/>
</CardContent>
</Card>
);
@ -112,7 +118,7 @@ export function SkillsActivityFeed({
return (
<div className="space-y-3">
{questions.map((q) => (
<Card key={q.id} variant="default" className="hover:border-foreground/20 transition-colors">
<Card key={q.id} variant="default" className="hover:border-primary/20 transition-colors">
<CardHeader padding="sm" className="pb-2">
<div className="flex items-start justify-between gap-2">
<CardTitle as="h4" className="text-sm font-medium line-clamp-2">
@ -127,13 +133,13 @@ export function SkillsActivityFeed({
</CardHeader>
<CardContent padding="sm" className="pt-0">
<CardDescription className="line-clamp-2 text-xs mb-2">
<CardDescription className="line-clamp-2 text-xs mb-3">
{q.answer.slice(0, 150)}
{q.answer.length > 150 ? '...' : ''}
</CardDescription>
<div className="flex items-center justify-between">
<div className="flex gap-1 flex-wrap">
<div className="flex gap-1.5 flex-wrap">
{q.skillNodes.slice(0, 2).map((sn, i) => (
<Badge key={i} variant="outline" size="sm">
{sn.skill.name}
@ -145,11 +151,11 @@ export function SkillsActivityFeed({
</Badge>
)}
</div>
<div className="flex items-center gap-2 text-xs text-foreground-tertiary">
<div className="flex items-center gap-3 text-xs text-foreground-tertiary">
{q.similarCount > 0 && (
<span className="flex items-center gap-1">
<Icon icon="user" size="sm" />
{q.similarCount} similar
{q.similarCount}
</span>
)}
<span>{formatRelativeTime(new Date(q.createdAt))}</span>

View file

@ -1,6 +1,7 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
@ -52,29 +53,29 @@ curl -X POST "${skillsUrl}" \\
}`;
return (
<section className="space-y-4">
<section className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-1.5 bg-purple-100 rounded-lg">
<Icon icon="star" className="w-4 h-4 text-purple-600" />
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-none bg-primary/10 flex items-center justify-center">
<Icon icon="star" className="w-5 h-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-semibold text-foreground">Skills</h2>
<p className="text-xs text-foreground-tertiary">
<p className="text-sm text-foreground-secondary">
Proven in the wild not declared on paper
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
<Icon icon="terminal" className="w-4 h-4 mr-1" />
<Icon icon="terminal" className="w-4 h-4 mr-1.5" />
API
</Button>
<Link href={`/${username}/collections/${slug}/skills`}>
<Button variant="secondary" size="sm">
<Icon icon="externalLink" className="w-4 h-4 mr-1" />
Full Docs
<Icon icon="externalLink" className="w-4 h-4 mr-1.5" />
Docs
</Button>
</Link>
</div>
@ -82,34 +83,36 @@ curl -X POST "${skillsUrl}" \\
{/* API Documentation Toggle */}
{showApiDocs && (
<div className="p-4 bg-surface border border-border rounded-lg space-y-4">
<div>
<h4 className="text-sm font-medium mb-2">API Endpoint</h4>
<div className="px-3 py-2 bg-background border border-border rounded-lg font-mono text-sm text-foreground-secondary overflow-x-auto">
POST {skillsUrl}
<Card variant="default">
<CardContent padding="lg" className="space-y-4">
<div>
<h4 className="text-sm font-medium text-foreground mb-2">API Endpoint</h4>
<div className="px-3 py-2 bg-muted border border-border font-mono text-sm text-foreground-secondary overflow-x-auto">
POST {skillsUrl}
</div>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-2">Example Request</h4>
<CodeBlock language="bash" code={apiExample} />
</div>
<div>
<h4 className="text-sm font-medium text-foreground mb-2">Example Request</h4>
<CodeBlock language="bash" code={apiExample} />
</div>
<div>
<h4 className="text-sm font-medium mb-2">Example Response</h4>
<CodeBlock language="json" code={responseExample} />
</div>
<div>
<h4 className="text-sm font-medium text-foreground mb-2">Example Response</h4>
<CodeBlock language="json" code={responseExample} />
</div>
<div className="text-xs text-foreground-tertiary">
<Link href="/docs/skills" className="text-primary hover:underline">
View full API documentation
</Link>
</div>
</div>
<div className="pt-2 border-t border-border">
<Link href="/docs/skills" className="text-sm text-primary hover:underline">
View full API documentation
</Link>
</div>
</CardContent>
</Card>
)}
{/* Main Content */}
<div className="grid md:grid-cols-3 gap-4">
<div className="grid md:grid-cols-3 gap-6">
{/* Stats Column */}
<div className="md:col-span-1">
<SkillsStats collectionId={collectionId} />
@ -117,32 +120,34 @@ curl -X POST "${skillsUrl}" \\
{/* Activity Feed Column */}
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-foreground-secondary">Recent Questions</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Recent Questions</h3>
</div>
<SkillsActivityFeed collectionId={collectionId} limit={5} />
</div>
</div>
{/* CTA for empty state */}
<div className="p-4 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100 rounded-lg">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg shadow-sm">
<Icon icon="message" className="w-5 h-5 text-purple-600" />
{/* CTA */}
<Card variant="default" className="border-primary/20 bg-primary/5">
<CardContent padding="md">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-none bg-primary/10 flex items-center justify-center flex-shrink-0">
<Icon icon="message" className="w-5 h-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">
Ask questions to build the skill graph
</p>
<p className="text-sm text-foreground-secondary">
Every question helps improve future responses for all agents.
</p>
</div>
<Button variant="default" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
Get Started
</Button>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-foreground">
Ask questions to build the skill graph
</p>
<p className="text-xs text-foreground-secondary">
Every question helps improve future responses for all agents.
</p>
</div>
<Button variant="default" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
Get Started
</Button>
</div>
</div>
</CardContent>
</Card>
</section>
);
}

View file

@ -3,6 +3,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { ProgressBar } from '@tpmjs/ui/ProgressBar/ProgressBar';
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
import { useEffect, useState } from 'react';
@ -46,7 +47,7 @@ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElem
if (loading) {
return (
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-4">
{[1, 2].map((i) => (
<Card key={i} variant="default">
<CardContent padding="md">
@ -61,9 +62,12 @@ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElem
if (error) {
return (
<Card variant="default" className="border-red-200 bg-red-50">
<Card variant="default" className="border-error/20 bg-error/5">
<CardContent padding="md">
<p className="text-sm text-red-600">{error}</p>
<div className="flex items-center gap-2">
<Icon icon="alertCircle" size="sm" className="text-error" />
<p className="text-sm text-error">{error}</p>
</div>
</CardContent>
</Card>
);
@ -76,15 +80,15 @@ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElem
return (
<div className="space-y-4">
{/* Summary Stats */}
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-4">
<Card variant="default">
<CardContent padding="md">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-blue-50">
<Icon icon="message" size="md" className="text-blue-600" />
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-none bg-primary/10 flex items-center justify-center">
<Icon icon="message" size="md" className="text-primary" />
</div>
<div>
<p className="text-2xl font-semibold">{stats.totalQuestions}</p>
<p className="text-2xl font-semibold text-foreground">{stats.totalQuestions}</p>
<p className="text-xs text-foreground-secondary">Questions</p>
</div>
</div>
@ -93,12 +97,12 @@ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElem
<Card variant="default">
<CardContent padding="md">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-purple-50">
<Icon icon="star" size="md" className="text-purple-600" />
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-none bg-primary/10 flex items-center justify-center">
<Icon icon="star" size="md" className="text-primary" />
</div>
<div>
<p className="text-2xl font-semibold">{stats.totalSkills}</p>
<p className="text-2xl font-semibold text-foreground">{stats.totalSkills}</p>
<p className="text-xs text-foreground-secondary">Skills</p>
</div>
</div>
@ -115,26 +119,21 @@ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElem
</CardTitle>
</CardHeader>
<CardContent padding="sm" className="pt-0">
<div className="space-y-2">
<div className="space-y-3">
{stats.topSkills.slice(0, 5).map((skill, i) => (
<div key={i} className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<Badge variant="outline" size="sm" className="truncate max-w-[180px]">
<div key={i} className="space-y-1">
<div className="flex items-center justify-between">
<Badge variant="outline" size="sm" className="truncate max-w-[140px]">
{skill.name}
</Badge>
<span className="text-xs text-foreground-secondary">{skill.questionCount} Q</span>
</div>
<div className="flex items-center gap-2 text-xs text-foreground-secondary flex-shrink-0">
<span>{skill.questionCount} Q</span>
<div
className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden"
title={`${Math.round(skill.confidence * 100)}% confidence`}
>
<div
className="h-full bg-green-500 rounded-full"
style={{ width: `${skill.confidence * 100}%` }}
/>
</div>
</div>
<ProgressBar
value={skill.confidence * 100}
size="sm"
variant="primary"
showLabel={false}
/>
</div>
))}
</div>

View file

@ -36,8 +36,8 @@ async function isServerAvailable(): Promise<boolean> {
describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Scenarios CRUD Integration', () => {
let serverAvailable = false;
let testCollection: { id: string; slug: string; userId: string } | null = null;
const testScenarioId: string | null = null;
let testCollection: { id: string; slug: string | null; userId: string } | null = null;
let testScenarioId: string | null = null;
beforeAll(async () => {
serverAvailable = await isServerAvailable();

View file

@ -30,7 +30,7 @@ async function isServerAvailable(): Promise<boolean> {
describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Scenario Execution Integration', () => {
let serverAvailable = false;
let testScenario: { id: string; name: string; collectionId: string } | null = null;
let testScenario: { id: string; name: string | null; collectionId: string | null } | null = null;
beforeAll(async () => {
serverAvailable = await isServerAvailable();