refactor(web): replace raw HTML with design system components

- Replace raw <label> with Label component in auth pages, profile settings, tool-ideas
- Replace raw <button> with Button in CopyButton, CopyDropdown, ExecutorConfigPanel, PackageManagerSelector, ToolPlayground
- Replace raw <textarea> with Textarea in ToolPlayground
- Replace inline SVG with Icon component in error.tsx
This commit is contained in:
Ajax Davis 2026-01-16 08:51:31 +10:00
parent 144f07ea75
commit 0770d88815
10 changed files with 122 additions and 160 deletions

View file

@ -2,6 +2,7 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import Link from 'next/link';
import { useState } from 'react';
@ -63,16 +64,17 @@ export default function ForgotPasswordPage() {
<p className="text-center text-sm text-foreground-secondary">
Didn&apos;t receive the email?{' '}
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={() => {
setSuccess(false);
setEmail('');
}}
className="text-foreground hover:underline font-medium"
className="p-0 h-auto text-foreground hover:underline font-medium"
>
Try again
</button>
</Button>
</p>
<p className="text-center text-sm text-foreground-secondary">
@ -101,9 +103,7 @@ export default function ForgotPasswordPage() {
)}
<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"

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 { suggestUsername } from '@tpmjs/types/user';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
@ -182,9 +183,7 @@ export default function SignUpPage() {
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-foreground mb-1">
Name
</label>
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
@ -196,9 +195,7 @@ export default function SignUpPage() {
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
Username
</label>
<Label htmlFor="username">Username</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-foreground-secondary">
@
@ -245,9 +242,7 @@ export default function SignUpPage() {
</div>
<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"
@ -259,9 +254,7 @@ export default function SignUpPage() {
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
Password
</label>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"

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 { useSearchParams } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';
@ -222,9 +223,7 @@ function ProfileSettingsContent({ isSetupMode }: { isSetupMode: boolean }): Reac
{/* Name field */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-foreground mb-1">
Display Name
</label>
<Label htmlFor="name">Display Name</Label>
<Input
id="name"
type="text"
@ -240,9 +239,7 @@ function ProfileSettingsContent({ isSetupMode }: { isSetupMode: boolean }): Reac
{/* Username field */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
Username
</label>
<Label htmlFor="username">Username</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-foreground-secondary z-10">
@
@ -292,9 +289,7 @@ function ProfileSettingsContent({ isSetupMode }: { isSetupMode: boolean }): Reac
{/* Email (read-only) */}
<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"

View file

@ -2,6 +2,7 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Container } from '@tpmjs/ui/Container/Container';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { useEffect } from 'react';
@ -25,23 +26,7 @@ export default function ErrorPage({
{/* Error Icon */}
<div className="mb-8">
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-error/10 mb-4">
<svg
className="w-12 h-12 text-error"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="error-icon-title"
>
<title id="error-icon-title">Error warning icon</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<Icon icon="alertTriangle" className="w-12 h-12 text-error" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-2">
Something went wrong

View file

@ -3,6 +3,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import { ProgressBar } from '@tpmjs/ui/ProgressBar/ProgressBar';
import { Select } from '@tpmjs/ui/Select/Select';
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
@ -99,9 +100,9 @@ export function ToolIdeasClient() {
}, [fetchTools]);
const qualityColor = (score: number) => {
if (score >= 0.9) return 'text-green-600';
if (score >= 0.7) return 'text-yellow-600';
return 'text-red-600';
if (score >= 0.9) return 'text-success';
if (score >= 0.7) return 'text-warning';
return 'text-error';
};
const ToolCard = ({ tool }: { tool: ToolIdea }) => {
@ -227,12 +228,7 @@ export function ToolIdeasClient() {
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 bg-surface p-4 rounded-lg border border-border">
<div>
<label
htmlFor="tool-search"
className="text-xs font-medium text-foreground-secondary mb-1.5 block"
>
Search
</label>
<Label htmlFor="tool-search" className="text-xs mb-1.5">Search</Label>
<Input
id="tool-search"
type="text"
@ -244,12 +240,7 @@ export function ToolIdeasClient() {
</div>
<div>
<label
htmlFor="tool-category"
className="text-xs font-medium text-foreground-secondary mb-1.5 block"
>
Category
</label>
<Label htmlFor="tool-category" className="text-xs mb-1.5">Category</Label>
<Select
id="tool-category"
value={category}
@ -260,12 +251,7 @@ export function ToolIdeasClient() {
</div>
<div>
<label
htmlFor="tool-verb"
className="text-xs font-medium text-foreground-secondary mb-1.5 block"
>
Verb
</label>
<Label htmlFor="tool-verb" className="text-xs mb-1.5">Verb</Label>
<Select
id="tool-verb"
value={verb}
@ -276,12 +262,7 @@ export function ToolIdeasClient() {
</div>
<div>
<label
htmlFor="tool-quality"
className="text-xs font-medium text-foreground-secondary mb-1.5 block"
>
Min Quality
</label>
<Label htmlFor="tool-quality" className="text-xs mb-1.5">Min Quality</Label>
<Select
id="tool-quality"
value={minQuality}

View file

@ -1,5 +1,6 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
@ -33,16 +34,19 @@ export function CopyButton({
}, [text, successMessage]);
const iconSize = size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md';
// Map xs to sm for Button since Button doesn't support xs size
const buttonSize = size === 'xs' ? 'sm' : size;
return (
<button
type="button"
<Button
variant="ghost"
size={buttonSize}
onClick={handleCopy}
className={`inline-flex items-center gap-1.5 px-2 py-1 text-foreground-secondary hover:text-foreground hover:bg-surface rounded transition-colors ${className}`}
className={`inline-flex items-center gap-1.5 text-foreground-secondary hover:text-foreground ${className}`}
title={label || 'Copy to clipboard'}
>
<Icon icon={copied ? 'check' : 'copy'} size={iconSize} />
{label && <span className="text-xs">{label}</span>}
</button>
</Button>
);
}

View file

@ -1,5 +1,6 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
@ -46,10 +47,11 @@ export function CopyDropdown({
return (
<div className={`relative ${className}`} ref={dropdownRef}>
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-foreground-secondary hover:text-foreground hover:bg-surface rounded transition-colors"
className="inline-flex items-center gap-1 text-xs text-foreground-secondary hover:text-foreground"
>
<Icon icon="copy" size="xs" />
<span>{buttonLabel}</span>
@ -58,24 +60,26 @@ export function CopyDropdown({
size="xs"
className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}
/>
</button>
</Button>
{isOpen && (
<div className="absolute right-0 top-full mt-1 w-56 bg-background border border-border rounded-lg shadow-lg py-1 z-50">
{options.map((option) => (
<button
<Button
key={option.label}
type="button"
variant="ghost"
onClick={() => handleCopy(option)}
className="w-full px-3 py-2 text-left text-sm hover:bg-surface transition-colors"
className="w-full px-3 py-2 h-auto text-left justify-start hover:bg-surface"
>
<div className="font-medium text-foreground">{option.label}</div>
{option.description && (
<div className="text-xs text-foreground-tertiary truncate">
{option.description}
</div>
)}
</button>
<div className="flex flex-col items-start">
<div className="font-medium text-foreground">{option.label}</div>
{option.description && (
<div className="text-xs text-foreground-tertiary truncate max-w-full">
{option.description}
</div>
)}
</div>
</Button>
))}
</div>
)}

View file

@ -142,67 +142,71 @@ export function ExecutorConfigPanel({
{/* Executor Type Selection */}
<div className="flex gap-3">
<button
type="button"
<Button
variant="ghost"
onClick={() => handleTypeChange('default')}
disabled={disabled}
className={`flex-1 p-3 rounded-lg border-2 text-left transition-colors ${
className={`flex-1 p-3 h-auto rounded-lg border-2 text-left justify-start transition-colors ${
executorType === 'default'
? 'border-primary bg-primary/5'
: 'border-border hover:border-foreground-secondary'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
}`}
>
<div className="flex items-center gap-2">
<div
className={`w-4 h-4 rounded-full border-2 ${
executorType === 'default'
? 'border-primary bg-primary'
: 'border-foreground-tertiary'
}`}
>
{executorType === 'default' && (
<div className="w-full h-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
)}
<div className="flex flex-col items-start">
<div className="flex items-center gap-2">
<div
className={`w-4 h-4 rounded-full border-2 ${
executorType === 'default'
? 'border-primary bg-primary'
: 'border-foreground-tertiary'
}`}
>
{executorType === 'default' && (
<div className="w-full h-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
)}
</div>
<span className="font-medium text-sm text-foreground">TPMJS Default</span>
</div>
<span className="font-medium text-sm text-foreground">TPMJS Default</span>
<p className="text-xs text-foreground-tertiary mt-1 ml-6">
Free, managed executor hosted by TPMJS
</p>
</div>
<p className="text-xs text-foreground-tertiary mt-1 ml-6">
Free, managed executor hosted by TPMJS
</p>
</button>
</Button>
<button
type="button"
<Button
variant="ghost"
onClick={() => handleTypeChange('custom_url')}
disabled={disabled}
className={`flex-1 p-3 rounded-lg border-2 text-left transition-colors ${
className={`flex-1 p-3 h-auto rounded-lg border-2 text-left justify-start transition-colors ${
executorType === 'custom_url'
? 'border-primary bg-primary/5'
: 'border-border hover:border-foreground-secondary'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
}`}
>
<div className="flex items-center gap-2">
<div
className={`w-4 h-4 rounded-full border-2 ${
executorType === 'custom_url'
? 'border-primary bg-primary'
: 'border-foreground-tertiary'
}`}
>
{executorType === 'custom_url' && (
<div className="w-full h-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
)}
<div className="flex flex-col items-start">
<div className="flex items-center gap-2">
<div
className={`w-4 h-4 rounded-full border-2 ${
executorType === 'custom_url'
? 'border-primary bg-primary'
: 'border-foreground-tertiary'
}`}
>
{executorType === 'custom_url' && (
<div className="w-full h-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
)}
</div>
<span className="font-medium text-sm text-foreground">Custom Executor</span>
</div>
<span className="font-medium text-sm text-foreground">Custom Executor</span>
<p className="text-xs text-foreground-tertiary mt-1 ml-6">
Self-hosted executor on your infrastructure
</p>
</div>
<p className="text-xs text-foreground-tertiary mt-1 ml-6">
Self-hosted executor on your infrastructure
</p>
</button>
</Button>
</div>
{/* Custom URL Configuration */}

View file

@ -1,5 +1,6 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { useCallback, useEffect, useState } from 'react';
export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
@ -59,18 +60,19 @@ export function PackageManagerSelector({
<span className="text-sm text-foreground-secondary mr-1">Package Manager:</span>
<div className="inline-flex rounded-lg border border-border overflow-hidden">
{packageManagers.map((pm) => (
<button
<Button
key={pm.id}
type="button"
variant={selected === pm.id ? 'default' : 'ghost'}
size="sm"
onClick={() => handleSelect(pm.id)}
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
className={`px-3 py-1.5 text-xs font-medium rounded-none ${
selected === pm.id
? 'bg-primary text-primary-foreground'
? ''
: 'bg-background text-foreground-secondary hover:bg-surface hover:text-foreground'
}`}
>
{pm.label}
</button>
</Button>
))}
</div>
</div>

View file

@ -7,7 +7,10 @@
import type { TokenBreakdown as TokenData } from '@/lib/ai-agent/tool-executor-agent';
import type { Package, Tool } from '@tpmjs/db';
import { Button } from '@tpmjs/ui/Button/Button';
import { Label } from '@tpmjs/ui/Label/Label';
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@ -211,11 +214,11 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
<div className="border-b border-border bg-muted/10">
<div className="flex space-x-1 px-6">
{tabs.map((tab) => (
<button
<Button
key={tab.id}
type="button"
variant="ghost"
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-3 text-sm font-medium transition-colors relative ${
className={`px-4 py-3 rounded-none text-sm font-medium transition-colors relative ${
activeTab === tab.id
? 'text-foreground border-b-2 border-primary'
: 'text-foreground-secondary hover:text-foreground'
@ -227,7 +230,7 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
{tab.badge}
</span>
)}
</button>
</Button>
))}
</div>
</div>
@ -237,15 +240,14 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
{activeTab === 'input' && (
<div className="space-y-4">
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-foreground mb-2">
Prompt
</label>
<textarea
<Label htmlFor="prompt" className="mb-2">Prompt</Label>
<Textarea
id="prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Enter your prompt here... (e.g., 'Create a blog post about TypeScript best practices')"
className="w-full h-32 px-4 py-3 rounded-lg border border-input bg-white text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-ring resize-none"
className="h-32"
resize="none"
disabled={isExecuting}
/>
<p className="text-xs text-foreground-tertiary mt-2">
@ -253,21 +255,13 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
</p>
</div>
<button
type="button"
<Button
onClick={handleExecute}
disabled={isExecuting || !prompt.trim()}
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
loading={isExecuting}
>
{isExecuting ? (
<span className="flex items-center gap-2">
<Spinner size="sm" />
Executing...
</span>
) : (
'Execute'
)}
</button>
{isExecuting ? 'Executing...' : 'Execute'}
</Button>
</div>
)}