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

@ -0,0 +1,102 @@
---
description: Develop and validate TPMJS tools using the blocks CLI
---
Help the user develop new tools for the TPMJS registry using the blocks CLI. This workflow covers defining tools in blocks.yml, implementing them with AI SDK v6, validating with the blocks CLI, and publishing to npm.
## Development Workflow
### 1. Define Tool in blocks.yml
Add tool definition to `packages/tools/official/blocks.yml`:
```yaml
blocks:
category.toolName:
type: utility
description: "Clear description for LLMs"
path: "tool-directory-name"
domain_rules:
- id: rule_name
description: "Implementation requirement"
inputs:
- name: paramName
type: string
description: "Parameter description"
outputs:
- name: result
type: ResultType
description: "Output description"
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
```
### 2. Create Package Structure
```
packages/tools/official/tool-name/
├── package.json # npm package with tpmjs field
├── tsconfig.json # Extends @tpmjs/tsconfig
├── tsup.config.ts # Build config
├── block.ts # REQUIRED by validator
├── index.ts # Re-export from src
└── src/index.ts # Main implementation
```
### 3. Implement with AI SDK v6
```typescript
import { jsonSchema, tool } from 'ai';
export const myTool = tool({
description: 'Description for LLMs',
parameters: jsonSchema<InputType>({
type: 'object',
properties: { /* ... */ },
required: ['field1'],
}),
async execute(input): Promise<OutputType> {
// REAL implementation - no stubs
return result;
},
});
export default myTool;
```
### 4. Run Validation
```bash
cd packages/tools/official
pnpm blocks run tool-name # Validate single tool
pnpm blocks run tool-name --force # Force full validation
pnpm blocks run --all # Validate all tools
```
### 5. Build and Publish
```bash
pnpm build
npm publish --access public
# Trigger sync to tpmjs.com
source apps/web/.env.local
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
```
## Valid Categories
For `tpmjs.category` in package.json: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`
## Required Files
- **block.ts** at root: `export const block = { name: 'tool-name', tools: { myTool } };`
- **index.ts** at root: `export * from './src/index.js';`
- Both are required for the validator to find the tool
## Common Issues
- "invalid tpmjs field" during sync = Invalid category or missing tools array
- "Tool not found in exports" = Export name must match blocks.yml
- "Required file not found" = Need index.ts and block.ts at package root
When helping the user, read the full skill documentation at `.claude/skills/blocks-develop.md` for comprehensive details on entities, measures, and multi-tool packages.

View file

@ -0,0 +1,357 @@
# TPMJS Tool Development with Blocks CLI
Use this skill when developing new tools for the TPMJS registry. This covers the full workflow from defining a tool in blocks.yml through implementation, validation, and publishing.
## Quick Start
```bash
# Navigate to official tools directory
cd packages/tools/official
# Run validation on a specific tool
pnpm blocks run <block-name>
# Run validation on all tools
pnpm blocks run --all
# Force full validation (ignore cache)
pnpm blocks run <block-name> --force
```
## Development Workflow
### 1. Define the Tool Block in blocks.yml
Add your tool definition to `packages/tools/official/blocks.yml` in the `blocks:` section:
```yaml
blocks:
# Category.toolName format
sandbox.myTool:
type: utility
description: "Clear, LLM-friendly description of what the tool does"
path: "my-tool" # Directory name under packages/tools/official/
domain_rules:
- id: rule_name
description: "What this implementation must do"
inputs:
- name: inputName
type: string
description: "Description for LLMs"
- name: optionalInput
type: number
optional: true
description: "Optional parameter"
outputs:
- name: result
type: MyResultType
description: "What the tool returns"
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
```
**Key Fields:**
- `type`: Usually `utility` for single-shot tools
- `path`: Directory name (kebab-case)
- `domain_rules`: Implementation requirements the validator checks
- `inputs/outputs`: Schema for validation
- `measures`: Quality constraints from the domain section
### 2. Create the Tool Package
Create the directory structure:
```
packages/tools/official/my-tool/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── block.ts # Required by validator
├── index.ts # Re-export from src
└── src/
└── index.ts # Main implementation
```
**package.json:**
```json
{
"name": "@tpmjs/tools-my-tool",
"version": "0.1.0",
"description": "Short description for npm",
"type": "module",
"keywords": ["tpmjs", "category-name", "ai"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
},
"dependencies": {
"ai": "6.0.23"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/tpmjs/tpmjs.git",
"directory": "packages/tools/official/my-tool"
},
"homepage": "https://tpmjs.com",
"license": "MIT",
"tpmjs": {
"category": "sandbox",
"frameworks": ["vercel-ai"],
"tools": [
{
"name": "myTool",
"description": "Clear description (20+ chars) of what this tool does."
}
]
}
}
```
**Valid categories for tpmjs.category:**
- `research`, `web`, `data`, `documentation`, `engineering`
- `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`
- `html`, `compliance`
**tsconfig.json:**
```json
{
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
**tsup.config.ts:**
```typescript
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
target: 'es2022',
});
```
**block.ts (Required by validator):**
```typescript
import { myTool } from './src/index.js';
export const block = {
name: 'my-tool',
description: 'Short description',
tools: { myTool },
};
export default block;
```
**index.ts (Root re-export):**
```typescript
export * from './src/index.js';
export { default } from './src/index.js';
```
### 3. Implement the Tool
**src/index.ts:**
```typescript
import { jsonSchema, tool } from 'ai';
// Define input/output types
interface MyToolInput {
param1: string;
param2?: number;
}
interface MyToolResult {
data: string;
metadata: {
processedAt: string;
};
}
// Export the tool using AI SDK v6 pattern
export const myTool = tool({
description: 'Clear description for LLMs explaining what this tool does and when to use it.',
parameters: jsonSchema<MyToolInput>({
type: 'object',
properties: {
param1: {
type: 'string',
description: 'Description of param1',
},
param2: {
type: 'number',
description: 'Optional description of param2',
},
},
required: ['param1'],
}),
async execute(input): Promise<MyToolResult> {
// REAL implementation - no stubs, no TODOs
const result = await doSomething(input.param1);
return {
data: result,
metadata: {
processedAt: new Date().toISOString(),
},
};
},
});
// Default export for compatibility
export default myTool;
```
### 4. Run Validation
```bash
cd packages/tools/official
# Validate your tool
pnpm blocks run my-tool
# The validator runs 3 stages:
# 1. schema - Validates inputs/outputs match blocks.yml
# 2. shape - Verifies exports and structure
# 3. domain - Checks domain rules are satisfied
```
**Common validation errors:**
- `Required file "index.ts" not found` - Need index.ts at package root
- `Required file "block.ts" not found` - Need block.ts at package root
- `Tool "myTool" not found in exports` - Export name must match blocks.yml
- `invalid tpmjs field` - Category must be valid, tools array required
### 5. Build and Publish
```bash
# Build the package
pnpm build
# Publish to npm
npm publish --access public
# Trigger sync to tpmjs.com
source apps/web/.env.local
curl -X POST https://tpmjs.com/api/sync/keyword \
-H "Authorization: Bearer $CRON_SECRET"
```
## Multi-Tool Packages
For packages with multiple tools (like unsandbox):
**blocks.yml:**
```yaml
blocks:
sandbox.executeCodeAsync:
type: utility
path: "unsandbox" # Same path for all tools in package
# ...
sandbox.getJob:
type: utility
path: "unsandbox" # Same path
# ...
```
**block.ts:**
```typescript
import { executeCodeAsync, getJob, listJobs } from './src/index.js';
export const block = {
name: 'unsandbox',
tools: { executeCodeAsync, getJob, listJobs },
};
export default block;
```
**package.json tpmjs field:**
```json
{
"tpmjs": {
"category": "sandbox",
"frameworks": ["vercel-ai"],
"tools": [
{ "name": "executeCodeAsync", "description": "..." },
{ "name": "getJob", "description": "..." },
{ "name": "listJobs", "description": "..." }
]
}
}
```
## Philosophy (from blocks.yml)
- Every tool MUST be a working, production-ready implementation - no stubs, no TODOs
- Tools use AI SDK v6 `tool()` + `jsonSchema()` pattern exclusively
- Each tool does ONE thing exceptionally well (single-shot, one call in, one result out)
- Tools return structured, typed outputs that agents can reliably parse
- Error handling is explicit - throw meaningful errors, never silently fail
- Dependencies are minimal and production-stable
## Domain Entities
When defining outputs, reference existing entities from blocks.yml:
```yaml
# Example entities available:
url: [href, domain, protocol, path, query, fragment]
webpage: [url, title, html, text, metadata]
text_content: [raw, sentences, paragraphs, wordCount]
claim: [statement, confidence, needsCitation, category]
timeline: [events, dateRange, gaps, eventCount]
```
Or define new entities in the `domain.entities` section if needed.
## Quality Measures
Reference these in your tool's `measures` array:
- `working_implementation` - No stubs, TODOs, or placeholders
- `valid_output_structure` - Returns correct typed object
- `proper_error_handling` - Throws descriptive errors
- `ai_sdk_compliance` - Uses tool() and jsonSchema()
- `npm_publishable` - Valid package.json with tpmjs field
- `readme_documentation` - Has README with examples
## Debugging Tips
```bash
# Force rebuild without cache
pnpm blocks run my-tool --force --no-cache
# See JSON output for debugging
pnpm blocks run my-tool --json
# Check if validator finds your package
ls packages/tools/official/my-tool/
# Must have: index.ts, block.ts at root level
```

View file

@ -26,7 +26,37 @@ This project uses a Turborepo monorepo architecture with the following structure
### Architecture Principles ### Architecture Principles
#### 1. Design System First
**Always use `@tpmjs/ui` components instead of raw HTML elements.** This ensures visual consistency, accessibility, and maintainability across the application.
```typescript
// Good - use design system components
import { Button } from '@tpmjs/ui/Button/Button';
import { Input } from '@tpmjs/ui/Input/Input';
import { Table, TableRow, TableCell } from '@tpmjs/ui/Table/Table';
<Button onClick={handleClick}>Submit</Button>
<Input value={value} onChange={onChange} />
// Bad - raw HTML elements
<button onClick={handleClick}>Submit</button>
<input value={value} onChange={onChange} />
```
**When to create/update UI components:**
- If a pattern is used in 2+ places, create a reusable component in `@tpmjs/ui`
- If existing component styling doesn't match the design, update the component (not the usage site)
- If you need virtualization (e.g., `react-virtuoso`), match the design system's styling classes
**Common components to use:**
- `Button` - all clickable actions
- `Input`, `Select`, `Textarea` - form inputs
- `Table`, `TableRow`, `TableCell` - data tables
- `Card` - content containers
- `Badge` - status indicators
- `Icon` - all icons (not inline SVGs)
- `Spinner`, `Skeleton` - loading states
#### 2. No Barrel Exports #### 2. No Barrel Exports

559
DESIGN_SYSTEM.md Normal file
View file

@ -0,0 +1,559 @@
# TPMJS Design System Specification
> A technical, precise design system inspired by [turbopuffer.com](https://turbopuffer.com) - warm, monospace-driven, with generous whitespace and fieldset-style containers.
---
## Brand Direction
### Mood & Personality
- **Technical & Precise** - Engineering-focused, trustworthy, developer-first
- **Warm & Distinctive** - Not cold/corporate, the copper accent adds warmth
- **Confident & Minimal** - Let the content speak, reduce visual noise
### Reference Sites
- [turbopuffer.com](https://turbopuffer.com) - Primary inspiration
- Linear, Vercel - Secondary references for technical clarity
---
## Color Palette
### Primary Accent
```css
--color-accent: #A6592D; /* Copper/terracotta - primary brand color */
--color-accent-hover: #8B4A26; /* Darker copper for hover states */
--color-accent-light: #D4A574; /* Light copper for backgrounds/highlights */
```
### Gradient Header
```css
/* Warm gradient for top bar/hero sections */
--gradient-header: linear-gradient(135deg, #D4732A 0%, #8B3D1A 50%, #2D1810 100%);
```
### Neutral Palette
```css
/* Backgrounds */
--color-bg-primary: #FFFFFF; /* Main background */
--color-bg-secondary: #FAFAFA; /* Subtle sections */
--color-bg-elevated: #FFFFFF; /* Cards, elevated surfaces */
/* Text */
--color-text-primary: #1A1A1A; /* Primary text - near black */
--color-text-secondary: #666666; /* Secondary/muted text */
--color-text-tertiary: #999999; /* Placeholder, hints */
/* Borders */
--color-border: #E5E5E5; /* Default borders */
--color-border-strong: #CCCCCC; /* Emphasized borders */
--color-border-focus: #A6592D; /* Focus state - uses accent */
```
### Semantic Colors
```css
--color-success: #22C55E;
--color-error: #EF4444;
--color-warning: #F59E0B;
--color-info: #3B82F6;
```
### Dark Mode (Future)
```css
/* Dark mode should invert while keeping the warm accent */
--color-bg-primary-dark: #0D0D0D;
--color-bg-secondary-dark: #1A1A1A;
--color-text-primary-dark: #F5F5F5;
--color-border-dark: #333333;
```
---
## Typography
### Font Stack
**Headings & Code: Monospace**
```css
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', monospace;
```
**Body Text: Sans-serif (for longer reading)**
```css
--font-sans: 'Inter', 'IBM Plex Sans', system-ui, sans-serif;
```
### Type Scale
| Element | Font | Size | Weight | Line Height | Letter Spacing |
|---------|------|------|--------|-------------|----------------|
| H1 | Mono | 48px (3rem) | 600 | 1.1 | -0.02em |
| H2 | Mono | 36px (2.25rem) | 600 | 1.2 | -0.01em |
| H3 | Mono | 24px (1.5rem) | 600 | 1.3 | 0 |
| H4 | Mono | 20px (1.25rem) | 600 | 1.4 | 0 |
| Body Large | Sans | 18px (1.125rem) | 400 | 1.7 | 0 |
| Body | Sans | 16px (1rem) | 400 | 1.7 | 0 |
| Body Small | Sans | 14px (0.875rem) | 400 | 1.6 | 0 |
| Caption | Sans | 12px (0.75rem) | 400 | 1.5 | 0.01em |
| Code | Mono | 14px (0.875rem) | 400 | 1.6 | 0 |
### Typography Rules
1. **Headings are lowercase** - "pricing", "faq", "tools" (not "Pricing", "FAQ", "Tools")
2. **Generous line-height** - Minimum 1.6 for body text, 1.7 preferred
3. **Bold sparingly** - Use weight 600 for emphasis, not 700+
4. **Monospace for data** - Numbers, metrics, technical values always in mono
### CSS Variables
```css
/* Font families */
--font-heading: var(--font-mono);
--font-body: var(--font-sans);
--font-code: var(--font-mono);
/* Font sizes */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 2.25rem; /* 36px */
--text-4xl: 3rem; /* 48px */
/* Line heights */
--leading-tight: 1.2;
--leading-normal: 1.5;
--leading-relaxed: 1.7;
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
```
---
## Spacing
### Spacing Scale
```css
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
```
### Spacing Philosophy
- **Generous whitespace** - When in doubt, add more space
- **Vertical rhythm** - Use consistent spacing between sections (typically `--space-16` to `--space-24`)
- **Component padding** - Cards and containers use `--space-6` to `--space-8`
- **Text spacing** - Paragraphs separated by `--space-4` to `--space-6`
---
## Borders & Containers
### Border Radius
```css
--radius-none: 0; /* DEFAULT - sharp corners */
--radius-sm: 2px; /* Use sparingly for special cases */
--radius-md: 4px; /* Use sparingly for special cases */
```
**Rule: Default to 0 border-radius. Sharp corners are the brand.**
### Border Styles
**Dashed (Primary)**
```css
border: 1px dashed var(--color-border);
```
**Solid (Emphasis)**
```css
border: 2px solid var(--color-text-primary); /* Featured items */
```
### Fieldset-Style Containers
The signature container style with a label that "cuts into" the border:
```html
<fieldset class="fieldset-container">
<legend>section title</legend>
<!-- content -->
</fieldset>
```
```css
.fieldset-container {
border: 1px dashed var(--color-border);
padding: var(--space-6);
margin: 0;
}
.fieldset-container legend {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text-secondary);
padding: 0 var(--space-2);
text-transform: lowercase;
}
```
### Container Variants
| Variant | Border | Background | Use Case |
|---------|--------|------------|----------|
| Default | 1px dashed | transparent | Most containers |
| Elevated | 1px dashed | white | Cards on gray bg |
| Featured | 2px solid | white | Highlighted item |
| Ghost | none | transparent | Minimal grouping |
---
## Components
### Buttons
**Primary Button (Accent)**
```css
.btn-primary {
background: var(--color-accent);
color: white;
border: none;
padding: var(--space-3) var(--space-6);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: background 150ms ease;
}
.btn-primary:hover {
background: var(--color-accent-hover);
}
```
**Secondary Button (Outline)**
```css
.btn-secondary {
background: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
padding: var(--space-3) var(--space-6);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: border-color 150ms ease;
}
.btn-secondary:hover {
border-color: var(--color-text-primary);
}
```
**Button Sizes**
| Size | Padding | Font Size |
|------|---------|-----------|
| sm | `--space-2` `--space-4` | `--text-xs` |
| md | `--space-3` `--space-6` | `--text-sm` |
| lg | `--space-4` `--space-8` | `--text-base` |
### Links
```css
a {
color: var(--color-text-primary);
text-decoration: underline;
text-underline-offset: 3px;
transition: opacity 150ms ease;
}
a:hover {
opacity: 0.7;
}
```
**Rule: Links are underlined, not colored.** Use underline as the primary affordance.
### Inputs
```css
.input {
width: 100%;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-none);
font-family: var(--font-mono);
font-size: var(--text-base);
background: white;
transition: border-color 150ms ease;
}
.input:focus {
outline: none;
border-color: var(--color-accent);
}
.input::placeholder {
color: var(--color-text-tertiary);
}
```
### Cards
```css
.card {
border: 1px dashed var(--color-border);
padding: var(--space-6);
background: white;
}
.card--featured {
border: 2px solid var(--color-text-primary);
}
.card__title {
font-family: var(--font-mono);
font-size: var(--text-lg);
font-weight: var(--font-semibold);
text-transform: lowercase;
margin-bottom: var(--space-2);
}
.card__description {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-text-secondary);
line-height: var(--leading-relaxed);
}
```
### Badges
```css
.badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-xs);
border: 1px solid currentColor;
text-transform: lowercase;
}
.badge--default { color: var(--color-text-secondary); }
.badge--success { color: var(--color-success); }
.badge--error { color: var(--color-error); }
.badge--warning { color: var(--color-warning); }
```
### Tables
```css
.table-container {
border: 1px dashed var(--color-border);
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
.table th {
text-align: left;
padding: var(--space-4);
border-bottom: 1px dashed var(--color-border);
font-weight: var(--font-semibold);
text-transform: lowercase;
}
.table td {
padding: var(--space-4);
border-bottom: 1px dashed var(--color-border);
}
.table tr:last-child td {
border-bottom: none;
}
```
---
## Layout
### Container Widths
```css
--container-sm: 640px;
--container-md: 768px;
--container-lg: 1024px;
--container-xl: 1280px;
```
### Page Structure
```
┌─────────────────────────────────────────────┐
│ Gradient Header Bar (announcement) │
├─────────────────────────────────────────────┤
│ Navigation (sticky, white bg) │
├─────────────────────────────────────────────┤
│ │
│ Hero Section │
│ (generous padding: --space-24) │
│ │
├─────────────────────────────────────────────┤
│ │
│ Content Sections │
│ (separated by --space-16 to --space-24) │
│ │
│ ┌─ fieldset container ─────────────────┐ │
│ │ section title │ │
│ │ │ │
│ │ Content with generous padding │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
```
---
## Interactions
### Hover States
- **Buttons**: Background color change (accent → darker)
- **Links**: Opacity reduction to 0.7
- **Cards**: Border color change (border → border-strong)
- **No transforms** - Avoid scale/translate on hover (too playful)
### Focus States
```css
*:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
```
### Transitions
```css
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
```
**Rule: Keep transitions subtle and fast. No bouncy/spring animations.**
---
## Special Elements
### Gradient Header Bar
```css
.header-bar {
background: var(--gradient-header);
color: white;
padding: var(--space-2) var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-sm);
text-align: center;
}
```
### Technical Diagrams
Use ASCII-style box diagrams with monospace font:
```
┌─────────────┐ ┌─────────────┐
│ client │─────▶│ API │
└─────────────┘ └─────────────┘
```
### Code Blocks
```css
.code-block {
background: var(--color-bg-secondary);
border: 1px dashed var(--color-border);
padding: var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-sm);
overflow-x: auto;
}
```
### Sliders/Range Inputs
Custom styled with accent color, monospace tooltips showing values.
---
## Do's and Don'ts
### Do
- Use lowercase for headings
- Use dashed borders for containers
- Use generous whitespace
- Use monospace for technical content
- Use underlines for links
- Keep interactions subtle and fast
- Use the copper accent sparingly but confidently
### Don't
- Don't use rounded corners (except for special cases)
- Don't use drop shadows
- Don't use gradients (except header bar)
- Don't use icons where text works
- Don't use colored links
- Don't use bouncy animations
- Don't use multiple accent colors
---
## Implementation Priority
### Phase 1: Foundation
1. Update CSS variables (colors, spacing, typography)
2. Install fonts (JetBrains Mono, Inter)
3. Update base styles (reset, typography)
### Phase 2: Core Components
1. Button variants
2. Input/Form elements
3. Card/Container styles
4. Badge variants
### Phase 3: Layout
1. Fieldset-style containers
2. Page layouts with generous spacing
3. Navigation updates
4. Gradient header bar
### Phase 4: Polish
1. Table styles
2. Code blocks
3. Interactive elements (sliders, toggles)
4. Transitions and hover states
---
## References
- **Turbopuffer**: https://turbopuffer.com - Primary design inspiration
- **JetBrains Mono**: https://www.jetbrains.com/lp/mono/
- **Inter**: https://rsms.me/inter/
---
*Last updated: January 2025*
*Version: 1.0*

View file

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <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 // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // 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 { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon'; import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input'; import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { signIn } from '~/lib/auth-client'; import { signIn } from '~/lib/auth-client';
@ -77,9 +78,7 @@ export default function SignInPage() {
)} )}
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-foreground mb-1"> <Label htmlFor="email">Email</Label>
Email
</label>
<Input <Input
id="email" id="email"
type="email" type="email"
@ -92,9 +91,7 @@ export default function SignInPage() {
<div> <div>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<label htmlFor="password" className="block text-sm font-medium text-foreground"> <Label htmlFor="password" className="mb-0">Password</Label>
Password
</label>
<Link <Link
href="/forgot-password" href="/forgot-password"
className="text-sm text-foreground-secondary hover:text-foreground hover:underline" className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
@ -112,14 +109,16 @@ export default function SignInPage() {
placeholder="Enter your password" placeholder="Enter your password"
className="pr-10" className="pr-10"
/> />
<button <Button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setShowPassword(!showPassword)} 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'} aria-label={showPassword ? 'Hide password' : 'Show password'}
> >
<Icon icon={showPassword ? 'eyeOff' : 'eye'} size="sm" /> <Icon icon={showPassword ? 'eyeOff' : 'eye'} size="sm" />
</button> </Button>
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,8 +88,8 @@ export default function AuthenticationPage(): React.ReactElement {
<li>Copy your key and store it securely</li> <li>Copy your key and store it securely</li>
</ol> </ol>
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4"> <div className="bg-warning/10 border border-warning/20 rounded-lg p-4">
<p className="text-amber-600 dark:text-amber-400 font-medium">Important</p> <p className="text-warning font-medium">Important</p>
<p className="text-foreground-secondary text-sm mt-1"> <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 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. 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; children?: React.ReactNode;
}) { }) {
const methodColors = { const methodColors = {
GET: 'bg-green-500/10 text-green-500 border-green-500/30', GET: 'bg-success/10 text-success border-success/30',
POST: 'bg-blue-500/10 text-blue-500 border-blue-500/30', POST: 'bg-info/10 text-info border-info/30',
PUT: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30', PUT: 'bg-warning/10 text-warning border-warning/30',
DELETE: 'bg-red-500/10 text-red-500 border-red-500/30', DELETE: 'bg-error/10 text-error border-error/30',
}; };
return ( return (
@ -377,7 +377,7 @@ export default function APIDocsPage(): React.ReactElement {
/> />
</div> </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> <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"> <ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
<li> <li>
@ -398,7 +398,7 @@ export default function APIDocsPage(): React.ReactElement {
</ul> </ul>
</div> </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> <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"> <ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
<li>FREE tier: 100 requests/hour</li> <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>Optionally add an API key if your executor requires authentication</li>
<li>Click &quot;Verify Connection&quot; to test the configuration</li> <li>Click &quot;Verify Connection&quot; to test the configuration</li>
</ol> </ol>
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg"> <div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
<p className="text-sm text-amber-600 dark:text-amber-400"> <p className="text-sm text-warning">
<strong>Security tip:</strong> Set the{' '} <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. variable in your Vercel project to require authentication for all requests.
</p> </p>
</div> </div>
@ -214,7 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
{/* GET /health */} {/* GET /health */}
<div className="mb-8"> <div className="mb-8">
<h3 className="text-lg font-medium text-foreground mb-2"> <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 /health
</h3> </h3>
<p className="text-foreground-secondary mb-4"> <p className="text-foreground-secondary mb-4">

View file

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

View file

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

View file

@ -176,15 +176,15 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
<CodeBlock language="bash" code={envVarsExample} /> <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"> <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> <div>
<p className="font-medium text-amber-600 dark:text-amber-400"> <p className="font-medium text-warning">
Security Recommendation Security Recommendation
</p> </p>
<p className="text-sm text-foreground-secondary mt-1"> <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 require authentication. Without it, anyone with your executor URL can execute
tools. tools.
</p> </p>
@ -221,12 +221,12 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
</div> </div>
</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"> <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"> <p className="text-sm text-foreground-secondary">
If you see{' '} 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; &quot;status&quot;: &quot;ok&quot;
</code> </code>
, your executor is running and ready to use! , 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> <h4 className="text-lg font-semibold text-foreground mb-3">Without MCP</h4>
<ul className="space-y-3 text-foreground-secondary"> <ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2"> <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> <span>AI can only process text</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>No access to external data</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>Can&apos;t take real actions</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>Limited to knowledge cutoff</span>
</li> </li>
</ul> </ul>
@ -86,19 +86,19 @@ const slides: Slide[] = [
<h4 className="text-lg font-semibold text-foreground mb-3">With MCP + TPMJS</h4> <h4 className="text-lg font-semibold text-foreground mb-3">With MCP + TPMJS</h4>
<ul className="space-y-3 text-foreground-secondary"> <ul className="space-y-3 text-foreground-secondary">
<li className="flex items-start gap-2"> <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> <span>Execute code in 40+ languages</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>Fetch data from any URL</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>Search the web in real-time</span>
</li> </li>
<li className="flex items-start gap-2"> <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> <span>Use specialized tools</span>
</li> </li>
</ul> </ul>
@ -362,16 +362,16 @@ const slides: Slide[] = [
</div> </div>
<div className="flex justify-center gap-3 text-sm text-foreground-secondary"> <div className="flex justify-center gap-3 text-sm text-foreground-secondary">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="text-green-500"></span> Code execution <span className="text-success"></span> Code execution
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="text-green-500"></span> Web fetching <span className="text-success"></span> Web fetching
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="text-green-500"></span> Web search <span className="text-success"></span> Web search
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="text-green-500"></span> And more! <span className="text-success"></span> And more!
</span> </span>
</div> </div>
</div> </div>

View file

@ -154,7 +154,7 @@ export default function PublishPage(): React.ReactElement {
</div> </div>
{/* Auto-discovery callout */} {/* 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="flex items-start gap-4">
<div className="text-3xl">🔍</div> <div className="text-3xl">🔍</div>
<div> <div>

View file

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

View file

@ -1,3 +1,4 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Container } from '@tpmjs/ui/Container/Container'; import { Container } from '@tpmjs/ui/Container/Container';
import Link from 'next/link'; import Link from 'next/link';
import { AppHeader } from '~/components/AppHeader'; import { AppHeader } from '~/components/AppHeader';
@ -450,20 +451,10 @@ export default function TermsPage(): React.ReactElement {
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="mailto:hello@tpmjs.com"> <a href="mailto:hello@tpmjs.com">
<button <Button size="lg">Contact Us</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>
</a> </a>
<Link href="/"> <Link href="/">
<button <Button variant="outline" size="lg">Back to Home</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>
</Link> </Link>
</div> </div>
</div> </div>

View file

@ -2,6 +2,8 @@
import { Button } from '@tpmjs/ui/Button/Button'; import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon'; 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 { useCallback, useMemo, useRef, useState } from 'react';
import { parseEnvString } from '~/lib/utils/env-parser'; import { parseEnvString } from '~/lib/utils/env-parser';
@ -148,9 +150,6 @@ export function EnvVarsEditor({
return parseEnvString(pasteContent); return parseEnvString(pasteContent);
}, [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 ( return (
<div className={className}> <div className={className}>
{/* Header */} {/* Header */}
@ -182,11 +181,12 @@ export function EnvVarsEditor({
<Icon icon="x" size="xs" /> <Icon icon="x" size="xs" />
</Button> </Button>
</div> </div>
<textarea <Textarea
value={pasteContent} value={pasteContent}
onChange={(e) => setPasteContent(e.target.value)} onChange={(e) => setPasteContent(e.target.value)}
placeholder={`# Paste your .env content here\nAPI_KEY=your-api-key\nDATABASE_URL="postgres://..."\n`} 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} disabled={disabled}
/> />
{parsedPreview.length > 0 && ( {parsedPreview.length > 0 && (
@ -217,20 +217,20 @@ export function EnvVarsEditor({
<div className="space-y-2 mb-3"> <div className="space-y-2 mb-3">
{envVars.map((env, index) => ( {envVars.map((env, index) => (
<div key={`env-${env.key || index}`} className="flex items-center gap-2"> <div key={`env-${env.key || index}`} className="flex items-center gap-2">
<input <Input
type="text" type="text"
value={env.key} value={env.key}
onChange={(e) => updateEnvVar(index, 'key', e.target.value)} onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
placeholder="KEY" placeholder="KEY"
className={inputClassName} size="sm"
disabled={disabled} disabled={disabled}
/> />
<input <Input
type="password" type="password"
value={env.value} value={env.value}
onChange={(e) => updateEnvVar(index, 'value', e.target.value)} onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
placeholder="value" placeholder="value"
className={inputClassName} size="sm"
disabled={disabled} disabled={disabled}
/> />
<Button <Button
@ -249,12 +249,12 @@ export function EnvVarsEditor({
{/* Add new env var */} {/* Add new env var */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <Input
type="text" type="text"
value={newEnvKey} value={newEnvKey}
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())} onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
placeholder={keyPlaceholder} placeholder={keyPlaceholder}
className={inputClassName} size="sm"
disabled={disabled} disabled={disabled}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && newEnvKey.trim()) { if (e.key === 'Enter' && newEnvKey.trim()) {
@ -262,12 +262,12 @@ export function EnvVarsEditor({
} }
}} }}
/> />
<input <Input
type="text" type="text"
value={newEnvValue} value={newEnvValue}
onChange={(e) => setNewEnvValue(e.target.value)} onChange={(e) => setNewEnvValue(e.target.value)}
placeholder={valuePlaceholder} placeholder={valuePlaceholder}
className={inputClassName} size="sm"
disabled={disabled} disabled={disabled}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && newEnvKey.trim()) { if (e.key === 'Enter' && newEnvKey.trim()) {

View file

@ -1,6 +1,6 @@
{ {
"name": "@tpmjs/tools-unsandbox", "name": "@tpmjs/tools-unsandbox",
"version": "0.1.0", "version": "0.1.1",
"description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.", "description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.",
"type": "module", "type": "module",
"keywords": [ "keywords": [
@ -41,66 +41,38 @@
"homepage": "https://tpmjs.com", "homepage": "https://tpmjs.com",
"license": "MIT", "license": "MIT",
"tpmjs": { "tpmjs": {
"category": "code-execution", "category": "sandbox",
"frameworks": [ "frameworks": [
"vercel-ai" "vercel-ai"
], ],
"tools": [ "tools": [
{ {
"name": "executeCodeAsync", "name": "executeCodeAsync",
"description": "Execute code asynchronously in a secure sandbox. Returns a job_id immediately. Use getJob to check status and retrieve results.", "description": "Execute code asynchronously in a secure sandbox. Returns a job_id immediately. Use getJob to check status and retrieve results. Supports 42+ languages."
"parameters": [
{
"name": "language",
"type": "string",
"description": "Programming language to execute",
"required": true
},
{
"name": "code",
"type": "string",
"description": "The source code to execute",
"required": true
},
{
"name": "input_files",
"type": "array",
"description": "Optional array of input files",
"required": false
},
{
"name": "network_mode",
"type": "string",
"description": "Network isolation mode: zerotrust or semitrusted",
"required": false
},
{
"name": "ttl",
"type": "number",
"description": "Execution timeout in seconds (1-900)",
"required": false
}
],
"returns": {
"type": "object",
"description": "Job ID and initial status"
}
}, },
{ {
"name": "getJob", "name": "getJob",
"description": "Get the status and results of an async code execution job", "description": "Get the status and results of an async code execution job by job_id."
"parameters": [ },
{ {
"name": "job_id", "name": "execute",
"type": "string", "description": "Execute code synchronously in a secure sandbox. Waits for completion and returns results directly. Best for quick scripts."
"description": "The job ID returned from executeCodeAsync", },
"required": true {
} "name": "run",
], "description": "Execute code synchronously with automatic language detection via shebang (e.g., #!/usr/bin/env python)."
"returns": { },
"type": "object", {
"description": "Job status and results" "name": "runAsync",
} "description": "Execute code asynchronously with automatic language detection via shebang. Returns job_id for polling."
},
{
"name": "listJobs",
"description": "List all active code execution jobs with their current status."
},
{
"name": "deleteJob",
"description": "Cancel an active code execution job by job_id."
} }
] ]
}, },