refactor: replace raw HTML elements with design system components

- Replace <select>, <input>, <label>, <textarea> with UI components
- Update various dashboard and docs pages
- Simplify unsandbox package.json
- Add DESIGN_SYSTEM.md documentation
- Add Claude skills configuration
This commit is contained in:
Ajax Davis 2026-01-16 16:52:44 +10:00
parent 7d9321d9c6
commit 23d5159b28
26 changed files with 1280 additions and 335 deletions

View file

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -3,6 +3,7 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import Link from 'next/link';
import { useState } from 'react';
import { signIn } from '~/lib/auth-client';
@ -77,9 +78,7 @@ export default function SignInPage() {
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground mb-1">
Email
</label>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
@ -92,9 +91,7 @@ export default function SignInPage() {
<div>
<div className="flex items-center justify-between mb-1">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<Label htmlFor="password" className="mb-0">Password</Label>
<Link
href="/forgot-password"
className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
@ -112,14 +109,16 @@ export default function SignInPage() {
placeholder="Enter your password"
className="pr-10"
/>
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground transition-colors"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
<Icon icon={showPassword ? 'eyeOff' : 'eye'} size="sm" />
</button>
</Button>
</div>
</div>

View file

@ -125,7 +125,7 @@ export default function PrettyAgentDetailPage(): React.ReactElement {
</div>
) : error ? (
<div className="text-center py-12">
<p className="text-red-500">{error}</p>
<p className="text-error">{error}</p>
</div>
) : agent ? (
<div className="space-y-8">

View file

@ -255,7 +255,7 @@ export default function PrettyCollectionDetailPage(): React.ReactElement {
</div>
) : error ? (
<div className="text-center py-12">
<p className="text-red-500">{error}</p>
<p className="text-error">{error}</p>
</div>
) : collection ? (
<div className="space-y-8">

View file

@ -83,7 +83,7 @@ export default function UserProfilePage(): React.ReactElement {
</div>
) : error ? (
<div className="text-center py-12">
<p className="text-red-500">{error}</p>
<p className="text-error">{error}</p>
</div>
) : profile ? (
<div className="space-y-8">

View file

@ -4,6 +4,7 @@ import type { AIProvider } from '@tpmjs/types/agent';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
@ -102,10 +103,10 @@ function ToolCallCard({
onToggle: () => void;
}) {
const statusColors = {
pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
running: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
success: 'bg-green-500/20 text-green-400 border-green-500/30',
error: 'bg-red-500/20 text-red-400 border-red-500/30',
pending: 'bg-warning/10 text-warning border-warning/30',
running: 'bg-info/10 text-info border-info/30',
success: 'bg-success/10 text-success border-success/30',
error: 'bg-error/10 text-error border-error/30',
};
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
@ -126,10 +127,10 @@ function ToolCallCard({
return (
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden font-mono text-xs">
{/* Header */}
<button
type="button"
<Button
variant="ghost"
onClick={onToggle}
className="w-full flex items-center gap-3 p-3 hover:bg-surface-secondary/80 transition-colors"
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
>
<div className={`p-1.5 rounded ${statusColors[toolCall.status]}`}>
<Icon
@ -151,7 +152,7 @@ function ToolCallCard({
size="xs"
className={`text-foreground-tertiary transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
</Button>
{/* Expanded Content */}
{isExpanded && (
@ -180,7 +181,7 @@ function ToolCallCard({
</span>
<div className="flex-1 h-px bg-border/50" />
</div>
<pre className="text-[11px] text-green-400 overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
<pre className="text-[11px] text-success overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
{formatJson(toolCall.output)}
</pre>
</div>
@ -661,14 +662,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
<div className="border-b border-border bg-surface/50 px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(!sidebarOpen)}
className="p-2 hover:bg-surface-secondary rounded-lg transition-colors"
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
>
<Icon icon="menu" size="sm" />
</button>
</Button>
<div>
<h1 className="text-lg font-semibold text-foreground">{agent.name}</h1>
<p className="text-sm text-foreground-tertiary">
@ -683,28 +684,20 @@ export default function PublicAgentChatPage(): React.ReactElement {
</div>
{/* View Mode Tabs */}
<div className="flex gap-1 mt-3">
<button
type="button"
<Button
variant={viewMode === 'chat' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('chat')}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
viewMode === 'chat'
? 'bg-primary text-primary-foreground'
: 'text-foreground-secondary hover:text-foreground hover:bg-surface-secondary'
}`}
>
Chat
</button>
<button
type="button"
</Button>
<Button
variant={viewMode === 'debug' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('debug')}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
viewMode === 'debug'
? 'bg-primary text-primary-foreground'
: 'text-foreground-secondary hover:text-foreground hover:bg-surface-secondary'
}`}
>
Debug JSON
</button>
</Button>
</div>
</div>
@ -776,13 +769,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
<span className="text-sm">Loading older messages...</span>
</div>
) : (
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={loadMoreMessages}
className="text-sm text-primary hover:text-primary/80 transition-colors"
className="text-primary hover:text-primary/80"
>
Load older messages
</button>
</Button>
)}
</div>
) : null,
@ -950,22 +944,23 @@ export default function PublicAgentChatPage(): React.ReactElement {
{/* Error Message */}
{error && (
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
<div className="px-4 py-2 bg-error/10 border-t border-error/20">
<p className="text-sm text-error">{error}</p>
</div>
)}
{/* Input Area */}
<div className="border-t border-border p-4">
<div className="flex items-end gap-2">
<textarea
<Textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
rows={1}
className="flex-1 px-4 py-3 bg-background border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
resize="none"
className="flex-1 min-h-[48px] max-h-[200px]"
style={{
height: 'auto',
minHeight: '48px',

View file

@ -3,6 +3,7 @@
import type { AIProvider } from '@tpmjs/types/agent';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
@ -87,14 +88,10 @@ function ToolCallCard({
const effectiveStatus = hasError ? 'error' : toolCall.status;
const statusColors = {
pending:
'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30',
running:
'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-500/30',
success:
'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-500/30',
error:
'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-500/30',
pending: 'bg-warning/10 text-warning border-warning/30',
running: 'bg-info/10 text-info border-info/30',
success: 'bg-success/10 text-success border-success/30',
error: 'bg-error/10 text-error border-error/30',
};
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
@ -114,13 +111,13 @@ function ToolCallCard({
return (
<div
className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-red-500/50 bg-red-50 dark:bg-red-500/5' : 'border-border bg-slate-50 dark:bg-surface-secondary/50'}`}
className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-error/50 bg-error/5' : 'border-border bg-surface-secondary/50'}`}
>
{/* Header */}
<button
type="button"
<Button
variant="ghost"
onClick={onToggle}
className="w-full flex items-center gap-3 p-3 hover:bg-slate-100 dark:hover:bg-surface-secondary/80 transition-colors"
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
>
<div className={`p-1.5 rounded ${statusColors[effectiveStatus]}`}>
<Icon
@ -136,14 +133,14 @@ function ToolCallCard({
{toolCall.toolCallId.slice(0, 8)}...
</span>
{hasError && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-error/10 text-error">
ERROR
</span>
)}
</div>
{/* Show error message preview in header */}
{hasError && errorMessage && !isExpanded && (
<div className="text-red-600 dark:text-red-400 text-[10px] mt-1 truncate max-w-[300px]">
<div className="text-error text-[10px] mt-1 truncate max-w-[300px]">
{errorMessage}
</div>
)}
@ -153,7 +150,7 @@ function ToolCallCard({
size="xs"
className={`text-foreground-tertiary transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
</Button>
{/* Expanded Content */}
{isExpanded && (
@ -175,14 +172,14 @@ function ToolCallCard({
{/* Error Message Section */}
{hasError && errorMessage && (
<div className="p-3 bg-red-100 dark:bg-red-500/10 border-b border-red-200 dark:border-red-500/20">
<div className="p-3 bg-error/10 border-b border-error/20">
<div className="flex items-center gap-2 mb-2">
<Icon icon="alertCircle" size="xs" className="text-red-600 dark:text-red-400" />
<span className="text-[10px] uppercase tracking-wider text-red-600 dark:text-red-400 font-semibold">
<Icon icon="alertCircle" size="xs" className="text-error" />
<span className="text-[10px] uppercase tracking-wider text-error font-semibold">
Error
</span>
</div>
<p className="text-[11px] text-red-600 dark:text-red-400">{errorMessage}</p>
<p className="text-[11px] text-error">{errorMessage}</p>
</div>
)}
@ -196,7 +193,7 @@ function ToolCallCard({
<div className="flex-1 h-px bg-border/50" />
</div>
<pre
className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-red-600 dark:text-red-300' : 'text-emerald-700 dark:text-green-400'}`}
className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-error' : 'text-success'}`}
>
{formatJson(toolCall.output)}
</pre>
@ -573,19 +570,21 @@ export default function AgentChatPage(): React.ReactElement {
</p>
) : (
conversations.map((conv) => (
<button
<Button
key={conv.id}
type="button"
variant="ghost"
onClick={() => handleSelectConversation(conv.slug)}
className={`w-full text-left px-3 py-2 rounded-lg mb-1 transition-colors ${
className={`w-full text-left h-auto py-2 px-3 justify-start mb-1 ${
chatId === conv.slug
? 'bg-primary/10 text-primary'
: 'text-foreground-secondary hover:bg-surface-secondary'
}`}
>
<p className="text-sm font-medium truncate">{conv.title || 'Untitled Chat'}</p>
<p className="text-xs text-foreground-tertiary">{conv.messageCount} messages</p>
</button>
<div className="flex flex-col items-start">
<p className="text-sm font-medium truncate">{conv.title || 'Untitled Chat'}</p>
<p className="text-xs text-foreground-tertiary">{conv.messageCount} messages</p>
</div>
</Button>
))
)}
</div>
@ -738,22 +737,23 @@ export default function AgentChatPage(): React.ReactElement {
{/* Error Message */}
{error && (
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
<div className="px-4 py-2 bg-error/10 border-t border-error/20">
<p className="text-sm text-error">{error}</p>
</div>
)}
{/* Input Area */}
<div className="border-t border-border p-4">
<div className="flex items-end gap-2">
<textarea
<Textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
rows={1}
className="flex-1 px-4 py-3 bg-surface border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
resize="none"
className="flex-1 min-h-[48px] max-h-[200px]"
style={{
height: 'auto',
minHeight: '48px',

View file

@ -7,8 +7,12 @@ import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import { Select } from '@tpmjs/ui/Select/Select';
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
import { Switch } from '@tpmjs/ui/Switch/Switch';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import {
Table,
TableBody,
@ -259,17 +263,12 @@ conv = resp.json() # conv['data']['messages']`,
onTabChange={setActiveSection}
size="sm"
/>
<select
<Select
value={effectiveLang}
onChange={(e) => setActiveLang(e.target.value)}
className="px-2 py-1 text-sm bg-surface border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
{langOptions.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.label}
</option>
))}
</select>
size="sm"
options={langOptions.map((opt) => ({ value: opt.id, label: opt.label }))}
/>
</div>
<div className="p-4">
@ -1067,121 +1066,82 @@ export default function AgentDetailPage(): React.ReactElement {
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="name" className="block text-sm font-medium text-foreground mb-1">
Name
</label>
<input
<Label htmlFor="name">Name</Label>
<Input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
<div>
<label htmlFor="uid" className="block text-sm font-medium text-foreground mb-1">
UID
</label>
<input
<Label htmlFor="uid">UID</Label>
<Input
type="text"
id="uid"
name="uid"
value={formData.uid}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
</div>
<div>
<label
htmlFor="description"
className="block text-sm font-medium text-foreground mb-1"
>
Description
</label>
<textarea
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
rows={2}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
resize="none"
size="sm"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label
htmlFor="provider"
className="block text-sm font-medium text-foreground mb-1"
>
Provider
</label>
<select
<Label htmlFor="provider">Provider</Label>
<Select
id="provider"
name="provider"
value={formData.provider}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
>
{SUPPORTED_PROVIDERS.map((p) => (
<option key={p} value={p}>
{PROVIDER_DISPLAY_NAMES[p]}
</option>
))}
</select>
options={SUPPORTED_PROVIDERS.map((p) => ({
value: p,
label: PROVIDER_DISPLAY_NAMES[p],
}))}
/>
</div>
<div>
<label
htmlFor="modelId"
className="block text-sm font-medium text-foreground mb-1"
>
Model
</label>
<select
<Label htmlFor="modelId">Model</Label>
<Select
id="modelId"
name="modelId"
value={formData.modelId}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
options={models.map((m) => ({ value: m.id, label: m.name }))}
/>
</div>
</div>
<div>
<label
htmlFor="systemPrompt"
className="block text-sm font-medium text-foreground mb-1"
>
System Prompt
</label>
<textarea
<Label htmlFor="systemPrompt">System Prompt</Label>
<Textarea
id="systemPrompt"
name="systemPrompt"
value={formData.systemPrompt}
onChange={handleChange}
rows={6}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
resize="none"
/>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div>
<label
htmlFor="temperature"
className="block text-sm font-medium text-foreground mb-1"
>
Temperature
</label>
<input
<Label htmlFor="temperature">Temperature</Label>
<Input
type="number"
id="temperature"
name="temperature"
@ -1190,17 +1150,11 @@ export default function AgentDetailPage(): React.ReactElement {
min={0}
max={2}
step={0.1}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
<div>
<label
htmlFor="maxToolCallsPerTurn"
className="block text-sm font-medium text-foreground mb-1"
>
Max Tool Calls
</label>
<input
<Label htmlFor="maxToolCallsPerTurn">Max Tool Calls</Label>
<Input
type="number"
id="maxToolCallsPerTurn"
name="maxToolCallsPerTurn"
@ -1208,17 +1162,11 @@ export default function AgentDetailPage(): React.ReactElement {
onChange={handleChange}
min={1}
max={100}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
<div>
<label
htmlFor="maxMessagesInContext"
className="block text-sm font-medium text-foreground mb-1"
>
Context Messages
</label>
<input
<Label htmlFor="maxMessagesInContext">Context Messages</Label>
<Input
type="number"
id="maxMessagesInContext"
name="maxMessagesInContext"
@ -1226,7 +1174,6 @@ export default function AgentDetailPage(): React.ReactElement {
onChange={handleChange}
min={1}
max={100}
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
</div>

View file

@ -3,6 +3,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import {
Table,
TableBody,
@ -277,16 +278,13 @@ export default function TpmjsApiKeysPage(): React.ReactElement {
<div className="bg-surface border border-border rounded-lg p-6 mb-6">
<h2 className="text-lg font-medium text-foreground mb-4">Create API Key</h2>
{createError && <p className="text-error text-sm mb-3">{createError}</p>}
<div className="space-y-3">
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
placeholder="Key name (e.g., Production Server, CI/CD)"
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<Input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
placeholder="Key name (e.g., Production Server, CI/CD)"
/>
<div className="flex items-center gap-2 mt-4">
<Button onClick={handleCreate} disabled={creating || !newKeyName.trim()}>
{creating ? 'Creating...' : 'Create Key'}

View file

@ -3,6 +3,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Select } from '@tpmjs/ui/Select/Select';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
@ -107,17 +108,16 @@ export default function UsagePage(): React.ReactElement {
title="Usage"
subtitle="Monitor your API usage and costs"
actions={
<div className="flex items-center gap-2">
<select
value={period}
onChange={(e) => setPeriod(e.target.value as Period)}
className="px-3 py-1.5 bg-surface border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="hourly">Hourly</option>
<option value="daily">Daily</option>
<option value="monthly">Monthly</option>
</select>
</div>
<Select
value={period}
onChange={(e) => setPeriod(e.target.value as Period)}
size="sm"
options={[
{ value: 'hourly', label: 'Hourly' },
{ value: 'daily', label: 'Daily' },
{ value: 'monthly', label: 'Monthly' },
]}
/>
}
>
{isLoading ? (

View file

@ -88,8 +88,8 @@ export default function AuthenticationPage(): React.ReactElement {
<li>Copy your key and store it securely</li>
</ol>
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
<p className="text-amber-600 dark:text-amber-400 font-medium">Important</p>
<div className="bg-warning/10 border border-warning/20 rounded-lg p-4">
<p className="text-warning font-medium">Important</p>
<p className="text-foreground-secondary text-sm mt-1">
Your API key is displayed only once when created. Make sure to copy and store it
securely. If you lose it, you&apos;ll need to generate a new one.

View file

@ -120,10 +120,10 @@ function EndpointCard({
children?: React.ReactNode;
}) {
const methodColors = {
GET: 'bg-green-500/10 text-green-500 border-green-500/30',
POST: 'bg-blue-500/10 text-blue-500 border-blue-500/30',
PUT: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
DELETE: 'bg-red-500/10 text-red-500 border-red-500/30',
GET: 'bg-success/10 text-success border-success/30',
POST: 'bg-info/10 text-info border-info/30',
PUT: 'bg-warning/10 text-warning border-warning/30',
DELETE: 'bg-error/10 text-error border-error/30',
};
return (
@ -377,7 +377,7 @@ export default function APIDocsPage(): React.ReactElement {
/>
</div>
<div className="p-4 border border-yellow-500/30 rounded-lg bg-yellow-500/5">
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5">
<h3 className="font-semibold text-foreground mb-2">API Key Scopes</h3>
<ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
<li>
@ -398,7 +398,7 @@ export default function APIDocsPage(): React.ReactElement {
</ul>
</div>
<div className="p-4 border border-green-500/30 rounded-lg bg-green-500/5">
<div className="p-4 border border-success/30 rounded-lg bg-success/5">
<h3 className="font-semibold text-foreground mb-2">Rate Limits</h3>
<ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
<li>FREE tier: 100 requests/hour</li>

View file

@ -174,10 +174,10 @@ export default function ExecutorsDocsPage(): React.ReactElement {
<li>Optionally add an API key if your executor requires authentication</li>
<li>Click &quot;Verify Connection&quot; to test the configuration</li>
</ol>
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<p className="text-sm text-amber-600 dark:text-amber-400">
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
<p className="text-sm text-warning">
<strong>Security tip:</strong> Set the{' '}
<code className="px-1 bg-amber-500/20 rounded">EXECUTOR_API_KEY</code> environment
<code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> environment
variable in your Vercel project to require authentication for all requests.
</p>
</div>
@ -214,7 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
{/* GET /health */}
<div className="mb-8">
<h3 className="text-lg font-medium text-foreground mb-2">
<code className="px-2 py-1 bg-green-500/10 text-green-500 rounded">GET</code>{' '}
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code>{' '}
/health
</h3>
<p className="text-foreground-secondary mb-4">

View file

@ -224,15 +224,15 @@ const slides: Slide[] = [
</p>
<ul className="space-y-2 text-sm text-foreground-secondary">
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>Execute JavaScript/Python</span>
</li>
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>Fetch web pages</span>
</li>
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>Web search</span>
</li>
</ul>
@ -244,15 +244,15 @@ const slides: Slide[] = [
</p>
<ul className="space-y-2 text-sm text-foreground-secondary">
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>Pre-curated tool sets</span>
</li>
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>One-click to add many tools</span>
</li>
<li className="flex items-center gap-2">
<span className="text-green-500">✓</span>
<span className="text-success">✓</span>
<span>Community collections</span>
</li>
</ul>
@ -308,7 +308,7 @@ const slides: Slide[] = [
</div>
</div>
</div>
<div className="max-w-2xl mx-auto p-4 bg-green-500/10 border border-green-500/20 rounded-lg">
<div className="max-w-2xl mx-auto p-4 bg-success/10 border border-success/20 rounded-lg">
<p className="text-sm text-foreground text-center">
🎉 <strong>Conversations are automatically saved</strong> — pick up where you left off
anytime!

View file

@ -66,23 +66,23 @@ const slides: Slide[] = [
<h4 className="text-lg font-semibold text-foreground mb-3">Cloud-Only Tools</h4>
<ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Code execution (sandboxed)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Web fetching</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Web search</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>Browser automation</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>Local file access</span>
</li>
</ul>
@ -91,23 +91,23 @@ const slides: Slide[] = [
<h4 className="text-lg font-semibold text-foreground mb-3">With Bridge</h4>
<ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Chrome DevTools control</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Read/write local files</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Local database access</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Custom internal APIs</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Any stdio MCP server</span>
</li>
</ul>
@ -439,13 +439,13 @@ Opening browser for authentication...
</div>
<div className="flex justify-center gap-3 text-sm text-foreground-secondary">
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Chrome automation
<span className="text-success">✓</span> Chrome automation
</span>
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Local files
<span className="text-success">✓</span> Local files
</span>
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Custom tools
<span className="text-success">✓</span> Custom tools
</span>
</div>
</div>

View file

@ -176,15 +176,15 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
<CodeBlock language="bash" code={envVarsExample} />
<div className="mt-4 p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<div className="mt-4 p-4 bg-warning/10 border border-warning/30 rounded-lg">
<div className="flex items-start gap-2">
<Icon icon="alertCircle" className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
<Icon icon="alertCircle" className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-amber-600 dark:text-amber-400">
<p className="font-medium text-warning">
Security Recommendation
</p>
<p className="text-sm text-foreground-secondary mt-1">
Set <code className="px-1 bg-amber-500/20 rounded">EXECUTOR_API_KEY</code> to
Set <code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> to
require authentication. Without it, anyone with your executor URL can execute
tools.
</p>
@ -221,12 +221,12 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
</div>
</div>
<div className="mt-4 p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<div className="mt-4 p-4 bg-success/10 border border-success/30 rounded-lg">
<div className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<p className="text-sm text-foreground-secondary">
If you see{' '}
<code className="px-1 bg-green-500/20 rounded">
<code className="px-1 bg-success/20 rounded">
&quot;status&quot;: &quot;ok&quot;
</code>
, your executor is running and ready to use!

View file

@ -65,19 +65,19 @@ const slides: Slide[] = [
<h4 className="text-lg font-semibold text-foreground mb-3">Without MCP</h4>
<ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>AI can only process text</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>No access to external data</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>Can&apos;t take real actions</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-0.5">✗</span>
<span className="text-error mt-0.5">✗</span>
<span>Limited to knowledge cutoff</span>
</li>
</ul>
@ -86,19 +86,19 @@ const slides: Slide[] = [
<h4 className="text-lg font-semibold text-foreground mb-3">With MCP + TPMJS</h4>
<ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Execute code in 40+ languages</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Fetch data from any URL</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Search the web in real-time</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-500 mt-0.5">✓</span>
<span className="text-success mt-0.5">✓</span>
<span>Use specialized tools</span>
</li>
</ul>
@ -362,16 +362,16 @@ const slides: Slide[] = [
</div>
<div className="flex justify-center gap-3 text-sm text-foreground-secondary">
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Code execution
<span className="text-success">✓</span> Code execution
</span>
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Web fetching
<span className="text-success">✓</span> Web fetching
</span>
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> Web search
<span className="text-success">✓</span> Web search
</span>
<span className="flex items-center gap-1">
<span className="text-green-500">✓</span> And more!
<span className="text-success">✓</span> And more!
</span>
</div>
</div>

View file

@ -154,7 +154,7 @@ export default function PublishPage(): React.ReactElement {
</div>
{/* Auto-discovery callout */}
<div className="mb-8 p-6 border-2 border-amber-500/30 rounded-lg bg-amber-500/5">
<div className="mb-8 p-6 border-2 border-warning/30 rounded-lg bg-warning/5">
<div className="flex items-start gap-4">
<div className="text-3xl">🔍</div>
<div>

View file

@ -2,6 +2,7 @@
import { TPMJS_CATEGORIES } from '@tpmjs/types/tpmjs';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
import { Container } from '@tpmjs/ui/Container/Container';
import { useState } from 'react';
@ -122,28 +123,22 @@ export default function SpecPage(): React.ReactElement {
{/* View Toggle */}
<div className="flex gap-1 p-1 bg-surface rounded-lg border border-border self-center md:self-auto">
<button
type="button"
<Button
variant={view === 'spec' ? 'default' : 'ghost'}
size="sm"
onClick={() => setView('spec')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
view === 'spec'
? 'bg-foreground text-background'
: 'text-foreground-secondary hover:text-foreground'
}`}
className="rounded-md"
>
Specification
</button>
<button
type="button"
</Button>
<Button
variant={view === 'example' ? 'default' : 'ghost'}
size="sm"
onClick={() => setView('example')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
view === 'example'
? 'bg-foreground text-background'
: 'text-foreground-secondary hover:text-foreground'
}`}
className="rounded-md"
>
Full Example
</button>
</Button>
</div>
</div>
</Container>

View file

@ -1,3 +1,4 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Container } from '@tpmjs/ui/Container/Container';
import Link from 'next/link';
import { AppHeader } from '~/components/AppHeader';
@ -450,20 +451,10 @@ export default function TermsPage(): React.ReactElement {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="mailto:hello@tpmjs.com">
<button
type="button"
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Contact Us
</button>
<Button size="lg">Contact Us</Button>
</a>
<Link href="/">
<button
type="button"
className="px-6 py-3 border border-border rounded-lg font-medium hover:bg-surface transition-colors text-foreground"
>
Back to Home
</button>
<Button variant="outline" size="lg">Back to Home</Button>
</Link>
</div>
</div>

View file

@ -2,6 +2,8 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import { useCallback, useMemo, useRef, useState } from 'react';
import { parseEnvString } from '~/lib/utils/env-parser';
@ -148,9 +150,6 @@ export function EnvVarsEditor({
return parseEnvString(pasteContent);
}, [pasteContent]);
const inputClassName =
'flex-1 px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary disabled:opacity-50 disabled:cursor-not-allowed';
return (
<div className={className}>
{/* Header */}
@ -182,11 +181,12 @@ export function EnvVarsEditor({
<Icon icon="x" size="xs" />
</Button>
</div>
<textarea
<Textarea
value={pasteContent}
onChange={(e) => setPasteContent(e.target.value)}
placeholder={`# Paste your .env content here\nAPI_KEY=your-api-key\nDATABASE_URL="postgres://..."\n`}
className="w-full h-32 px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-xs focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
className="h-32 text-xs"
resize="none"
disabled={disabled}
/>
{parsedPreview.length > 0 && (
@ -217,20 +217,20 @@ export function EnvVarsEditor({
<div className="space-y-2 mb-3">
{envVars.map((env, index) => (
<div key={`env-${env.key || index}`} className="flex items-center gap-2">
<input
<Input
type="text"
value={env.key}
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
placeholder="KEY"
className={inputClassName}
size="sm"
disabled={disabled}
/>
<input
<Input
type="password"
value={env.value}
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
placeholder="value"
className={inputClassName}
size="sm"
disabled={disabled}
/>
<Button
@ -249,12 +249,12 @@ export function EnvVarsEditor({
{/* Add new env var */}
<div className="flex items-center gap-2">
<input
<Input
type="text"
value={newEnvKey}
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
placeholder={keyPlaceholder}
className={inputClassName}
size="sm"
disabled={disabled}
onKeyDown={(e) => {
if (e.key === 'Enter' && newEnvKey.trim()) {
@ -262,12 +262,12 @@ export function EnvVarsEditor({
}
}}
/>
<input
<Input
type="text"
value={newEnvValue}
onChange={(e) => setNewEnvValue(e.target.value)}
placeholder={valuePlaceholder}
className={inputClassName}
size="sm"
disabled={disabled}
onKeyDown={(e) => {
if (e.key === 'Enter' && newEnvKey.trim()) {