style(playground): polish UI design for better consistency and alignment
- ChatInput: Redesign with rounded container, better button alignment, helper text - ChatHeader: Consistent bg-surface, refined typography and spacing - ChatMessages: Improved empty state with icon badge and suggestion box - MessageBubble: Cleaner message cards, collapsible tool input/output, status badges - ToolsSidebar: Header/content separation, keyboard accessibility, wider width - SettingsSidebar: Matching header style, dashed empty state, footer info section All components now use consistent theme tokens (bg-surface, bg-background, etc.) and follow the same visual patterns for headers, cards, and spacing.
This commit is contained in:
parent
a793322710
commit
3ce5ea9bb7
6 changed files with 275 additions and 219 deletions
|
|
@ -23,26 +23,26 @@ export function ChatHeader({ onClear }: ChatHeaderProps): React.ReactElement {
|
|||
};
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background px-4 py-3">
|
||||
<header className="border-b border-border bg-surface px-6 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-xl font-bold">TPMJS Playground</h1>
|
||||
<div className="flex items-center gap-6">
|
||||
<h1 className="text-lg font-bold tracking-tight">TPMJS Playground</h1>
|
||||
<Link
|
||||
href="https://tpmjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-foreground-secondary hover:text-foreground"
|
||||
className="text-sm text-foreground-tertiary transition-colors hover:text-foreground"
|
||||
>
|
||||
View Registry →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{mounted && (
|
||||
<Button variant="ghost" onClick={toggleTheme} size="md">
|
||||
<Button variant="ghost" onClick={toggleTheme} size="sm">
|
||||
{theme === 'dark' ? '☀️' : '🌙'}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={onClear} size="md">
|
||||
<Button variant="ghost" onClick={onClear} size="sm">
|
||||
Clear Chat
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,20 +32,31 @@ export function ChatInput({
|
|||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="border-t border-border bg-background p-4">
|
||||
<div className="mx-auto flex max-w-4xl gap-2">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask me to tell you a fish joke..."
|
||||
className="min-h-[60px] flex-1 resize-none"
|
||||
disabled={isLoading}
|
||||
rows={3}
|
||||
/>
|
||||
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
|
||||
Send
|
||||
</Button>
|
||||
<form onSubmit={onSubmit} className="border-t border-border bg-surface p-4">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-end gap-3 rounded-xl border border-border bg-background p-3 shadow-sm">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask me to tell you a fish joke..."
|
||||
className="min-h-[44px] max-h-[200px] flex-1 resize-none border-0 bg-transparent p-0 focus:ring-0"
|
||||
disabled={isLoading}
|
||||
rows={1}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!input.trim() || isLoading}
|
||||
loading={isLoading}
|
||||
size="md"
|
||||
className="shrink-0"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-2 text-center text-xs text-foreground-tertiary">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,11 +22,20 @@ export function ChatMessages({
|
|||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-foreground-secondary">
|
||||
<div className="mb-2 text-4xl">🐟</div>
|
||||
<p className="text-lg">Start chatting to test TPMJS tools</p>
|
||||
<p className="mt-2 text-sm">Try asking: “Tell me a fish joke”</p>
|
||||
<div className="flex h-full items-center justify-center p-8">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-surface">
|
||||
<span className="text-3xl">🐟</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold text-foreground">Test TPMJS Tools</h2>
|
||||
<p className="mb-4 text-sm text-foreground-secondary">
|
||||
Select tools from the sidebar and start chatting to test them.
|
||||
</p>
|
||||
<div className="rounded-lg border border-dashed border-border bg-surface/50 p-4">
|
||||
<p className="text-xs text-foreground-tertiary">
|
||||
Try: “Tell me a fish joke” or “What tools are available?”
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
|
||||
import type { UIMessage } from 'ai';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Streamdown } from 'streamdown';
|
||||
|
|
@ -93,119 +92,134 @@ export function MessageBubble({
|
|||
|
||||
return (
|
||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<Card variant={isUser ? 'elevated' : 'outline'} className="w-full max-w-2xl">
|
||||
<CardContent className="p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge variant={isUser ? 'default' : 'secondary'} size="sm">
|
||||
{isUser ? 'You' : 'AI'}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
<div
|
||||
className={`w-full max-w-3xl rounded-xl border p-4 ${
|
||||
isUser ? 'border-foreground/10 bg-foreground/5' : 'border-border bg-background shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<div
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${
|
||||
isUser ? 'bg-foreground text-background' : 'bg-surface text-foreground-secondary'
|
||||
}`}
|
||||
>
|
||||
{isUser ? 'Y' : 'AI'}
|
||||
</div>
|
||||
<span className="text-xs font-medium text-foreground-secondary">
|
||||
{isUser ? 'You' : 'Assistant'}
|
||||
</span>
|
||||
<span className="text-xs text-foreground-tertiary">
|
||||
{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Render message parts */}
|
||||
{message.parts && message.parts.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{message.parts.map((part, idx) => {
|
||||
// Skip step-start markers
|
||||
if (part.type === 'step-start') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render text parts with markdown support
|
||||
if (part.type === 'text') {
|
||||
const timing = getPartTiming(part, idx);
|
||||
return (
|
||||
<div key={`text-${message.id}-${idx}`} className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>
|
||||
{part.text || ''}
|
||||
</Streamdown>
|
||||
{timing?.duration && (
|
||||
<div className="mt-1 text-xs text-foreground-tertiary">
|
||||
⏱ {formatDuration(timing.duration)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render tool calls (type starts with 'tool-')
|
||||
if (part.type.startsWith('tool-')) {
|
||||
const toolName = part.type.replace('tool-', '');
|
||||
// Type assertion for tool parts
|
||||
const toolPart = part as any;
|
||||
const timing = getPartTiming(part, idx);
|
||||
return (
|
||||
<div
|
||||
key={toolPart.toolCallId || idx}
|
||||
className="rounded border border-amber-500/20 bg-amber-500/5 p-3"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2 border-b border-amber-500/20 pb-2">
|
||||
<span className="text-base">🔧</span>
|
||||
<strong className="font-mono text-sm text-foreground">{toolName}</strong>
|
||||
{toolPart.state && (
|
||||
<Badge variant="secondary" size="sm">
|
||||
{toolPart.state}
|
||||
</Badge>
|
||||
)}
|
||||
{timing?.duration && (
|
||||
<span className="ml-auto text-xs text-foreground-tertiary">
|
||||
⏱ {formatDuration(timing.duration)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Input */}
|
||||
{toolPart.input && (
|
||||
<div className="mb-3">
|
||||
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
|
||||
Input:
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(toolPart.input, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Output */}
|
||||
{toolPart.output && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
|
||||
Output:
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(toolPart.output, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Error */}
|
||||
{(toolPart.errorText || toolPart.error) && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold text-red-500">Error:</div>
|
||||
<pre className="overflow-x-auto rounded bg-red-500/10 p-2 text-xs text-red-400">
|
||||
{typeof (toolPart.errorText || toolPart.error) === 'string'
|
||||
? toolPart.errorText || toolPart.error
|
||||
: JSON.stringify(toolPart.errorText || toolPart.error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Render message parts */}
|
||||
{message.parts && message.parts.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{message.parts.map((part, idx) => {
|
||||
// Skip step-start markers
|
||||
if (part.type === 'step-start') {
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback if no parts
|
||||
<div className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>...</Streamdown>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
|
||||
// Render text parts with markdown support
|
||||
if (part.type === 'text') {
|
||||
const timing = getPartTiming(part, idx);
|
||||
return (
|
||||
<div key={`text-${message.id}-${idx}`} className="text-sm leading-relaxed">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>{part.text || ''}</Streamdown>
|
||||
{timing?.duration && (
|
||||
<div className="mt-2 text-xs text-foreground-tertiary">
|
||||
⏱ {formatDuration(timing.duration)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render tool calls (type starts with 'tool-')
|
||||
if (part.type.startsWith('tool-')) {
|
||||
const toolName = part.type.replace('tool-', '');
|
||||
// Type assertion for tool parts
|
||||
const toolPart = part as any;
|
||||
const timing = getPartTiming(part, idx);
|
||||
const isResult = toolPart.state === 'result';
|
||||
const hasError = toolPart.errorText || toolPart.error;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={toolPart.toolCallId || idx}
|
||||
className={`rounded-lg border p-3 ${
|
||||
hasError
|
||||
? 'border-red-500/30 bg-red-500/5'
|
||||
: 'border-amber-500/30 bg-amber-500/5'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm">🔧</span>
|
||||
<code className="text-sm font-medium">{toolName}</code>
|
||||
<Badge
|
||||
variant={hasError ? 'error' : isResult ? 'success' : 'secondary'}
|
||||
size="sm"
|
||||
>
|
||||
{hasError ? 'error' : toolPart.state || 'running'}
|
||||
</Badge>
|
||||
{timing?.duration && (
|
||||
<span className="ml-auto text-xs text-foreground-tertiary">
|
||||
{formatDuration(timing.duration)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Input */}
|
||||
{toolPart.input && (
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-xs font-medium text-foreground-secondary hover:text-foreground">
|
||||
Input
|
||||
</summary>
|
||||
<pre className="mt-2 overflow-x-auto rounded-md bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(toolPart.input, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Tool Output */}
|
||||
{toolPart.output && (
|
||||
<details className="group mt-2" open>
|
||||
<summary className="cursor-pointer text-xs font-medium text-foreground-secondary hover:text-foreground">
|
||||
Output
|
||||
</summary>
|
||||
<pre className="mt-2 max-h-48 overflow-auto rounded-md bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(toolPart.output, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Tool Error */}
|
||||
{hasError && (
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 text-xs font-medium text-red-500">Error</div>
|
||||
<pre className="overflow-x-auto rounded-md bg-red-500/10 p-2 text-xs text-red-400">
|
||||
{typeof (toolPart.errorText || toolPart.error) === 'string'
|
||||
? toolPart.errorText || toolPart.error
|
||||
: JSON.stringify(toolPart.errorText || toolPart.error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback if no parts
|
||||
<div className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>...</Streamdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
|
@ -65,24 +64,32 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
};
|
||||
|
||||
return (
|
||||
<aside className="hidden w-80 border-l border-border bg-surface md:block">
|
||||
<div className="flex h-full flex-col p-4">
|
||||
<h2 className="mb-4 text-lg font-bold">Settings</h2>
|
||||
<aside className="hidden w-72 border-l border-border bg-surface md:block">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground-secondary">
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables Section */}
|
||||
<Card variant="outline" className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">
|
||||
Environment Variables{' '}
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Environment Variables Section */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground-tertiary">
|
||||
Environment Variables
|
||||
</h3>
|
||||
<Badge variant="secondary" size="sm">
|
||||
{envVars.length}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-xs text-foreground-secondary">
|
||||
Add API keys and other environment variables. They will be forwarded to tool
|
||||
executions.
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-xs text-foreground-tertiary">
|
||||
Add API keys for tools that require authentication.
|
||||
</p>
|
||||
|
||||
{/* Add new env var form */}
|
||||
|
|
@ -109,24 +116,26 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
{/* List of env vars */}
|
||||
<div className="space-y-2">
|
||||
{envVars.length === 0 ? (
|
||||
<p className="text-xs text-foreground-tertiary">No environment variables set</p>
|
||||
<div className="rounded-lg border border-dashed border-border p-4 text-center">
|
||||
<p className="text-xs text-foreground-tertiary">No variables configured</p>
|
||||
</div>
|
||||
) : (
|
||||
envVars.map((env) => (
|
||||
<div
|
||||
key={env.key}
|
||||
className="flex items-center justify-between rounded border border-border bg-background p-2"
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-background p-3"
|
||||
>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="truncate font-mono text-xs font-semibold">{env.key}</p>
|
||||
<p className="truncate font-mono text-xs font-medium">{env.key}</p>
|
||||
<p className="truncate font-mono text-xs text-foreground-tertiary">
|
||||
{env.value ? '•'.repeat(Math.min(env.value.length, 20)) : '(empty)'}
|
||||
{env.value ? '•'.repeat(Math.min(env.value.length, 16)) : '(empty)'}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleRemoveEnvVar(env.key)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="ml-2"
|
||||
className="ml-2 h-6 w-6 p-0 text-foreground-tertiary hover:text-foreground"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
|
|
@ -134,14 +143,13 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-auto rounded border border-border bg-background p-3">
|
||||
<p className="text-xs text-foreground-secondary">
|
||||
<strong>Note:</strong> Environment variables are stored locally in your browser and sent
|
||||
with each tool execution request.
|
||||
{/* Footer Info */}
|
||||
<div className="border-t border-border p-4">
|
||||
<p className="text-xs text-foreground-tertiary">
|
||||
Variables are stored in your browser and sent with each tool execution.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
import { Checkbox } from '@tpmjs/ui/Checkbox/Checkbox';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { ToolHealthBadge } from '@tpmjs/ui/ToolHealthBadge/ToolHealthBadge';
|
||||
|
|
@ -76,69 +75,84 @@ export function ToolsSidebar(): React.ReactElement {
|
|||
|
||||
return (
|
||||
<>
|
||||
<aside className="hidden w-64 border-r border-border bg-surface md:block">
|
||||
<div className="flex h-full flex-col p-4">
|
||||
<h2 className="mb-4 text-lg font-bold">
|
||||
Available Tools <Badge variant="secondary">{filteredTools.length}</Badge>
|
||||
</h2>
|
||||
<aside className="hidden w-72 border-r border-border bg-surface md:block">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground-secondary">
|
||||
Tools
|
||||
</h2>
|
||||
<Badge variant="secondary" size="sm">
|
||||
{filteredTools.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Filter tools..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="mb-3"
|
||||
/>
|
||||
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-foreground-secondary">
|
||||
<Checkbox
|
||||
id="hide-broken"
|
||||
checked={hideBroken}
|
||||
onChange={(e) => setHideBroken(e.target.checked)}
|
||||
size="sm"
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search tools..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="mb-3"
|
||||
/>
|
||||
{/* biome-ignore lint/a11y/noLabelWithoutControl: Checkbox is associated via htmlFor */}
|
||||
<label htmlFor="hide-broken" className="cursor-pointer">
|
||||
Hide broken tools
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-secondary">
|
||||
<Checkbox
|
||||
id="hide-broken"
|
||||
checked={hideBroken}
|
||||
onChange={(e) => setHideBroken(e.target.checked)}
|
||||
size="sm"
|
||||
/>
|
||||
<label htmlFor="hide-broken" className="cursor-pointer select-none">
|
||||
Hide broken tools
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
||||
{loading ? (
|
||||
<p className="text-sm text-foreground-secondary">Loading tools...</p>
|
||||
) : filteredTools.length === 0 ? (
|
||||
<p className="text-sm text-foreground-secondary">No tools found</p>
|
||||
) : (
|
||||
filteredTools.map((tool) => (
|
||||
<Card
|
||||
key={`${tool.packageName}-${tool.exportName}`}
|
||||
variant="outline"
|
||||
className="cursor-pointer transition-all hover:bg-background hover:shadow-md"
|
||||
onClick={() => setSelectedTool(tool)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">{tool.exportName}</CardTitle>
|
||||
<p className="text-xs text-foreground-tertiary mt-0.5">{tool.packageName}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-foreground-secondary line-clamp-2">
|
||||
{tool.description}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" size="sm">
|
||||
{tool.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
|
||||
{/* Tools List */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-sm text-foreground-tertiary">Loading tools...</p>
|
||||
</div>
|
||||
) : filteredTools.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-sm text-foreground-tertiary">No tools found</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredTools.map((tool) => (
|
||||
// biome-ignore lint/a11y/useSemanticElements: Custom styled card with complex layout
|
||||
<div
|
||||
key={`${tool.packageName}-${tool.exportName}`}
|
||||
className="cursor-pointer rounded-lg border border-border bg-background p-3 transition-all hover:border-foreground-tertiary hover:shadow-sm"
|
||||
onClick={() => setSelectedTool(tool)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && setSelectedTool(tool)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="mb-1 flex items-start justify-between gap-2">
|
||||
<h3 className="text-sm font-medium leading-tight">{tool.exportName}</h3>
|
||||
<ToolHealthBadge
|
||||
importHealth={tool.importHealth}
|
||||
executionHealth={tool.executionHealth}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
<p className="mb-2 text-xs text-foreground-tertiary">{tool.packageName}</p>
|
||||
<p className="text-xs text-foreground-secondary line-clamp-2">
|
||||
{tool.description}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge variant="secondary" size="sm">
|
||||
{tool.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
@ -159,7 +173,7 @@ export function ToolsSidebar(): React.ReactElement {
|
|||
tabIndex={0}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<div className="relative mx-4 max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-white dark:bg-gray-900 p-6 shadow-xl">
|
||||
<div className="relative mx-4 max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-xl border border-border bg-background p-6 shadow-xl">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue