feat: add use cases marketing product with AI-generated content

Transform qualifying scenarios into marketing-ready use cases with:
- AI-generated titles, descriptions, ROI estimates, business value
- Persona/industry/category taxonomy for targeting
- Browseable feed with filtering and ranking
- SEO-optimized case study pages
- Daily cron job for generation and ranking

Database:
- Add Persona, Industry, Category lookup tables
- Add UseCase model with marketing content fields
- Add junction tables for personas/industries/categories
- Add SocialProof model for cached metrics

API:
- GET /api/use-cases - Global directory with filtering
- GET /api/use-cases/[id] - Individual use case details
- GET /api/public/users/[username]/collections/[slug]/use-cases
- POST /api/cron/use-cases - Nightly generation job

Frontend:
- /use-cases - Global feed with persona dropdown
- /use-cases/[slug] - SEO case study page
- /[username]/collections/[slug]/use-cases - Collection feed
- UseCasesFeed component - Sortable table component
- UseCaseCaseStudy component - Full case study layout
This commit is contained in:
Ajax Davis 2026-01-20 16:17:35 +10:00
parent 5fafc90e3e
commit 48c41e8733
245 changed files with 4734 additions and 2075 deletions

View file

@ -1,8 +1,8 @@
import { Args, Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
import { getApiKey, getApiUrl } from '../../lib/config.js';
import { createOutput } from '../../lib/output.js';
export default class AgentChat extends Command {
static description = 'Chat with an agent';
@ -202,11 +202,11 @@ export default class AgentChat extends Command {
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({})) as { message?: string };
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
throw new Error(errorData.message || `HTTP ${response.status}`);
}
const data = await response.json() as {
const data = (await response.json()) as {
content: string;
conversationId: string;
toolCalls?: { name: string; result: unknown }[];

View file

@ -1,7 +1,7 @@
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
import * as readline from 'node:readline';
export default class AgentDelete extends Command {
static description = 'Delete an agent';

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import { hasCredentials, getApiKey, getApiUrl } from '../../lib/config.js';
import { TpmClient } from '../../lib/api-client.js';
import { getApiKey, getApiUrl, hasCredentials } from '../../lib/config.js';
import { createOutput } from '../../lib/output.js';
export default class Status extends Command {
@ -80,7 +80,7 @@ export default class Status extends Command {
authSource,
apiUrl,
user: response.data,
keyPrefix: apiKey.substring(0, 12) + '...',
keyPrefix: `${apiKey.substring(0, 12)}...`,
});
} else {
output.newLine();
@ -92,8 +92,11 @@ export default class Status extends Command {
output.keyValue('Name', response.data.name);
}
output.keyValue('API URL', apiUrl);
output.keyValue('Auth Source', authSource === 'env' ? 'Environment variable' : 'Config file');
output.keyValue('Key Prefix', apiKey.substring(0, 12) + '...');
output.keyValue(
'Auth Source',
authSource === 'env' ? 'Environment variable' : 'Config file'
);
output.keyValue('Key Prefix', `${apiKey.substring(0, 12)}...`);
}
} catch (error) {
spinner.fail('Failed to verify authentication');

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -5,9 +5,7 @@ import { createOutput } from '../../lib/output.js';
export default class CollectionRemove extends Command {
static description = 'Remove a tool from a collection';
static examples = [
'<%= config.bin %> <%= command.id %> my-collection tool-id-1',
];
static examples = ['<%= config.bin %> <%= command.id %> my-collection tool-id-1'];
static flags = {
json: Flags.boolean({

View file

@ -1,8 +1,8 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { getApiKey, getApiUrl, getConfigDir, hasCredentials, getConfig } from '../lib/config.js';
import { Command, Flags } from '@oclif/core';
import { TpmClient } from '../lib/api-client.js';
import { getApiKey, getApiUrl, getConfig, getConfigDir, hasCredentials } from '../lib/config.js';
import { createOutput } from '../lib/output.js';
interface DiagnosticCheck {
@ -59,7 +59,11 @@ export default class Doctor extends Command {
// 3. Check authentication
const hasAuth = hasCredentials() || !!process.env.TPMJS_API_KEY;
const authSource = process.env.TPMJS_API_KEY ? 'environment' : hasCredentials() ? 'config file' : 'none';
const authSource = process.env.TPMJS_API_KEY
? 'environment'
: hasCredentials()
? 'config file'
: 'none';
checks.push({
name: 'Authentication',
status: hasAuth ? 'ok' : 'warning',

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import { Args, Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
type ClientType = 'claude' | 'cursor' | 'windsurf' | 'generic';
@ -75,9 +75,10 @@ export default class McpConfig extends Command {
}
// Merge mcpServers
const existingServers = typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
? existingConfig.mcpServers as Record<string, unknown>
: {};
const existingServers =
typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
? (existingConfig.mcpServers as Record<string, unknown>)
: {};
const mergedConfig = {
...existingConfig,
mcpServers: {

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as http from 'node:http';
import * as readline from 'node:readline';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -1,5 +1,5 @@
import { Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../lib/api-client.js';
import { createOutput } from '../lib/output.js';

View file

@ -1,6 +1,6 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
@ -67,9 +67,7 @@ export default class PublishCheck extends Command {
}
// Find exact match
const exactMatch = searchResponse.data?.find(
(tool) => tool.npmPackageName === packageName
);
const exactMatch = searchResponse.data?.find((tool) => tool.npmPackageName === packageName);
if (flags.json) {
output.json({
@ -88,7 +86,9 @@ export default class PublishCheck extends Command {
output.text(`Category: ${exactMatch.category}`);
output.text(`Tier: ${exactMatch.tier}`);
output.text(`Version: ${exactMatch.npmVersion}`);
output.text(`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`);
output.text(
`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`
);
output.text(`Quality Score: ${exactMatch.qualityScore?.toFixed(2) || 'N/A'}`);
output.text('');
output.text(`View on tpmjs.com:`);

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
export default class PublishPreview extends Command {

View file

@ -104,7 +104,7 @@ export default class ScenarioList extends Command {
if (scenarios.length === 0) {
output.info(`No scenarios found for collection "${collection.name}"`);
output.text(
'Generate some with: tpm scenario generate ' + (collection.slug || collection.id)
`Generate some with: tpm scenario generate ${collection.slug || collection.id}`
);
return;
}
@ -112,8 +112,8 @@ export default class ScenarioList extends Command {
output.text(output.bold(`Scenarios for ${collection.name}\n`));
output.table(
scenarios.map((s) => ({
name: s.name || s.prompt.slice(0, 30) + '...',
quality: (s.qualityScore * 100).toFixed(0) + '%',
name: s.name || `${s.prompt.slice(0, 30)}...`,
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
runs: s.totalRuns,
status: s.lastRunStatus || '-',
tags: s.tags.slice(0, 3).join(', ') || '-',
@ -147,9 +147,9 @@ export default class ScenarioList extends Command {
output.table(
response.data.map((s) => ({
name: s.name || s.prompt.slice(0, 30) + '...',
name: s.name || `${s.prompt.slice(0, 30)}...`,
collection: s.collection?.name || '-',
quality: (s.qualityScore * 100).toFixed(0) + '%',
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
runs: s.totalRuns,
status: s.lastRunStatus || '-',
})),

View file

@ -121,7 +121,7 @@ export default class ScenarioRun extends Command {
let errors = 0;
for (const scenario of scenarios) {
const name = scenario.name || scenario.prompt.slice(0, 40) + '...';
const name = scenario.name || `${scenario.prompt.slice(0, 40)}...`;
const runSpinner = output.spinner(`Running: ${name}`);
try {

View file

@ -56,7 +56,7 @@ export default class ScenarioTest extends Command {
}
).data;
scenarioName = scenario.name || scenario.prompt.slice(0, 50) + '...';
scenarioName = scenario.name || `${scenario.prompt.slice(0, 50)}...`;
infoSpinner.stop();
output.text(output.bold(`Scenario: ${scenarioName}`));
@ -131,7 +131,7 @@ export default class ScenarioTest extends Command {
} else {
const truncatedReason =
runData.evaluator.reason.length > 80
? runData.evaluator.reason.slice(0, 80) + '...'
? `${runData.evaluator.reason.slice(0, 80)}...`
: runData.evaluator.reason;
output.text(` Reason: ${truncatedReason}`);
}

View file

@ -69,7 +69,9 @@ export default class ToolExecute extends Command {
const content = fs.readFileSync(flags['input-file'], 'utf-8');
params = JSON.parse(content);
} catch (error) {
output.error(`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`);
output.error(
`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`
);
return;
}
}

View file

@ -61,7 +61,7 @@ export default class ToolInfo extends Command {
output.keyValue('Package', tool.package?.npmPackageName || tool.npmPackageName);
output.keyValue('Category', tool.package?.category || tool.category);
output.keyValue('Official', (tool.package?.isOfficial || tool.isOfficial) ? 'Yes' : 'No');
output.keyValue('Official', tool.package?.isOfficial || tool.isOfficial ? 'Yes' : 'No');
output.newLine();
output.subheading('Description');
@ -75,13 +75,20 @@ export default class ToolInfo extends Command {
output.subheading('Metrics');
output.keyValue('Quality Score', tool.qualityScore ? tool.qualityScore.toFixed(2) : 'N/A');
output.keyValue('Downloads/Month', formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth));
output.keyValue(
'Downloads/Month',
formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth)
);
output.keyValue('Likes', tool.likeCount.toString());
output.newLine();
output.subheading('Links');
output.text(`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`);
output.text(`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`);
output.text(
`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`
);
output.text(
`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`
);
} catch (error) {
spinner.fail('Failed to fetch tool info');
output.error(

View file

@ -1,7 +1,7 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
const CATEGORIES = [
@ -154,7 +154,8 @@ export default class ToolInit extends Command {
}
}
const author = (await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
const author =
(await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
return { name: toolName, description, category: category ?? 'utilities', author };
}
@ -219,7 +220,7 @@ export default class ToolInit extends Command {
],
...(template === 'rich'
? {
documentation: 'https://github.com/yourname/' + config.name + '#readme',
documentation: `https://github.com/yourname/${config.name}#readme`,
examples: [
{
title: 'Basic usage',
@ -231,10 +232,7 @@ export default class ToolInit extends Command {
},
};
fs.writeFileSync(
path.join(targetDir, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2));
// tsconfig.json
const tsconfig = {
@ -254,10 +252,7 @@ export default class ToolInit extends Command {
exclude: ['node_modules', 'dist'],
};
fs.writeFileSync(
path.join(targetDir, 'tsconfig.json'),
JSON.stringify(tsconfig, null, 2)
);
fs.writeFileSync(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
// tsup.config.ts
const tsupConfig = `import { defineConfig } from 'tsup';

View file

@ -102,11 +102,7 @@ export default class ToolSearch extends Command {
);
if (response.pagination.hasMore) {
output.text(
output.dim(
`Use --offset ${flags.offset + flags.limit} to see more results`
)
);
output.text(output.dim(`Use --offset ${flags.offset + flags.limit} to see more results`));
}
} catch (error) {
spinner.fail('Search failed');

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
@ -47,7 +47,10 @@ export default class ToolValidate extends Command {
const content = fs.readFileSync(packagePath, 'utf-8');
packageJson = JSON.parse(content);
} catch (error) {
output.error('Failed to parse package.json', error instanceof Error ? error.message : undefined);
output.error(
'Failed to parse package.json',
error instanceof Error ? error.message : undefined
);
return;
}
@ -104,7 +107,10 @@ export default class ToolValidate extends Command {
output.success('Configuration is valid');
output.newLine();
output.keyValue('Tier', response.data.tier || 'minimal');
output.keyValue('Has tpmjs keyword', hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)');
output.keyValue(
'Has tpmjs keyword',
hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)'
);
if (!hasTpmjsKeyword) {
output.newLine();

View file

@ -1,5 +1,5 @@
import { Command, Flags } from '@oclif/core';
import { execSync } from 'node:child_process';
import { Command, Flags } from '@oclif/core';
import { createOutput } from '../lib/output.js';
export default class Update extends Command {

View file

@ -1,6 +1,6 @@
import type { Hook } from '@oclif/core';
const hook: Hook<'init'> = async function () {
const hook: Hook<'init'> = async () => {
// Initialization hook - runs before any command
// Can be used for:
// - Checking for updates

View file

@ -1,36 +1,35 @@
// Library exports for programmatic use
export { TpmClient, getClient, ApiError } from './lib/api-client.js';
export type {
TpmClientOptions,
Agent,
ApiKey,
ApiResponse,
PaginationOptions,
Collection,
CreateAgentInput,
CreateCollectionInput,
PaginatedResponse,
PaginationOptions,
Stats,
Tool,
ToolSearchOptions,
Agent,
CreateAgentInput,
TpmClientOptions,
UpdateAgentInput,
Collection,
CreateCollectionInput,
UpdateCollectionInput,
User,
ApiKey,
Stats,
} from './lib/api-client.js';
export { ApiError, getClient, TpmClient } from './lib/api-client.js';
export type { TpmConfig, TpmCredentials } from './lib/config.js';
export {
getConfig,
setConfig,
getConfigValue,
setConfigValue,
loadCredentials,
saveCredentials,
deleteCredentials,
hasCredentials,
getApiKey,
getApiUrl,
getConfig,
getConfigValue,
hasCredentials,
loadCredentials,
saveCredentials,
setConfig,
setConfigValue,
} from './lib/config.js';
export type { TpmConfig, TpmCredentials } from './lib/config.js';
export { OutputFormatter, createOutput } from './lib/output.js';
export type { OutputOptions } from './lib/output.js';
export { createOutput, OutputFormatter } from './lib/output.js';

View file

@ -250,7 +250,7 @@ export class TpmClient {
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
headers.Authorization = `Bearer ${this.apiKey}`;
}
const controller = new AbortController();
@ -357,7 +357,7 @@ export class TpmClient {
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
headers.Authorization = `Bearer ${this.apiKey}`;
}
const response = await fetch(url, {

View file

@ -1,7 +1,7 @@
import Conf from 'conf';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import Conf from 'conf';
export interface TpmConfig {
apiUrl?: string;
@ -57,10 +57,7 @@ export function getConfigValue<K extends keyof TpmConfig>(key: K): TpmConfig[K]
return configStore.get(key);
}
export function setConfigValue<K extends keyof TpmConfig>(
key: K,
value: TpmConfig[K]
): void {
export function setConfigValue<K extends keyof TpmConfig>(key: K, value: TpmConfig[K]): void {
configStore.set(key, value);
}

View file

@ -107,13 +107,13 @@ export class OutputFormatter {
// Key-value pair
keyValue(key: string, value: string | number | boolean | undefined): void {
if (this.options.json) return;
console.log(`${pc.dim(key + ':')} ${value ?? pc.dim('(not set)')}`);
console.log(`${pc.dim(`${key}:`)} ${value ?? pc.dim('(not set)')}`);
}
// List item
listItem(text: string, indent = 0): void {
if (this.options.json) return;
const prefix = ' '.repeat(indent) + '•';
const prefix = `${' '.repeat(indent)}•`;
console.log(`${prefix} ${text}`);
}
@ -148,7 +148,7 @@ export class OutputFormatter {
this.json({ code: text, language });
return;
}
console.log(pc.dim('```' + (language ?? '')));
console.log(pc.dim(`\`\`\`${language ?? ''}`));
console.log(text);
console.log(pc.dim('```'));
}

View file

@ -1,11 +1,7 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/commands/**/*.ts',
'src/hooks/**/*.ts',
],
entry: ['src/index.ts', 'src/commands/**/*.ts', 'src/hooks/**/*.ts'],
format: ['esm'],
dts: true,
clean: true,

View file

@ -1,4 +1,5 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {

View file

@ -1158,6 +1158,7 @@ model Scenario {
// Relations
runs ScenarioRun[]
embedding ScenarioEmbedding?
useCase UseCase?
@@index([collectionId])
@@index([qualityScore])
@ -1252,3 +1253,164 @@ model ScenarioQuota {
@@index([userId])
@@map("scenario_quotas")
}
// ============================================================================
// Use Cases Models (Marketing Content from Scenarios)
// ============================================================================
/// Persona - user personas for use case targeting (AI-populated)
model Persona {
id String @id @default(cuid())
name String @unique @db.VarChar(100)
slug String @unique @db.VarChar(100)
description String? @db.Text
icon String? @db.VarChar(50) // emoji or icon name
createdAt DateTime @default(now()) @map("created_at")
// Relations
useCases UseCasePersona[]
@@index([slug])
@@map("personas")
}
/// Industry - industries for use case categorization (AI-populated)
model Industry {
id String @id @default(cuid())
name String @unique @db.VarChar(100)
slug String @unique @db.VarChar(100)
description String? @db.Text
icon String? @db.VarChar(50)
parentId String? @map("parent_id")
parent Industry? @relation("IndustryHierarchy", fields: [parentId], references: [id])
children Industry[] @relation("IndustryHierarchy")
createdAt DateTime @default(now()) @map("created_at")
// Relations
useCases UseCaseIndustry[]
@@index([parentId])
@@index([slug])
@@map("industries")
}
/// Category - categories for use case organization (AI-populated)
model Category {
id String @id @default(cuid())
name String @unique @db.VarChar(100)
slug String @unique @db.VarChar(100)
type String @map("type") @db.VarChar(50) // 'functional', 'business-process', 'technical'
description String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
// Relations
useCases UseCaseCategory[]
@@index([type])
@@index([slug])
@@map("categories")
}
/// UseCase - auto-generated marketing content from qualifying scenarios
model UseCase {
id String @id @default(cuid())
scenarioId String @unique @map("scenario_id")
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
// SEO-friendly slug
slug String @db.VarChar(200)
// AI-generated marketing content
marketingTitle String @map("marketing_title") @db.VarChar(200)
marketingDesc String @map("marketing_desc") @db.Text
roiEstimate String? @map("roi_estimate") @db.Text // "Saves ~10 hours/week"
businessValue String? @map("business_value") @db.Text // Human-readable benefit
problemStatement String? @map("problem_statement") @db.Text // What problem it solves
solutionNarrative String? @map("solution_narrative") @db.Text // How it works
// Ranking (computed nightly)
rankScore Float @default(0) @map("rank_score")
lastRankedAt DateTime @map("last_ranked_at")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastRegeneratedAt DateTime? @map("last_regenerated_at")
// Relations
personas UseCasePersona[]
industries UseCaseIndustry[]
categories UseCaseCategory[]
socialProof SocialProof?
@@index([scenarioId])
@@index([slug])
@@index([rankScore])
@@index([lastRankedAt])
@@index([lastRegeneratedAt])
@@map("use_cases")
}
/// UseCasePersona - junction table for UseCase <-> Persona
model UseCasePersona {
useCaseId String @map("use_case_id")
personaId String @map("persona_id")
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
persona Persona @relation(fields: [personaId], references: [id], onDelete: Cascade)
relevance Float? @map("relevance") // Future: relevance score for this persona
@@id([useCaseId, personaId])
@@index([useCaseId])
@@index([personaId])
@@map("use_case_personas")
}
/// UseCaseIndustry - junction table for UseCase <-> Industry
model UseCaseIndustry {
useCaseId String @map("use_case_id")
industryId String @map("industry_id")
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
industry Industry @relation(fields: [industryId], references: [id], onDelete: Cascade)
@@id([useCaseId, industryId])
@@index([useCaseId])
@@index([industryId])
@@map("use_case_industries")
}
/// UseCaseCategory - junction table for UseCase <-> Category
model UseCaseCategory {
useCaseId String @map("use_case_id")
categoryId String @map("category_id")
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
@@id([useCaseId, categoryId])
@@index([useCaseId])
@@index([categoryId])
@@map("use_case_categories")
}
/// SocialProof - cached metrics from scenario runs for use cases
model SocialProof {
id String @id @default(cuid())
useCaseId String @unique @map("use_case_id")
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
qualityScore Float @map("quality_score")
totalRuns Int @map("total_runs")
consecutivePasses Int @map("consecutive_passes")
lastRunStatus String? @map("last_run_status") @db.VarChar(20)
lastRunAt DateTime? @map("last_run_at")
// Computed display fields
successRate Float? @map("success_rate")
lastRunAgo String? @map("last_run_ago") // "2 days ago"
updatedAt DateTime @updatedAt @map("updated_at")
@@index([useCaseId])
@@index([qualityScore])
@@index([totalRuns])
@@map("social_proofs")
}

View file

@ -1,2 +1,2 @@
export { prisma } from './client';
export * from '@prisma/client';
export { prisma } from './client';

View file

@ -1,4 +1,4 @@
import { http, HttpResponse } from 'msw';
import { HttpResponse, http } from 'msw';
export const handlers = [
http.get('/api/tools', () => {

View file

@ -5,44 +5,40 @@
// Changes feed
export {
fetchChanges,
type Change,
type ChangesFeedResponse,
type FetchChangesOptions,
type FetchChangesResult,
fetchChanges,
} from './changes';
// Keyword search
export {
searchByKeyword,
searchAllByKeyword,
type SearchPackage,
type SearchResult,
type SearchResponse,
type SearchByKeywordOptions,
} from './search';
// Package metadata
export {
fetchPackageMetadata,
fetchLatestPackageVersion,
fetchLatestPackageWithMetadata,
fetchPackageTpmjsField,
type PackageVersion,
type PackageMetadata,
type PackageVersionWithReadme,
} from './package';
// Download statistics
export { fetchDownloadStats, fetchBulkDownloadStats, type DownloadsResponse } from './stats';
// GitHub statistics
export {
fetchGitHubStars,
fetchGitHubStarsFromRepository,
parseGitHubUrl,
type GitHubRepoResponse,
parseGitHubUrl,
} from './github';
// Package metadata
export {
fetchLatestPackageVersion,
fetchLatestPackageWithMetadata,
fetchPackageMetadata,
fetchPackageTpmjsField,
type PackageMetadata,
type PackageVersion,
type PackageVersionWithReadme,
} from './package';
// Rate limiting
export { delay, RateLimiter, npmRateLimiter, retryWithBackoff } from './rate-limiter';
export { delay, npmRateLimiter, RateLimiter, retryWithBackoff } from './rate-limiter';
// Keyword search
export {
type SearchByKeywordOptions,
type SearchPackage,
type SearchResponse,
type SearchResult,
searchAllByKeyword,
searchByKeyword,
} from './search';
// Download statistics
export { type DownloadsResponse, fetchBulkDownloadStats, fetchDownloadStats } from './stats';

View file

@ -3,5 +3,5 @@
* @packageDocumentation
*/
export * from './types';
export * from './executor';
export * from './types';

View file

@ -27,8 +27,8 @@ export const enrichCommand = new Command('enrich')
const processor = new BatchProcessor({
dbPath: options.db,
batchSize: Number.parseInt(options.batch),
concurrency: Number.parseInt(options.concurrency),
batchSize: Number.parseInt(options.batch, 10),
concurrency: Number.parseInt(options.concurrency, 10),
costLimitUsd: Number.parseFloat(options.costLimit),
model: options.model,
onProgress: (processed, total, cost) => {

View file

@ -37,7 +37,7 @@ export const exportCommand = new Command('export')
try {
const db = getDatabase(options.db);
const minQuality = Number.parseFloat(options.minQuality);
const limit = Number.parseInt(options.limit);
const limit = Number.parseInt(options.limit, 10);
// Build query conditions
const conditions = [gte(toolIdeas.qualityScore, minQuality)];

View file

@ -14,9 +14,9 @@ export const generateCommand = new Command('generate')
const spinner = ora('Generating tool skeletons...').start();
try {
const count = Number.parseInt(options.count);
const count = Number.parseInt(options.count, 10);
const threshold = Number.parseFloat(options.threshold);
const seed = Number.parseInt(options.seed);
const seed = Number.parseInt(options.seed, 10);
spinner.text = `Generating up to ${count} skeletons (threshold: ${threshold})...`;

View file

@ -26,11 +26,11 @@ vocabCommand
getDatabase(options.db);
const counts = {
categories: Number.parseInt(options.categories),
verbs: Number.parseInt(options.verbs),
objects: Number.parseInt(options.objects),
contexts: Number.parseInt(options.contexts),
qualifiers: Number.parseInt(options.qualifiers),
categories: Number.parseInt(options.categories, 10),
verbs: Number.parseInt(options.verbs, 10),
objects: Number.parseInt(options.objects, 10),
contexts: Number.parseInt(options.contexts, 10),
qualifiers: Number.parseInt(options.qualifiers, 10),
};
spinner.text = `Generating ${counts.categories} categories...`;

View file

@ -4,17 +4,17 @@ import { eq, inArray, sql } from 'drizzle-orm';
import pLimit from 'p-limit';
import { getDatabase } from '../db/client.js';
import {
type NewToolIdea,
type ToolSkeleton,
categories,
contexts,
type NewToolIdea,
objects,
processingErrors,
type ToolSkeleton,
toolIdeas,
toolSkeletons,
verbs,
} from '../db/schema.js';
import { type SkeletonWithRelations, createEnrichmentPrompt } from './prompts.js';
import { createEnrichmentPrompt, type SkeletonWithRelations } from './prompts.js';
import { EnrichedToolSchema } from './schemas.js';
// =============================================================================

View file

@ -4,14 +4,15 @@ import { z } from 'zod';
import { getDatabase } from '../db/client.js';
import {
type Category,
type ToolObject,
type Verb,
categories,
categoryVerbAffinity,
objects,
type ToolObject,
type Verb,
verbObjectCompatibility,
verbs,
} from '../db/schema.js';
// Drizzle operators imported as needed
// =============================================================================
@ -216,7 +217,7 @@ export async function seedCompatibilityRules(options: { dbPath?: string } = {})
.onConflictDoNothing()
.run();
voCount++;
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -241,7 +242,7 @@ export async function seedCompatibilityRules(options: { dbPath?: string } = {})
.onConflictDoNothing()
.run();
cvCount++;
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -300,7 +301,7 @@ export function loadCompatibilityRules(db: ReturnType<typeof getDatabase>) {
cvRules.set(cat.name, { verbs: new Set(), score: rule.score });
}
cvRules.get(cat.name)!.verbs.add(verb.name);
cvRules.get(cat.name)?.verbs.add(verb.name);
}
return { voRules, cvRules };

View file

@ -4,13 +4,13 @@ import { getDatabase } from '../db/client.js';
import {
type Category,
type Context,
type NewToolSkeleton,
type ToolObject,
type Verb,
categories,
contexts,
type NewToolSkeleton,
objects,
type ToolObject,
toolSkeletons,
type Verb,
verbs,
} from '../db/schema.js';
import { calculateCompatibilityScore, loadCompatibilityRules } from './compatibility.js';
@ -212,7 +212,7 @@ export async function generateSkeletons(options: {
try {
db.insert(toolSkeletons).values(values).onConflictDoNothing().run();
inserted += batch.length;
} catch (e) {
} catch (_e) {
// Some might be duplicates
for (const v of values) {
try {

View file

@ -315,7 +315,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
})
.onConflictDoNothing()
.run();
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -334,7 +334,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
})
.onConflictDoNothing()
.run();
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -352,7 +352,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
})
.onConflictDoNothing()
.run();
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -369,7 +369,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
})
.onConflictDoNothing()
.run();
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}
@ -386,7 +386,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
})
.onConflictDoNothing()
.run();
} catch (e) {
} catch (_e) {
// Ignore duplicates
}
}

View file

@ -1,20 +1,19 @@
// Database
export { getDatabase } from './db/client.js';
export * from './db/schema.js';
// Generators
export { seedVocabulary, getVocabularyStats } from './generators/vocabulary.js';
export {
generateVerbObjectRules,
generateCategoryVerbRules,
calculateCompatibilityScore,
seedCompatibilityRules,
loadCompatibilityRules,
} from './generators/compatibility.js';
export { generateSkeletons, getSkeletonStats } from './generators/skeleton-generator.js';
export type { BatchProcessorOptions } from './enrichment/batch-processor.js';
export { BatchProcessor, getEnrichmentStats } from './enrichment/batch-processor.js';
export { createBatchEnrichmentPrompt, createEnrichmentPrompt } from './enrichment/prompts.js';
// Enrichment
export * from './enrichment/schemas.js';
export { createEnrichmentPrompt, createBatchEnrichmentPrompt } from './enrichment/prompts.js';
export { BatchProcessor, getEnrichmentStats } from './enrichment/batch-processor.js';
export type { BatchProcessorOptions } from './enrichment/batch-processor.js';
export {
calculateCompatibilityScore,
generateCategoryVerbRules,
generateVerbObjectRules,
loadCompatibilityRules,
seedCompatibilityRules,
} from './generators/compatibility.js';
export { generateSkeletons, getSkeletonStats } from './generators/skeleton-generator.js';
// Generators
export { getVocabularyStats, seedVocabulary } from './generators/vocabulary.js';

View file

@ -1,5 +1,5 @@
import { TPMJS_CATEGORIES, TpmjsToolDefinitionSchema } from '@tpmjs/types/tpmjs';
import type { TpmjsCategory } from '@tpmjs/types/tpmjs';
import { TPMJS_CATEGORIES, TpmjsToolDefinitionSchema } from '@tpmjs/types/tpmjs';
import type { ToolDefinition } from '../types.js';
/**

View file

@ -3,5 +3,5 @@
* AI SDK tools for emoji conversion and mood detection
*/
export { textToEmoji } from './tools/textToEmoji.js';
export { emojiMood } from './tools/emojiMood.js';
export { textToEmoji } from './tools/textToEmoji.js';

View file

@ -3,5 +3,5 @@
* Tools for formatting and manipulating markdown text
*/
export { markdownToPlainText } from './tools/markdownToPlainText.js';
export { formatMarkdownTable } from './tools/formatMarkdownTable.js';
export { markdownToPlainText } from './tools/markdownToPlainText.js';

View file

@ -52,13 +52,13 @@ export const markdownToPlainText = tool({
plainText = plainText.replace(/```[\s\S]*?```/g, '');
// Remove horizontal rules (---, ***)
plainText = plainText.replace(/^[\*\-_]{3,}$/gm, '');
plainText = plainText.replace(/^[*\-_]{3,}$/gm, '');
// Remove blockquotes (>)
plainText = plainText.replace(/^>\s*/gm, '');
// Remove list markers (-, *, +, 1.)
plainText = plainText.replace(/^[\s]*[-\*\+]\s+/gm, '');
plainText = plainText.replace(/^[\s]*[-*+]\s+/gm, '');
plainText = plainText.replace(/^[\s]*\d+\.\s+/gm, '');
// Handle line breaks

View file

@ -61,7 +61,7 @@ function extractDemographics(data: Record<string, unknown>): AudiencePersona['de
// Extract age range
if (data.age) {
const age = Number(data.age);
if (!isNaN(age)) {
if (!Number.isNaN(age)) {
if (age < 18) demographics.ageRange = 'Under 18';
else if (age < 25) demographics.ageRange = '18-24';
else if (age < 35) demographics.ageRange = '25-34';
@ -237,7 +237,7 @@ function generateMarketingImplications(
}
// Add age-specific recommendations
const ageNum = Number.parseInt(demographics.ageRange.split('-')[0] || '0');
const ageNum = Number.parseInt(demographics.ageRange.split('-')[0] || '0', 10);
if (ageNum < 35) {
contentRecommendations.push('Use short-form video content (TikTok, Reels)');
} else if (ageNum >= 35 && ageNum < 55) {

View file

@ -56,11 +56,11 @@ function calculateDateRange(duration: string): { startDate: Date; endDate: Date;
if (durationLower.includes('week')) {
const weekMatch = durationLower.match(/(\d+)\s*week/);
weeks = weekMatch?.[1] ? Number.parseInt(weekMatch[1]) : 1;
weeks = weekMatch?.[1] ? Number.parseInt(weekMatch[1], 10) : 1;
endDate.setDate(endDate.getDate() + weeks * 7);
} else if (durationLower.includes('month')) {
const monthMatch = durationLower.match(/(\d+)\s*month/);
const months = monthMatch?.[1] ? Number.parseInt(monthMatch[1]) : 1;
const months = monthMatch?.[1] ? Number.parseInt(monthMatch[1], 10) : 1;
endDate.setMonth(endDate.getMonth() + months);
weeks = Math.ceil((endDate.getTime() - startDate.getTime()) / (7 * 24 * 60 * 60 * 1000));
} else if (durationLower.includes('quarter')) {

View file

@ -143,7 +143,7 @@ const PATTERNS = {
// Authentication Credentials
apiKey: {
regex: /\b(?:api[_-]?key|apikey|access[_-]?token)[\s:=]*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi,
regex: /\b(?:api[_-]?key|apikey|access[_-]?token)[\s:=]*['"]?[A-Za-z0-9_-]{20,}['"]?/gi,
severity: 'critical' as const,
type: 'API Key',
description: 'API key or access token detected',

View file

@ -1,5 +1,6 @@
// tsup.config.ts
import { defineConfig } from 'tsup';
var tsup_config_default = defineConfig({
entry: ['src/index.ts'],
format: ['esm'],

View file

@ -139,21 +139,21 @@ function detectDuplicates(pkg: PackageJson): DependencyIssue[] {
if (!packageVersions.has(name)) {
packageVersions.set(name, []);
}
packageVersions.get(name)!.push({ version, type: 'dependencies' });
packageVersions.get(name)?.push({ version, type: 'dependencies' });
}
for (const [name, version] of Object.entries(devDeps)) {
if (!packageVersions.has(name)) {
packageVersions.set(name, []);
}
packageVersions.get(name)!.push({ version, type: 'devDependencies' });
packageVersions.get(name)?.push({ version, type: 'devDependencies' });
}
for (const [name, version] of Object.entries(peerDeps)) {
if (!packageVersions.has(name)) {
packageVersions.set(name, []);
}
packageVersions.get(name)!.push({ version, type: 'peerDependencies' });
packageVersions.get(name)?.push({ version, type: 'peerDependencies' });
}
// Find duplicates with different versions

View file

@ -119,7 +119,7 @@ const CATEGORY_KEYWORDS: Record<string, string[]> = {
* Determines the category for a FAQ based on question and answer content
*/
function categorize(question: string, answer: string): string {
const text = (question + ' ' + answer).toLowerCase();
const text = `${question} ${answer}`.toLowerCase();
// Check each category's keywords
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {

View file

@ -3,64 +3,65 @@
* This file provides metadata for the blocks validator
*/
import {
// Topology
executeTopology,
// Sessions
listSessions,
createSession,
getSession,
updateSession,
deleteSession,
// Messages
addMessage,
clearMessages,
// Prompts
listPrompts,
createApiKey,
createPrompt,
getPrompt,
updatePrompt,
deletePrompt,
incrementPromptUsage,
// User
getUserProfile,
updateUserProfile,
getUserStats,
// Env Vars
listEnvVars,
setEnvVar,
createSession,
deleteApiKey,
deleteEnvVar,
// Files
uploadFile,
getFile,
deleteFile,
// Models
listModels,
// Logs
getExecutionLogs,
// Metrics
getAgentMetrics,
// Tools
listTools,
deletePrompt,
deleteSession,
describeTool,
executeTool,
// Prompt Generation
generatePrompt,
improvePrompt,
// Topology
executeTopology,
// Export/Import
exportData,
importData,
// Prompt Generation
generatePrompt,
// Metrics
getAgentMetrics,
// Logs
getExecutionLogs,
getFile,
getPrompt,
getSession,
getStats,
// User
getUserProfile,
getUserStats,
// Public
healthCheck,
getStats,
importData,
improvePrompt,
incrementPromptUsage,
// API Keys
listApiKeys,
createApiKey,
deleteApiKey,
// Env Vars
listEnvVars,
// Models
listModels,
// Prompts
listPrompts,
// Sessions
listSessions,
// Tools
listTools,
setEnvVar,
updatePrompt,
updateSession,
updateUserProfile,
// Files
uploadFile,
} from './src/index.js';
export const block = {
name: 'hllm',
description: 'HLLM API client tools for AI agents. Manage topologies, sessions, prompts, files, and more.',
description:
'HLLM API client tools for AI agents. Manage topologies, sessions, prompts, files, and more.',
tools: {
// Topology
executeTopology,

View file

@ -87,9 +87,7 @@ function handleApiError(status: number, errorText: string): never {
case 400:
throw new Error(`Bad request: ${errorText}`);
case 401:
throw new Error(
'Authentication failed: Invalid API key. Ensure HLLM_API_KEY is correct.'
);
throw new Error('Authentication failed: Invalid API key. Ensure HLLM_API_KEY is correct.');
case 403:
throw new Error(`Access forbidden: ${errorText}`);
case 404:
@ -390,11 +388,7 @@ export const updateSession = tool({
}),
async execute(input: UpdateSessionInput): Promise<Session> {
const { sessionId, ...body } = input;
return apiRequest<Session>(
'PATCH',
`/chat/sessions/${encodeURIComponent(sessionId)}`,
body
);
return apiRequest<Session>('PATCH', `/chat/sessions/${encodeURIComponent(sessionId)}`, body);
},
});
@ -629,11 +623,7 @@ export const getPrompt = tool({
additionalProperties: false,
}),
async execute(input: { promptId: string }): Promise<Prompt> {
return apiRequest<Prompt>(
'GET',
`/prompts/${encodeURIComponent(input.promptId)}`,
undefined
);
return apiRequest<Prompt>('GET', `/prompts/${encodeURIComponent(input.promptId)}`, undefined);
},
});
@ -685,11 +675,7 @@ export const updatePrompt = tool({
}),
async execute(input: UpdatePromptInput): Promise<Prompt> {
const { promptId, ...body } = input;
return apiRequest<Prompt>(
'PATCH',
`/prompts/${encodeURIComponent(promptId)}`,
body
);
return apiRequest<Prompt>('PATCH', `/prompts/${encodeURIComponent(promptId)}`, body);
},
});
@ -991,11 +977,7 @@ export const getFile = tool({
additionalProperties: false,
}),
async execute(input: { fileId: string }): Promise<FileInfo> {
return apiRequest<FileInfo>(
'GET',
`/files/${encodeURIComponent(input.fileId)}`,
undefined
);
return apiRequest<FileInfo>('GET', `/files/${encodeURIComponent(input.fileId)}`, undefined);
},
});
@ -1134,11 +1116,14 @@ export interface AgentMetrics {
input: number;
output: number;
};
topologyBreakdown: Record<string, {
count: number;
avgLatency: number;
successRate: number;
}>;
topologyBreakdown: Record<
string,
{
count: number;
avgLatency: number;
successRate: number;
}
>;
periodStart: string;
periodEnd: string;
}
@ -1224,11 +1209,7 @@ export const describeTool = tool({
additionalProperties: false,
}),
async execute(input: { toolId: string }): Promise<ToolInfo> {
return apiRequest<ToolInfo>(
'POST',
'/tpmjs/describe',
{ toolId: input.toolId }
);
return apiRequest<ToolInfo>('POST', '/tpmjs/describe', { toolId: input.toolId });
},
});
@ -1265,11 +1246,10 @@ export const executeTool = tool({
additionalProperties: false,
}),
async execute(input: ExecuteToolInput): Promise<ExecuteToolResult> {
return apiRequest<ExecuteToolResult>(
'POST',
'/tpmjs/execute',
{ toolId: input.toolId, parameters: input.parameters }
);
return apiRequest<ExecuteToolResult>('POST', '/tpmjs/execute', {
toolId: input.toolId,
parameters: input.parameters,
});
},
});

View file

@ -42,7 +42,7 @@ export interface HtmlToMarkdownResult {
function countWords(text: string): number {
// Remove markdown formatting characters for accurate word count
const plainText = text
.replace(/[#*_`~\[\]()]/g, ' ') // Remove markdown syntax
.replace(/[#*_`~[\]()]/g, ' ') // Remove markdown syntax
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();

View file

@ -101,7 +101,7 @@ function extractVendorInfo(text: string): VendorInfo {
}
// Extract phone number
const phoneMatch = text.match(/(?:phone|tel|p):?\s*([0-9\-\(\)\s]{10,})/i);
const phoneMatch = text.match(/(?:phone|tel|p):?\s*([0-9\-()\s]{10,})/i);
const phone = phoneMatch?.[1]?.trim();
// Extract email
@ -109,7 +109,7 @@ function extractVendorInfo(text: string): VendorInfo {
const email = emailMatch?.[1];
// Extract tax ID
const taxIdMatch = text.match(/(?:tax\s*id|ein|vat):?\s*([0-9\-]+)/i);
const taxIdMatch = text.match(/(?:tax\s*id|ein|vat):?\s*([0-9-]+)/i);
const taxId = taxIdMatch?.[1];
// Extract address (simplified)
@ -133,12 +133,12 @@ function extractMetadata(text: string): {
invoiceDate?: string;
} {
// Extract invoice number
const invoiceNumMatch = text.match(/(?:invoice|inv)\s*(?:#|no|number):?\s*([A-Z0-9\-]+)/i);
const invoiceNumMatch = text.match(/(?:invoice|inv)\s*(?:#|no|number):?\s*([A-Z0-9-]+)/i);
const invoiceNumber = invoiceNumMatch?.[1];
// Extract invoice date
const dateMatch = text.match(
/(?:date|dated|invoice\s+date):?\s*([0-9]{1,2}[\/\-][0-9]{1,2}[\/\-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
/(?:date|dated|invoice\s+date):?\s*([0-9]{1,2}[/-][0-9]{1,2}[/-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
);
const invoiceDate = dateMatch?.[1];
@ -267,12 +267,12 @@ function extractTotals(text: string): {
*/
function extractPaymentTerms(text: string): PaymentTerms | undefined {
const dueDateMatch = text.match(
/(?:due\s*date|payment\s*due):?\s*([0-9]{1,2}[\/\-][0-9]{1,2}[\/\-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
/(?:due\s*date|payment\s*due):?\s*([0-9]{1,2}[/-][0-9]{1,2}[/-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
);
const dueDate = dueDateMatch?.[1];
const netDaysMatch = text.match(/(?:net|due\s+in)\s+(\d+)\s*(?:days?)/i);
const netDays = netDaysMatch?.[1] ? Number.parseInt(netDaysMatch[1]) : undefined;
const netDays = netDaysMatch?.[1] ? Number.parseInt(netDaysMatch[1], 10) : undefined;
if (!dueDate && !netDays) {
return undefined;

View file

@ -56,13 +56,13 @@ function extractNetDays(text: string): number | null {
// Match "Net XX" or "Net XX days"
const netMatch = normalizedText.match(/net\s+(\d+)(?:\s+days?)?/i);
if (netMatch && netMatch[1]) {
if (netMatch?.[1]) {
return Number.parseInt(netMatch[1], 10);
}
// Match "XX days" in payment terms context
const daysMatch = normalizedText.match(/(?:payment\s+)?(?:due\s+)?(?:in\s+)?(\d+)\s+days/i);
if (daysMatch && daysMatch[1]) {
if (daysMatch?.[1]) {
return Number.parseInt(daysMatch[1], 10);
}
@ -84,15 +84,15 @@ function extractNetDays(text: string): number | null {
function extractDueDate(text: string): string | null {
// Match common date formats: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD
const datePatterns = [
/due(?:\s+date)?:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/payment\s+due:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/due\s+by:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/due\s+on:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/due(?:\s+date)?:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
/payment\s+due:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
/due\s+by:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
/due\s+on:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
];
for (const pattern of datePatterns) {
const match = text.match(pattern);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}
}
@ -101,7 +101,7 @@ function extractDueDate(text: string): string | null {
const writtenDateMatch = text.match(
/due\s+(?:date|on|by)?:?\s*(\w+\s+\d{1,2},?\s+\d{4}|\d{1,2}\s+\w+\s+\d{4})/i
);
if (writtenDateMatch && writtenDateMatch[1]) {
if (writtenDateMatch?.[1]) {
return writtenDateMatch[1];
}
@ -113,14 +113,14 @@ function extractDueDate(text: string): string | null {
*/
function extractInvoiceDate(text: string): string | null {
const datePatterns = [
/invoice\s+date:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/date:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/dated:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
/invoice\s+date:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
/date:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
/dated:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
];
for (const pattern of datePatterns) {
const match = text.match(pattern);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}
}
@ -133,14 +133,14 @@ function extractInvoiceDate(text: string): string | null {
*/
function extractInvoiceNumber(text: string): string | null {
const patterns = [
/invoice\s+(?:number|#|no\.?):?\s*([A-Z0-9\-]+)/i,
/invoice:?\s+([A-Z0-9\-]+)/i,
/#\s*([A-Z0-9\-]+)/,
/invoice\s+(?:number|#|no\.?):?\s*([A-Z0-9-]+)/i,
/invoice:?\s+([A-Z0-9-]+)/i,
/#\s*([A-Z0-9-]+)/,
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}
}
@ -169,7 +169,7 @@ function extractTotalAmount(text: string): { amount: number | null; currency: st
if (amountStr) {
const amount = Number.parseFloat(amountStr.replace(/,/g, ''));
if (!isNaN(amount)) {
if (!Number.isNaN(amount)) {
return { amount, currency };
}
}
@ -189,7 +189,7 @@ function extractLateFee(text: string): LateFee | null {
const percentageMatch = normalizedText.match(
/([\d.]+)%\s*(?:per\s+month|monthly|late\s+fee|interest)/i
);
if (percentageMatch && percentageMatch[1]) {
if (percentageMatch?.[1]) {
return {
type: 'monthly',
amount: Number.parseFloat(percentageMatch[1]),
@ -199,7 +199,7 @@ function extractLateFee(text: string): LateFee | null {
// Match daily percentage: "0.05% per day"
const dailyMatch = normalizedText.match(/([\d.]+)%\s*(?:per\s+day|daily)/i);
if (dailyMatch && dailyMatch[1]) {
if (dailyMatch?.[1]) {
return {
type: 'daily',
amount: Number.parseFloat(dailyMatch[1]),
@ -209,7 +209,7 @@ function extractLateFee(text: string): LateFee | null {
// Match fixed fee: "$25 late fee" or "late fee of $50"
const fixedMatch = text.match(/(?:late\s+fee|fee)(?:\s+of)?\s*\$?\s*([\d.]+)/i);
if (fixedMatch && fixedMatch[1]) {
if (fixedMatch?.[1]) {
return {
type: 'fixed',
amount: Number.parseFloat(fixedMatch[1]),
@ -227,7 +227,7 @@ function extractLateFee(text: string): LateFee | null {
function extractEarlyPaymentDiscount(text: string): EarlyPaymentDiscount | null {
// Match "2/10 Net 30" format (2% discount if paid within 10 days)
const discountMatch = text.match(/(\d+)\/(\d+)\s+(?:net|n)\s+\d+/i);
if (discountMatch && discountMatch[1] && discountMatch[2]) {
if (discountMatch?.[1] && discountMatch[2]) {
return {
discountPercentage: Number.parseInt(discountMatch[1], 10),
paymentDays: Number.parseInt(discountMatch[2], 10),
@ -237,7 +237,7 @@ function extractEarlyPaymentDiscount(text: string): EarlyPaymentDiscount | null
// Match "X% discount if paid within Y days"
const explicitMatch = text.match(/(\d+)%\s+discount\s+(?:if\s+paid\s+)?within\s+(\d+)\s+days/i);
if (explicitMatch && explicitMatch[1] && explicitMatch[2]) {
if (explicitMatch?.[1] && explicitMatch[2]) {
return {
discountPercentage: Number.parseInt(explicitMatch[1], 10),
paymentDays: Number.parseInt(explicitMatch[2], 10),

View file

@ -6,7 +6,8 @@ import { judgeConversation } from './src/index.js';
export const block = {
name: 'judge',
description: 'AI conversation quality judge. Evaluates AI SDK messages across 10 metrics with scores, reasoning, suggestions, and actionable improvements.',
description:
'AI conversation quality judge. Evaluates AI SDK messages across 10 metrics with scores, reasoning, suggestions, and actionable improvements.',
tools: {
judgeConversation,
},

View file

@ -35,7 +35,7 @@ export type EvaluationMetric = (typeof EVALUATION_METRICS)[number];
* Metric descriptions for context
*/
export const METRIC_DESCRIPTIONS: Record<EvaluationMetric, string> = {
taskCompletion: 'Did the AI make meaningful progress toward completing the user\'s request?',
taskCompletion: "Did the AI make meaningful progress toward completing the user's request?",
accuracy: 'Are the responses factually correct and free of hallucinations or errors?',
relevance: 'Are responses directly addressing what the user asked for?',
clarity: 'Are responses clear, well-structured, and easy to understand?',
@ -51,16 +51,16 @@ export const METRIC_DESCRIPTIONS: Record<EvaluationMetric, string> = {
* Metric weights for overall score calculation
*/
export const METRIC_WEIGHTS: Record<EvaluationMetric, number> = {
taskCompletion: 2.0, // Most important - did we do what was asked?
taskCompletion: 2.0, // Most important - did we do what was asked?
userIntentAlignment: 1.5, // Critical - understanding the user
completeness: 1.5, // Important - full coverage
accuracy: 1.5, // Important - correctness
progress: 1.2, // Catches loops
actionability: 1.0, // Usability
relevance: 1.0, // On-topic
errorHandling: 1.0, // Resilience
clarity: 0.8, // Nice to have
efficiency: 0.5, // Least critical
completeness: 1.5, // Important - full coverage
accuracy: 1.5, // Important - correctness
progress: 1.2, // Catches loops
actionability: 1.0, // Usability
relevance: 1.0, // On-topic
errorHandling: 1.0, // Resilience
clarity: 0.8, // Nice to have
efficiency: 0.5, // Least critical
};
/**
@ -127,7 +127,7 @@ interface JudgeInput {
*/
function detectLoops(messages: Message[]): { detected: boolean; details: string[] } {
const details: string[] = [];
const assistantMessages = messages.filter(m => m.role === 'assistant');
const assistantMessages = messages.filter((m) => m.role === 'assistant');
if (assistantMessages.length < 2) {
return { detected: false, details: [] };
@ -168,7 +168,7 @@ function detectLoops(messages: Message[]): { detected: boolean; details: string[
// Check for oscillation patterns
if (assistantMessages.length >= 4) {
const last4 = assistantMessages.slice(-4).map(m => m.content.slice(0, 100));
const last4 = assistantMessages.slice(-4).map((m) => m.content.slice(0, 100));
if (last4[0] === last4[2] && last4[1] === last4[3]) {
details.push('Detected oscillation pattern in last 4 responses');
}
@ -186,10 +186,10 @@ function detectLoops(messages: Message[]): { detected: boolean; details: string[
function extractUserRequest(messages: Message[], provided?: string): string {
if (provided) return provided;
const userMessages = messages.filter(m => m.role === 'user');
const userMessages = messages.filter((m) => m.role === 'user');
if (userMessages.length === 0) return 'Unknown request';
return userMessages[0]!.content;
return userMessages[0]?.content;
}
/**
@ -216,7 +216,11 @@ function analyzeToolUsage(messages: Message[]): {
if (msg.toolResults) {
for (const result of msg.toolResults) {
const resultStr = JSON.stringify(result.result).toLowerCase();
if (resultStr.includes('error') || resultStr.includes('failed') || resultStr.includes('exception')) {
if (
resultStr.includes('error') ||
resultStr.includes('failed') ||
resultStr.includes('exception')
) {
failedCalls++;
} else {
successfulCalls++;
@ -249,16 +253,26 @@ function evaluateMetric(
let score = 5;
let reason = '';
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
const assistantCount = messages.filter(m => m.role === 'assistant').length;
const userCount = messages.filter(m => m.role === 'user').length;
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant');
const assistantCount = messages.filter((m) => m.role === 'assistant').length;
const userCount = messages.filter((m) => m.role === 'user').length;
switch (metric) {
case 'taskCompletion': {
// Check if there's evidence of task completion
const lastContent = lastAssistant?.content.toLowerCase() || '';
const completionIndicators = ['done', 'completed', 'finished', 'created', 'updated', 'fixed', 'resolved', 'here is', 'here\'s'];
const hasCompletion = completionIndicators.some(ind => lastContent.includes(ind));
const completionIndicators = [
'done',
'completed',
'finished',
'created',
'updated',
'fixed',
'resolved',
'here is',
"here's",
];
const hasCompletion = completionIndicators.some((ind) => lastContent.includes(ind));
if (hasCompletion && toolStats.successfulCalls > 0) {
score = 8;
@ -285,7 +299,9 @@ function evaluateMetric(
score = 3;
reason = 'More failed tool calls than successful ones indicates potential accuracy issues';
mustDos.push('Investigate and fix the failing tool calls');
observations.push(`${toolStats.failedCalls} failed vs ${toolStats.successfulCalls} successful calls`);
observations.push(
`${toolStats.failedCalls} failed vs ${toolStats.successfulCalls} successful calls`
);
} else if (toolStats.successfulCalls > 0) {
score = 7;
reason = 'Successful tool calls suggest accurate execution';
@ -300,10 +316,17 @@ function evaluateMetric(
case 'relevance': {
// Simple heuristic: check if user request terms appear in responses
const requestTerms = userRequest.toLowerCase().split(/\s+/).filter(t => t.length > 3);
const responseText = messages.filter(m => m.role === 'assistant').map(m => m.content.toLowerCase()).join(' ');
const matchedTerms = requestTerms.filter(t => responseText.includes(t));
const relevanceRatio = requestTerms.length > 0 ? matchedTerms.length / requestTerms.length : 0.5;
const requestTerms = userRequest
.toLowerCase()
.split(/\s+/)
.filter((t) => t.length > 3);
const responseText = messages
.filter((m) => m.role === 'assistant')
.map((m) => m.content.toLowerCase())
.join(' ');
const matchedTerms = requestTerms.filter((t) => responseText.includes(t));
const relevanceRatio =
requestTerms.length > 0 ? matchedTerms.length / requestTerms.length : 0.5;
score = Math.round(relevanceRatio * 10);
if (score >= 7) {
@ -320,7 +343,8 @@ function evaluateMetric(
}
case 'clarity': {
const totalWords = messages.filter(m => m.role === 'assistant')
const totalWords = messages
.filter((m) => m.role === 'assistant')
.reduce((sum, m) => sum + m.content.split(/\s+/).length, 0);
const avgWordsPerResponse = assistantCount > 0 ? totalWords / assistantCount : 0;
@ -360,11 +384,14 @@ function evaluateMetric(
case 'userIntentAlignment': {
// Check if the conversation seems to understand the request
const hasQuestions = messages.some(m => m.role === 'assistant' && m.content.includes('?'));
const hasConfirmation = messages.some(m => m.role === 'assistant' &&
(m.content.toLowerCase().includes('i understand') ||
m.content.toLowerCase().includes('let me') ||
m.content.toLowerCase().includes('i\'ll')));
const hasQuestions = messages.some((m) => m.role === 'assistant' && m.content.includes('?'));
const hasConfirmation = messages.some(
(m) =>
m.role === 'assistant' &&
(m.content.toLowerCase().includes('i understand') ||
m.content.toLowerCase().includes('let me') ||
m.content.toLowerCase().includes("i'll"))
);
if (hasConfirmation && !hasQuestions) {
score = 8;
@ -387,9 +414,14 @@ function evaluateMetric(
case 'actionability': {
const lastContent = lastAssistant?.content || '';
const hasCode = lastContent.includes('```') || lastContent.includes('function') || lastContent.includes('const ');
const hasCode =
lastContent.includes('```') ||
lastContent.includes('function') ||
lastContent.includes('const ');
const hasSteps = /\d+\.\s/.test(lastContent) || lastContent.includes('- ');
const hasFiles = toolStats.toolsUsed.some(t => t.toLowerCase().includes('write') || t.toLowerCase().includes('create'));
const hasFiles = toolStats.toolsUsed.some(
(t) => t.toLowerCase().includes('write') || t.toLowerCase().includes('create')
);
if (hasFiles) {
score = 9;
@ -446,18 +478,29 @@ function evaluateMetric(
score = 5;
reason = 'Error handling status unclear';
}
observations.push(`${toolStats.failedCalls} failed, ${toolStats.successfulCalls} successful calls`);
observations.push(
`${toolStats.failedCalls} failed, ${toolStats.successfulCalls} successful calls`
);
break;
}
case 'completeness': {
// Check for common incompleteness indicators
const lastContent = lastAssistant?.content.toLowerCase() || '';
const incompleteIndicators = ['todo', 'later', 'next time', 'will add', 'placeholder', 'not yet', 'tbd', '...'];
const hasIncomplete = incompleteIndicators.some(ind => lastContent.includes(ind));
const incompleteIndicators = [
'todo',
'later',
'next time',
'will add',
'placeholder',
'not yet',
'tbd',
'...',
];
const hasIncomplete = incompleteIndicators.some((ind) => lastContent.includes(ind));
const completeIndicators = ['all done', 'complete', 'finished', 'everything', 'all set'];
const hasComplete = completeIndicators.some(ind => lastContent.includes(ind));
const hasComplete = completeIndicators.some((ind) => lastContent.includes(ind));
if (hasComplete && !hasIncomplete) {
score = 9;
@ -517,8 +560,8 @@ function determineVerdict(
const threshold = strictMode ? 7 : 5;
// Critical failures that always result in retry/fail
const taskCompletion = metrics.find(m => m.metric === 'taskCompletion')?.score || 0;
const progress = metrics.find(m => m.metric === 'progress')?.score || 0;
const taskCompletion = metrics.find((m) => m.metric === 'taskCompletion')?.score || 0;
const progress = metrics.find((m) => m.metric === 'progress')?.score || 0;
if (loopDetected) {
return { verdict: 'retry', reason: 'Loop detected - must try a different approach' };
@ -550,9 +593,12 @@ function determineVerdict(
/**
* Generate conversation summary
*/
function generateSummary(messages: Message[], toolStats: ReturnType<typeof analyzeToolUsage>): string {
const userCount = messages.filter(m => m.role === 'user').length;
const assistantCount = messages.filter(m => m.role === 'assistant').length;
function generateSummary(
messages: Message[],
toolStats: ReturnType<typeof analyzeToolUsage>
): string {
const userCount = messages.filter((m) => m.role === 'user').length;
const assistantCount = messages.filter((m) => m.role === 'assistant').length;
return `Conversation with ${userCount} user message(s), ${assistantCount} assistant response(s), and ${toolStats.totalCalls} tool call(s) (${toolStats.successfulCalls} successful, ${toolStats.failedCalls} failed). Tools used: ${toolStats.toolsUsed.join(', ') || 'none'}.`;
}
@ -587,7 +633,8 @@ Returns a verdict (pass/retry/fail) with specific must-dos for any issues.`,
properties: {
messages: {
type: 'array',
description: 'Array of AI SDK messages to evaluate. Each message should have role and content.',
description:
'Array of AI SDK messages to evaluate. Each message should have role and content.',
items: {
type: 'object',
properties: {
@ -665,21 +712,26 @@ Returns a verdict (pass/retry/fail) with specific must-dos for any issues.`,
const toolStats = analyzeToolUsage(messages);
// Evaluate all metrics
const metrics = EVALUATION_METRICS.map(metric =>
const metrics = EVALUATION_METRICS.map((metric) =>
evaluateMetric(metric, messages, userRequest, loopInfo, toolStats)
);
const overallScore = calculateOverallScore(metrics);
const { verdict, reason: verdictReason } = determineVerdict(metrics, overallScore, loopInfo.detected, strictMode);
const { verdict, reason: verdictReason } = determineVerdict(
metrics,
overallScore,
loopInfo.detected,
strictMode
);
// Aggregate must-dos and suggestions
const allMustDos = metrics.flatMap(m => m.mustDos);
const allSuggestions = metrics.flatMap(m => m.suggestions);
const allMustDos = metrics.flatMap((m) => m.mustDos);
const allSuggestions = metrics.flatMap((m) => m.suggestions);
// Identify critical issues (metrics with score <= 3)
const criticalIssues = metrics
.filter(m => m.score <= 3)
.map(m => `${m.metric}: ${m.reason}`);
.filter((m) => m.score <= 3)
.map((m) => `${m.metric}: ${m.reason}`);
// Generate next steps based on verdict
const nextSteps: string[] = [];

View file

@ -52,7 +52,7 @@ function transpose(matrix: number[][]): number[][] {
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
result[j]![i] = matrix[i]![j] ?? 0;
result[j]![i] = matrix[i]?.[j] ?? 0;
}
}
return result;
@ -74,7 +74,7 @@ function matrixMultiply(a: number[][], b: number[][]): number[][] {
for (let j = 0; j < bCols; j++) {
let sum = 0;
for (let k = 0; k < aCols; k++) {
sum += (a[i]![k] ?? 0) * (b[k]![j] ?? 0);
sum += (a[i]?.[k] ?? 0) * (b[k]?.[j] ?? 0);
}
result[i]![j] = sum;
}
@ -100,27 +100,27 @@ function invertMatrix(matrix: number[][]): number[][] {
for (let i = 0; i < n; i++) {
let maxRow = i;
for (let k = i + 1; k < n; k++) {
if (Math.abs(augmented[k]![i] ?? 0) > Math.abs(augmented[maxRow]![i] ?? 0)) {
if (Math.abs(augmented[k]?.[i] ?? 0) > Math.abs(augmented[maxRow]?.[i] ?? 0)) {
maxRow = k;
}
}
[augmented[i], augmented[maxRow]] = [augmented[maxRow]!, augmented[i]!];
const pivot = augmented[i]![i];
const pivot = augmented[i]?.[i];
if (Math.abs(pivot ?? 0) < 1e-10) {
throw new Error('Matrix is singular - possible multicollinearity');
}
for (let j = 0; j < 2 * n; j++) {
augmented[i]![j] = (augmented[i]![j] ?? 0) / (pivot ?? 1);
augmented[i]![j] = (augmented[i]?.[j] ?? 0) / (pivot ?? 1);
}
for (let k = 0; k < n; k++) {
if (k !== i) {
const factor = augmented[k]![i] ?? 0;
const factor = augmented[k]?.[i] ?? 0;
for (let j = 0; j < 2 * n; j++) {
augmented[k]![j] = (augmented[k]![j] ?? 0) - factor * (augmented[i]![j] ?? 0);
augmented[k]![j] = (augmented[k]?.[j] ?? 0) - factor * (augmented[i]?.[j] ?? 0);
}
}
}
@ -191,7 +191,7 @@ function performLinearRegression(X: number[][], y: number[]): RegressionResult {
let XTXInv: number[][];
try {
XTXInv = invertMatrix(XTX);
} catch (error) {
} catch (_error) {
warnings.push('Multicollinearity detected: matrix inversion failed');
throw new Error('Cannot perform regression: multicollinearity issue');
}
@ -210,7 +210,7 @@ function performLinearRegression(X: number[][], y: number[]): RegressionResult {
for (let i = 0; i < n; i++) {
let predicted = intercept;
for (let j = 0; j < numFeatures; j++) {
predicted += (coefficients[j] ?? 0) * (X[i]![j] ?? 0);
predicted += (coefficients[j] ?? 0) * (X[i]?.[j] ?? 0);
}
predictions.push(predicted);
residuals.push((y[i] ?? 0) - predicted);
@ -313,7 +313,7 @@ export const linearRegressionOLSTool = tool({
// Validate all elements are numbers
for (let i = 0; i < X.length; i++) {
for (let j = 0; j < numFeatures; j++) {
const val = X[i]![j];
const val = X[i]?.[j];
if (typeof val !== 'number' || !Number.isFinite(val)) {
throw new Error(`X must contain only finite numbers (invalid at row ${i}, column ${j})`);
}

View file

@ -257,7 +257,7 @@ function checkLinkFormatting(lines: string[]): LintIssue[] {
if (!line) continue;
// Check for bare URLs (not in links or code)
const bareUrlMatch = line.match(/(?<![\(\[`])(https?:\/\/[^\s\)]+)(?![\)\]`])/);
const bareUrlMatch = line.match(/(?<![([`])(https?:\/\/[^\s)]+)(?![)\]`])/);
if (bareUrlMatch?.[1]) {
const url = bareUrlMatch[1];
// Make sure it's not in a code block or already a link

View file

@ -6,7 +6,7 @@
*/
import { jsonSchema, tool } from 'ai';
import { type Token, marked } from 'marked';
import { marked, type Token } from 'marked';
/**
* Heading information extracted from markdown

View file

@ -50,7 +50,7 @@ type OnboardingChecklistInput = {
*/
function validateDate(dateStr: string): boolean {
const date = new Date(dateStr);
return !isNaN(date.getTime());
return !Number.isNaN(date.getTime());
}
/**
@ -404,7 +404,7 @@ function formatChecklist(
if (!byOwner.has(task.owner)) {
byOwner.set(task.owner, []);
}
byOwner.get(task.owner)!.push(task);
byOwner.get(task.owner)?.push(task);
}
// Display tasks grouped by owner

View file

@ -128,7 +128,7 @@ function generateOpenAPIYaml(endpoints: EndpointDefinition[]): string {
if (!pathGroups.has(endpoint.path)) {
pathGroups.set(endpoint.path, []);
}
pathGroups.get(endpoint.path)!.push(endpoint);
pathGroups.get(endpoint.path)?.push(endpoint);
}
// Domain rule: openapi_format - Generate path and method entries

View file

@ -104,7 +104,7 @@ function pivotData(
}
// Accumulate values for aggregation
rowData.get(colValue)!.push(cellValue);
rowData.get(colValue)?.push(cellValue);
}
// Build pivoted array

View file

@ -203,7 +203,7 @@ function extractPurpose(content: string): string {
const purposeMatch = content.match(
/(?:purpose|objective|intent)[\s:]+(.+?)(?=\n\n|scope|policy|$)/is
);
if (purposeMatch && purposeMatch[1]) {
if (purposeMatch?.[1]) {
return purposeMatch[1].trim();
}
@ -216,7 +216,7 @@ function extractPurpose(content: string): string {
*/
function extractScope(content: string): string {
const scopeMatch = content.match(/scope[\s:]+(.+?)(?=\n\n|policy|procedure|$)/is);
if (scopeMatch && scopeMatch[1]) {
if (scopeMatch?.[1]) {
return scopeMatch[1].trim();
}
@ -229,7 +229,7 @@ function extractScope(content: string): string {
*/
function extractPolicyStatement(content: string): string {
const statementMatch = content.match(/policy[\s:]+(.+?)(?=\n\n|procedure|$)/is);
if (statementMatch && statementMatch[1]) {
if (statementMatch?.[1]) {
return statementMatch[1].trim();
}
@ -246,7 +246,7 @@ function extractProcedures(content: string): string[] {
// Look for numbered lists or bullet points
const procedureSection = content.match(/procedure[s]?[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
if (procedureSection && procedureSection[1]) {
if (procedureSection?.[1]) {
const text = procedureSection[1];
const matches = text.match(/(?:^|\n)\s*(?:\d+\.|[-*])\s*(.+)/gm);
if (matches) {
@ -276,7 +276,7 @@ function extractResponsibilities(content: string): Record<string, string[]> {
// Look for responsibility sections
const respMatch = content.match(/responsibilit(?:ies|y)[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
if (respMatch && respMatch[1]) {
if (respMatch?.[1]) {
const text = respMatch[1];
const roleMatches = text.matchAll(/([A-Za-z\s]+?)[:]\s*(.+?)(?=\n[A-Z]|$)/gs);
@ -295,11 +295,11 @@ function extractResponsibilities(content: string): Record<string, string[]> {
// Default responsibilities if none found
if (Object.keys(responsibilities).length === 0) {
responsibilities['Employees'] = [
responsibilities.Employees = [
'Comply with policy requirements',
'Report violations or concerns',
];
responsibilities['Managers'] = [
responsibilities.Managers = [
'Ensure team compliance',
'Address policy violations',
'Provide guidance and support',
@ -321,7 +321,7 @@ function extractDefinitions(content: string): Map<string, string> {
const definitions = new Map<string, string>();
const defMatch = content.match(/definition[s]?[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
if (defMatch && defMatch[1]) {
if (defMatch?.[1]) {
const text = defMatch[1];
const termMatches = text.matchAll(/([A-Za-z\s]+?)[:]\s*(.+?)(?=\n|$)/g);
@ -344,7 +344,7 @@ function extractRelatedPolicies(content: string): string[] {
const policies: string[] = [];
const relatedMatch = content.match(/related\s+polic(?:ies|y)[\s:]+(.+?)(?=\n\n|$)/is);
if (relatedMatch && relatedMatch[1]) {
if (relatedMatch?.[1]) {
const text = relatedMatch[1];
const policyMatches = text.match(/(?:^|\n)\s*(?:\d+\.|[-*])\s*(.+)/gm);
if (policyMatches) {
@ -360,7 +360,7 @@ function extractRelatedPolicies(content: string): string[] {
*/
function extractEnforcement(content: string): string {
const enforcementMatch = content.match(/enforcement|violation[s]?[\s:]+(.+?)(?=\n\n|$)/is);
if (enforcementMatch && enforcementMatch[1]) {
if (enforcementMatch?.[1]) {
return enforcementMatch[1].trim();
}

View file

@ -207,7 +207,7 @@ function generateValueProposition(
function formatPrice(price: number | string, billingPeriod?: string): string {
const priceNum = typeof price === 'string' ? Number.parseFloat(price) : price;
if (isNaN(priceNum)) {
if (Number.isNaN(priceNum)) {
return String(price);
}

View file

@ -146,7 +146,7 @@ function generateProposedSolution(opp: OpportunityData): ProposalSection {
return {
title: 'Proposed Solution',
content: [
'Comprehensive solution designed specifically for ' + opp.customerName,
`Comprehensive solution designed specifically for ${opp.customerName}`,
'Addresses all stated requirements and pain points',
],
subsections,

View file

@ -74,19 +74,19 @@ function stringSimilarity(str1: string, str2: string): number {
for (let i = 1; i <= s2.length; i++) {
for (let j = 1; j <= s1.length; j++) {
if (s2.charAt(i - 1) === s1.charAt(j - 1)) {
matrix[i]![j] = matrix[i - 1]![j - 1]!;
matrix[i]![j] = matrix[i - 1]?.[j - 1]!;
} else {
matrix[i]![j] = Math.min(
matrix[i - 1]![j - 1]! + 1,
matrix[i]![j - 1]! + 1,
matrix[i - 1]![j]! + 1
matrix[i - 1]?.[j - 1]! + 1,
matrix[i]?.[j - 1]! + 1,
matrix[i - 1]?.[j]! + 1
);
}
}
}
const maxLength = Math.max(s1.length, s2.length);
return 1 - matrix[s2.length]![s1.length]! / maxLength;
return 1 - matrix[s2.length]?.[s1.length]! / maxLength;
}
/**

View file

@ -53,12 +53,12 @@ const SECRET_PATTERNS: Array<{
},
{
type: 'aws-secret-key',
pattern: /aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]/gi,
pattern: /aws(.{0,20})?['"][0-9a-zA-Z/+]{40}['"]/gi,
description: 'AWS Secret Access Key',
},
{
type: 'aws-account-id',
pattern: /aws(.{0,20})?['\"]?[0-9]{12}['\"]?/gi,
pattern: /aws(.{0,20})?['"]?[0-9]{12}['"]?/gi,
description: 'AWS Account ID',
},
@ -109,12 +109,12 @@ const SECRET_PATTERNS: Array<{
// Generic API Keys
{
type: 'generic-api-key',
pattern: /api[_-]?key['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
description: 'Generic API Key',
},
{
type: 'generic-secret',
pattern: /secret['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
pattern: /secret['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
description: 'Generic Secret',
},
@ -155,7 +155,7 @@ const SECRET_PATTERNS: Array<{
// Generic Passwords in Code
{
type: 'hardcoded-password',
pattern: /password['\"]?\s*[:=]\s*['\"][^'\"]{8,}['\"](?!\s*=)/gi,
pattern: /password['"]?\s*[:=]\s*['"][^'"]{8,}['"](?!\s*=)/gi,
description: 'Hardcoded Password',
},
@ -190,7 +190,7 @@ const SECRET_PATTERNS: Array<{
// Generic Bearer Tokens
{
type: 'bearer-token',
pattern: /bearer\s+[a-zA-Z0-9_\-\.=]{20,}/gi,
pattern: /bearer\s+[a-zA-Z0-9_\-.=]{20,}/gi,
description: 'Bearer Token',
},
@ -237,7 +237,7 @@ async function generateFingerprint(value: string): Promise<string> {
hash = hash & hash; // Convert to 32bit integer
}
return `fallback-${Math.abs(hash).toString(16)}`;
} catch (error) {
} catch (_error) {
// If all else fails, return a placeholder
return `error-${value.length}`;
}

View file

@ -78,7 +78,7 @@ function normalizeToIsoDate(dateStr: string | undefined): string | undefined {
if (!dateStr) return undefined;
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr; // Return original if parsing fails
if (Number.isNaN(date.getTime())) return dateStr; // Return original if parsing fails
return date.toISOString();
} catch {
return dateStr; // Return original on error

View file

@ -61,13 +61,13 @@ const SECRET_PATTERNS: Array<{
},
{
type: 'aws-secret-key',
pattern: /aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]/gi,
pattern: /aws(.{0,20})?['"][0-9a-zA-Z/+]{40}['"]/gi,
severity: 'critical',
description: 'AWS Secret Access Key',
},
{
type: 'aws-account-id',
pattern: /aws(.{0,20})?['\"]?[0-9]{12}['\"]?/gi,
pattern: /aws(.{0,20})?['"]?[0-9]{12}['"]?/gi,
severity: 'medium',
description: 'AWS Account ID',
},
@ -126,13 +126,13 @@ const SECRET_PATTERNS: Array<{
// Generic API Keys
{
type: 'generic-api-key',
pattern: /api[_-]?key['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
severity: 'high',
description: 'Generic API Key',
},
{
type: 'generic-secret',
pattern: /secret['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
pattern: /secret['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
severity: 'high',
description: 'Generic Secret',
},
@ -179,7 +179,7 @@ const SECRET_PATTERNS: Array<{
// Generic Passwords in Code
{
type: 'hardcoded-password',
pattern: /password['\"]?\s*[:=]\s*['\"][^'\"]{8,}['\"](?!\s*=)/gi,
pattern: /password['"]?\s*[:=]\s*['"][^'"]{8,}['"](?!\s*=)/gi,
severity: 'high',
description: 'Hardcoded Password',
},
@ -219,7 +219,7 @@ const SECRET_PATTERNS: Array<{
// Generic Bearer Tokens
{
type: 'bearer-token',
pattern: /bearer\s+[a-zA-Z0-9_\-\.=]{20,}/gi,
pattern: /bearer\s+[a-zA-Z0-9_\-.=]{20,}/gi,
severity: 'high',
description: 'Bearer Token',
},

View file

@ -161,12 +161,12 @@ function formatPostContent(
break;
case 'casual':
if (!content.includes('!') && !content.includes('?')) {
content = content + '!';
content = `${content}!`;
}
break;
case 'friendly':
if (!content.match(/[!?😊😃🎉]/u)) {
content = content + ' 😊';
content = `${content} 😊`;
}
break;
}
@ -177,7 +177,7 @@ function formatPostContent(
case 'twitter':
// Twitter prefers concise, punchy content
if (content.length > 240) {
content = content.substring(0, 237) + '...';
content = `${content.substring(0, 237)}...`;
}
break;
case 'linkedin':
@ -185,7 +185,7 @@ function formatPostContent(
if (!content.includes('\n\n') && content.length > 150) {
const firstSentence = content.match(/^[^.!?]+[.!?]/)?.[0];
if (firstSentence) {
content = firstSentence + '\n\n' + content.substring(firstSentence.length);
content = `${firstSentence}\n\n${content.substring(firstSentence.length)}`;
}
}
break;
@ -300,12 +300,12 @@ export const socialPostDraftTool = tool({
// Add CTA if not already in content
if (cta && !formattedContent.toLowerCase().includes(cta.toLowerCase())) {
finalPost += '\n\n' + cta;
finalPost += `\n\n${cta}`;
}
// Add hashtags at the end
if (hashtags.length > 0) {
finalPost += '\n\n' + hashtags.join(' ');
finalPost += `\n\n${hashtags.join(' ')}`;
}
const characterCount = finalPost.length;

View file

@ -146,7 +146,7 @@ export const spritesCheckpointCreateTool = tool({
// Also try to extract from complete message like "Checkpoint v2 created successfully"
if (parsed.type === 'complete' && parsed.data && !checkpointId) {
const match = parsed.data.match(/Checkpoint\s+(\S+)\s+created/);
if (match && match[1]) {
if (match?.[1]) {
checkpointId = match[1];
}
}

View file

@ -277,7 +277,7 @@ export const taxDeductionScanTool = tool({
if (!categoryMap.has(normalizedCategory)) {
categoryMap.set(normalizedCategory, []);
}
categoryMap.get(normalizedCategory)!.push(expense);
categoryMap.get(normalizedCategory)?.push(expense);
totalExpenses += expense.amount;
}
@ -287,7 +287,7 @@ export const taxDeductionScanTool = tool({
const flaggedForReview: string[] = [];
for (const [category, categoryExpenses] of categoryMap.entries()) {
const rules = DEDUCTION_RULES[category] || DEDUCTION_RULES['other']!;
const rules = DEDUCTION_RULES[category] || DEDUCTION_RULES.other!;
// Check for entity-specific rules
let deductionRate = rules.rate;

View file

@ -40,7 +40,7 @@ function extractTemplateVariables(template: string): string[] {
if (match[1]) {
const variable = match[1].trim();
// Remove mustache directives (#, ^, /, etc.)
const cleanVariable = variable.replace(/^[#^\/&]/, '').trim();
const cleanVariable = variable.replace(/^[#^/&]/, '').trim();
if (cleanVariable) {
variables.add(cleanVariable);
}

View file

@ -76,14 +76,14 @@ function chunkBySentences(
if (overlap > 0) {
// Calculate how many sentences to include for overlap
let overlapText = '';
let overlapSentences = 0;
let _overlapSentences = 0;
for (let j = i - 1; j >= 0 && overlapText.length < overlap; j--) {
const overlapSentence = sentences[j];
if (!overlapSentence) continue;
if (overlapText.length + overlapSentence.length <= overlap) {
overlapText = `${overlapSentence} ${overlapText}`;
overlapSentences++;
_overlapSentences++;
} else {
break;
}
@ -221,7 +221,7 @@ export const textChunkTool = tool({
if (chunks.length === 0) {
chunks = chunkBySize(text, maxChunkSize, overlap);
}
} catch (error) {
} catch (_error) {
// Fall back to simple size-based chunking if sentence detection fails
chunks = chunkBySize(text, maxChunkSize, overlap);
}

View file

@ -259,14 +259,12 @@ function analyzeToS(tosText: string): TOSAnalysis {
if (complexWordPercentage > 20) {
recommendations.push(
'Reduce use of complex words (currently ' + complexWordPercentage.toFixed(1) + '% of text)'
`Reduce use of complex words (currently ${complexWordPercentage.toFixed(1)}% of text)`
);
}
if (problematicSections.length > 0) {
recommendations.push(
'Address ' + problematicSections.length + ' identified problematic sections'
);
recommendations.push(`Address ${problematicSections.length} identified problematic sections`);
}
if (totalWords > 5000) {

View file

@ -3,73 +3,73 @@
* This file provides metadata for the blocks validator
*/
import {
// Execution tools
executeCodeAsync,
execute,
run,
runAsync,
// Job tools
getJob,
listJobs,
deleteJob,
// Languages tools
getLanguages,
getShells,
// Session tools
createSession,
getSession,
listSessions,
executeInSession,
freezeSession,
unfreezeSession,
lockSession,
unlockSession,
createSessionSnapshot,
restoreSession,
deleteSession,
cloneSnapshot,
// Service tools
createService,
getService,
listServices,
executeInService,
freezeService,
unfreezeService,
lockService,
unlockService,
redeployService,
getServiceLogs,
createServiceSnapshot,
getServiceEnv,
setServiceEnv,
deleteServiceEnv,
deleteService,
// Session tools
createSession,
createSessionSnapshot,
// Snapshot tools
createSnapshot,
getSnapshot,
listSnapshots,
lockSnapshot,
unlockSnapshot,
restoreSnapshot,
cloneSnapshot,
deleteSnapshot,
// Image tools
publishImage,
getImage,
listImages,
lockImage,
unlockImage,
grantImageAccess,
revokeImageAccess,
transferImage,
setImageVisibility,
spawnFromImage,
getImageTrustedKeys,
deleteImage,
deleteJob,
deleteService,
deleteServiceEnv,
deleteSession,
deleteSnapshot,
execute,
// Execution tools
executeCodeAsync,
executeInService,
executeInSession,
freezeService,
freezeSession,
getClusterStatus,
getImage,
getImageTrustedKeys,
// Job tools
getJob,
// Languages tools
getLanguages,
getService,
getServiceEnv,
getServiceLogs,
getSession,
getShells,
getSnapshot,
getSystemStats,
grantImageAccess,
// System tools
healthCheck,
getClusterStatus,
getSystemStats,
listImages,
listJobs,
listPools,
listServices,
listSessions,
listSnapshots,
lockImage,
lockService,
lockSession,
lockSnapshot,
// Image tools
publishImage,
redeployService,
restoreSession,
restoreSnapshot,
revokeImageAccess,
run,
runAsync,
setImageVisibility,
setServiceEnv,
spawnFromImage,
transferImage,
unfreezeService,
unfreezeSession,
unlockImage,
unlockService,
unlockSession,
unlockSnapshot,
} from './src/index.js';
export const block = {

View file

@ -595,7 +595,7 @@ export const execute = tool({
stderr: result.stderr || '',
exit_code: result.exit_code ?? 0,
total_time_ms: result.total_time_ms || 0,
success: result.success ?? (result.exit_code === 0),
success: result.success ?? result.exit_code === 0,
artifact: result.artifact,
wasm_artifact: result.wasm_artifact,
};
@ -691,7 +691,7 @@ export const run = tool({
stdout: result.stdout || '',
stderr: result.stderr || '',
exit_code: result.exit_code ?? 0,
success: result.success ?? (result.exit_code === 0),
success: result.success ?? result.exit_code === 0,
};
},
});
@ -896,8 +896,7 @@ export interface LanguagesResult {
* List all supported programming languages.
*/
export const getLanguages = tool({
description:
'List all supported programming languages and their aliases for code execution.',
description: 'List all supported programming languages and their aliases for code execution.',
inputSchema: jsonSchema<Record<string, never>>({
type: 'object',
properties: {},
@ -923,8 +922,7 @@ export interface ShellsResult {
* List all supported shells and REPLs for interactive sessions.
*/
export const getShells = tool({
description:
'List all supported shells and REPLs for interactive sessions, grouped by category.',
description: 'List all supported shells and REPLs for interactive sessions, grouped by category.',
inputSchema: jsonSchema<Record<string, never>>({
type: 'object',
properties: {},
@ -1019,7 +1017,11 @@ export const getSession = tool({
additionalProperties: false,
}),
async execute(input: GetSessionInput): Promise<SessionResult> {
return apiRequest<SessionResult>('GET', `/sessions/${encodeURIComponent(input.session_id)}`, undefined);
return apiRequest<SessionResult>(
'GET',
`/sessions/${encodeURIComponent(input.session_id)}`,
undefined
);
},
});
@ -1103,7 +1105,8 @@ export interface SessionStateResult {
* Freeze a session to save resources.
*/
export const freezeSession = tool({
description: 'Freeze a session to save resources. Container state is preserved and can be woken later.',
description:
'Freeze a session to save resources. Container state is preserved and can be woken later.',
inputSchema: jsonSchema<SessionIdInput>({
type: 'object',
properties: {
@ -1223,7 +1226,10 @@ export const createSessionSnapshot = tool({
properties: {
session_id: { type: 'string', description: 'The session ID' },
name: { type: 'string', description: 'Optional snapshot name' },
hot: { type: 'boolean', description: 'Create hot snapshot without stopping container. Default: false' },
hot: {
type: 'boolean',
description: 'Create hot snapshot without stopping container. Default: false',
},
ttl: { type: 'number', description: 'Auto-delete after N seconds (optional)' },
},
required: ['session_id'],
@ -1329,7 +1335,10 @@ export const createService = tool({
properties: {
name: { type: 'string', description: 'Service name (becomes NAME.on.unsandbox.com)' },
bootstrap: { type: 'string', description: 'Bootstrap script content or URL' },
bootstrap_content: { type: 'string', description: 'Bootstrap script content (alternative to bootstrap)' },
bootstrap_content: {
type: 'string',
description: 'Bootstrap script content (alternative to bootstrap)',
},
ports: {
type: 'array',
items: { type: 'number' },
@ -1384,7 +1393,11 @@ export const getService = tool({
additionalProperties: false,
}),
async execute(input: ServiceIdInput): Promise<ServiceResult> {
return apiRequest<ServiceResult>('GET', `/services/${encodeURIComponent(input.service_id)}`, undefined);
return apiRequest<ServiceResult>(
'GET',
`/services/${encodeURIComponent(input.service_id)}`,
undefined
);
},
});
@ -1814,7 +1827,11 @@ export const getSnapshot = tool({
additionalProperties: false,
}),
async execute(input: SnapshotIdInput): Promise<SnapshotResult> {
return apiRequest<SnapshotResult>('GET', `/snapshots/${encodeURIComponent(input.snapshot_id)}`, undefined);
return apiRequest<SnapshotResult>(
'GET',
`/snapshots/${encodeURIComponent(input.snapshot_id)}`,
undefined
);
},
});
@ -2066,7 +2083,11 @@ export const getImage = tool({
additionalProperties: false,
}),
async execute(input: ImageIdInput): Promise<ImageResult> {
return apiRequest<ImageResult>('GET', `/images/${encodeURIComponent(input.image_id)}`, undefined);
return apiRequest<ImageResult>(
'GET',
`/images/${encodeURIComponent(input.image_id)}`,
undefined
);
},
});
@ -2165,11 +2186,9 @@ export const grantImageAccess = tool({
additionalProperties: false,
}),
async execute(input: GrantImageAccessInput): Promise<ImageResult> {
return apiRequest<ImageResult>(
'POST',
`/images/${encodeURIComponent(input.image_id)}/grant`,
{ api_key: input.api_key }
);
return apiRequest<ImageResult>('POST', `/images/${encodeURIComponent(input.image_id)}/grant`, {
api_key: input.api_key,
});
},
});
@ -2188,11 +2207,9 @@ export const revokeImageAccess = tool({
additionalProperties: false,
}),
async execute(input: GrantImageAccessInput): Promise<ImageResult> {
return apiRequest<ImageResult>(
'POST',
`/images/${encodeURIComponent(input.image_id)}/revoke`,
{ api_key: input.api_key }
);
return apiRequest<ImageResult>('POST', `/images/${encodeURIComponent(input.image_id)}/revoke`, {
api_key: input.api_key,
});
},
});
@ -2348,7 +2365,8 @@ export interface DeleteImageResult {
* Delete an image.
*/
export const deleteImage = tool({
description: 'Permanently delete an image from LXD and the database. Cannot delete locked images.',
description:
'Permanently delete an image from LXD and the database. Cannot delete locked images.',
inputSchema: jsonSchema<ImageIdInput>({
type: 'object',
properties: {

View file

@ -69,7 +69,7 @@ function normalizeUrl(
let url: URL;
try {
url = new URL(urlString);
} catch (error) {
} catch (_error) {
throw new Error(`Invalid URL: ${urlString}`);
}

View file

@ -9,12 +9,12 @@
*/
export {
cancelJob,
executeCode,
executeCodeAsync,
getJob,
listJobs,
listLanguages,
runCode,
runCodeAsync,
listJobs,
getJob,
cancelJob,
listLanguages,
} from '@thomasdavis/unsandbox';

View file

@ -86,9 +86,7 @@ export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
};
const isControlled = controlledValue !== undefined;
const [internalValue, setInternalValue] = useState<string[]>(
normalizeValue(defaultValue)
);
const [internalValue, setInternalValue] = useState<string[]>(normalizeValue(defaultValue));
const value = isControlled ? normalizeValue(controlledValue) : internalValue;
@ -138,11 +136,7 @@ export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
return (
<AccordionContext.Provider value={contextValue}>
<div
ref={ref}
className={cn(accordionVariants({ variant }), className)}
{...props}
>
<div ref={ref} className={cn(accordionVariants({ variant }), className)} {...props}>
{children}
</div>
</AccordionContext.Provider>
@ -232,7 +226,10 @@ export const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerPr
aria-expanded={isExpanded}
aria-controls={contentId}
disabled={disabled}
className={cn(accordionTriggerVariants({ disabled: disabled ? 'true' : 'false' }), className)}
className={cn(
accordionTriggerVariants({ disabled: disabled ? 'true' : 'false' }),
className
)}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
@ -266,12 +263,13 @@ export const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps
ref={ref}
role="region"
aria-hidden={!isExpanded}
className={cn(accordionContentVariants({ expanded: isExpanded ? 'true' : 'false' }), className)}
className={cn(
accordionContentVariants({ expanded: isExpanded ? 'true' : 'false' }),
className
)}
{...props}
>
<div className={accordionContentInnerVariants({})}>
{children}
</div>
<div className={accordionContentInnerVariants({})}>{children}</div>
</div>
);
}

View file

@ -4,9 +4,7 @@ import { createVariants } from '../system/variants';
* Accordion container variant definitions
*/
export const accordionVariants = createVariants({
base: [
'w-full',
].join(' '),
base: ['w-full'].join(' '),
variants: {
variant: {
@ -25,9 +23,7 @@ export const accordionVariants = createVariants({
* Accordion item variant definitions
*/
export const accordionItemVariants = createVariants({
base: [
'w-full',
].join(' '),
base: ['w-full'].join(' '),
variants: {
variant: {
@ -67,8 +63,8 @@ export const accordionTriggerVariants = createVariants({
variants: {
disabled: {
'true': 'opacity-50 cursor-not-allowed hover:bg-transparent',
'false': '',
true: 'opacity-50 cursor-not-allowed hover:bg-transparent',
false: '',
},
},
@ -81,16 +77,12 @@ export const accordionTriggerVariants = createVariants({
* Accordion trigger icon variant definitions
*/
export const accordionTriggerIconVariants = createVariants({
base: [
'flex-shrink-0',
'text-foreground-muted',
'transition-transform duration-200',
].join(' '),
base: ['flex-shrink-0', 'text-foreground-muted', 'transition-transform duration-200'].join(' '),
variants: {
expanded: {
'true': 'rotate-180',
'false': 'rotate-0',
true: 'rotate-180',
false: 'rotate-0',
},
},
@ -103,15 +95,12 @@ export const accordionTriggerIconVariants = createVariants({
* Accordion content variant definitions
*/
export const accordionContentVariants = createVariants({
base: [
'overflow-hidden',
'transition-all duration-200 ease-in-out',
].join(' '),
base: ['overflow-hidden', 'transition-all duration-200 ease-in-out'].join(' '),
variants: {
expanded: {
'true': 'max-h-[1000px] opacity-100',
'false': 'max-h-0 opacity-0',
true: 'max-h-[1000px] opacity-100',
false: 'max-h-0 opacity-0',
},
},
@ -124,11 +113,7 @@ export const accordionContentVariants = createVariants({
* Accordion content inner variant definitions
*/
export const accordionContentInnerVariants = createVariants({
base: [
'px-4 pb-4',
'font-mono text-sm',
'text-foreground-muted',
].join(' '),
base: ['px-4 pb-4', 'font-mono text-sm', 'text-foreground-muted'].join(' '),
variants: {},

View file

@ -1,13 +1,7 @@
'use client';
import { cn } from '@tpmjs/utils/cn';
import {
Children,
forwardRef,
isValidElement,
useMemo,
useState,
} from 'react';
import { Children, forwardRef, isValidElement, useMemo, useState } from 'react';
import { Icon, type IconName } from '../Icon/Icon';
import type {
BreadcrumbEllipsisProps,
@ -38,7 +32,6 @@ function getSeparatorIcon(separator: BreadcrumbSeparator): IconName {
return 'arrowRight';
case 'dot':
return 'circle';
case 'slash':
default:
return 'slash';
}
@ -103,11 +96,7 @@ export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
</BreadcrumbSeparatorComponent>
);
}
return (
<BreadcrumbSeparatorComponent key={key}>
{separator}
</BreadcrumbSeparatorComponent>
);
return <BreadcrumbSeparatorComponent key={key}>{separator}</BreadcrumbSeparatorComponent>;
};
return (
@ -129,10 +118,7 @@ export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
}
return (
<li
key={index}
className={breadcrumbItemVariants({})}
>
<li key={index} className={breadcrumbItemVariants({})}>
{index > 0 && renderSeparator(`sep-${index}`)}
{item}
</li>
@ -261,17 +247,18 @@ BreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis';
*
* Represents the current page in breadcrumbs (non-link, just text)
*/
export const BreadcrumbPage = forwardRef<HTMLSpanElement, Omit<BreadcrumbItemProps, 'href' | 'current'>>(
({ children, className, ...props }, ref) => (
<span
ref={ref}
aria-current="page"
className={cn(breadcrumbLinkVariants({ current: 'true' }), className)}
{...props}
>
{children}
</span>
)
);
export const BreadcrumbPage = forwardRef<
HTMLSpanElement,
Omit<BreadcrumbItemProps, 'href' | 'current'>
>(({ children, className, ...props }, ref) => (
<span
ref={ref}
aria-current="page"
className={cn(breadcrumbLinkVariants({ current: 'true' }), className)}
{...props}
>
{children}
</span>
));
BreadcrumbPage.displayName = 'BreadcrumbPage';

View file

@ -4,10 +4,7 @@ import { createVariants } from '../system/variants';
* Breadcrumbs container variant definitions
*/
export const breadcrumbsVariants = createVariants({
base: [
'flex items-center',
'font-mono text-sm',
].join(' '),
base: ['flex items-center', 'font-mono text-sm'].join(' '),
variants: {},
@ -18,12 +15,7 @@ export const breadcrumbsVariants = createVariants({
* Breadcrumbs list variant definitions
*/
export const breadcrumbsListVariants = createVariants({
base: [
'flex items-center gap-1',
'flex-wrap',
'list-none',
'm-0 p-0',
].join(' '),
base: ['flex items-center gap-1', 'flex-wrap', 'list-none', 'm-0 p-0'].join(' '),
variants: {},
@ -34,14 +26,12 @@ export const breadcrumbsListVariants = createVariants({
* Breadcrumb item variant definitions
*/
export const breadcrumbItemVariants = createVariants({
base: [
'flex items-center gap-1',
].join(' '),
base: ['flex items-center gap-1'].join(' '),
variants: {
current: {
'true': '',
'false': '',
true: '',
false: '',
},
},
@ -65,8 +55,8 @@ export const breadcrumbLinkVariants = createVariants({
variants: {
current: {
'true': 'text-foreground font-medium pointer-events-none',
'false': 'cursor-pointer',
true: 'text-foreground font-medium pointer-events-none',
false: 'cursor-pointer',
},
},
@ -79,12 +69,7 @@ export const breadcrumbLinkVariants = createVariants({
* Breadcrumb separator variant definitions
*/
export const breadcrumbSeparatorVariants = createVariants({
base: [
'mx-1',
'text-foreground-muted',
'select-none',
'opacity-60',
].join(' '),
base: ['mx-1', 'text-foreground-muted', 'select-none', 'opacity-60'].join(' '),
variants: {},
@ -113,11 +98,7 @@ export const breadcrumbEllipsisVariants = createVariants({
* Breadcrumb icon variant definitions
*/
export const breadcrumbIconVariants = createVariants({
base: [
'flex-shrink-0',
'w-4 h-4',
'mr-1',
].join(' '),
base: ['flex-shrink-0', 'w-4 h-4', 'mr-1'].join(' '),
variants: {},

View file

@ -16,10 +16,7 @@ export const cardVariants = createVariants({
variants: {
variant: {
default: [
'border border-dashed border-border',
'bg-card text-card-foreground',
].join(' '),
default: ['border border-dashed border-border', 'bg-card text-card-foreground'].join(' '),
elevated: [
'border border-dashed border-border',
@ -28,17 +25,13 @@ export const cardVariants = createVariants({
outline: ['border-2 border-dashed border-border', 'bg-transparent text-foreground'].join(' '),
blueprint: [
'border border-dashed border-border',
'bg-card text-card-foreground',
].join(' '),
blueprint: ['border border-dashed border-border', 'bg-card text-card-foreground'].join(' '),
ghost: ['bg-transparent text-foreground'].join(' '),
featured: [
'border-2 border-solid border-foreground',
'bg-card text-card-foreground',
].join(' '),
featured: ['border-2 border-solid border-foreground', 'bg-card text-card-foreground'].join(
' '
),
brutalist: [
'border-[6px] border-foreground',

View file

@ -4,12 +4,7 @@ import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Icon } from '../Icon/Icon';
import type {
DrawerBodyProps,
DrawerFooterProps,
DrawerHeaderProps,
DrawerProps,
} from './types';
import type { DrawerBodyProps, DrawerFooterProps, DrawerHeaderProps, DrawerProps } from './types';
import {
drawerBackdropVariants,
drawerBodyVariants,
@ -135,30 +130,27 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
);
// Handle focus trap within drawer
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key !== 'Tab') return;
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
if (event.key !== 'Tab') return;
const panel = panelRef.current;
if (!panel) return;
const panel = panelRef.current;
if (!panel) return;
const focusableElements = panel.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const focusableElements = panel.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
},
[]
);
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}, []);
if (!open) return null;
@ -175,10 +167,7 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
/>
{/* Container */}
<div
className={drawerContainerVariants({})}
onKeyDown={handleKeyDown}
>
<div className={drawerContainerVariants({})} onKeyDown={handleKeyDown}>
{/* Panel */}
<div
ref={(node) => {
@ -195,10 +184,7 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
aria-labelledby={title ? titleId : undefined}
aria-describedby={description ? descriptionId : undefined}
tabIndex={-1}
className={cn(
drawerPanelVariants({ side, size, state: 'entered' }),
className
)}
className={cn(drawerPanelVariants({ side, size, state: 'entered' }), className)}
{...props}
>
{/* Header */}
@ -230,16 +216,10 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
)}
{/* Body */}
<div className={drawerBodyVariants({})}>
{children}
</div>
<div className={drawerBodyVariants({})}>{children}</div>
{/* Footer */}
{footer && (
<div className={drawerFooterVariants({})}>
{footer}
</div>
)}
{footer && <div className={drawerFooterVariants({})}>{footer}</div>}
</div>
</div>
</>
@ -256,11 +236,7 @@ Drawer.displayName = 'Drawer';
*/
export const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(drawerHeaderVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(drawerHeaderVariants({}), className)} {...props}>
{children}
</div>
)
@ -273,11 +249,7 @@ DrawerHeader.displayName = 'DrawerHeader';
*/
export const DrawerBody = forwardRef<HTMLDivElement, DrawerBodyProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(drawerBodyVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(drawerBodyVariants({}), className)} {...props}>
{children}
</div>
)
@ -290,11 +262,7 @@ DrawerBody.displayName = 'DrawerBody';
*/
export const DrawerFooter = forwardRef<HTMLDivElement, DrawerFooterProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(drawerFooterVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(drawerFooterVariants({}), className)} {...props}>
{children}
</div>
)

View file

@ -29,11 +29,7 @@ export const drawerBackdropVariants = createVariants({
* Drawer container variant definitions
*/
export const drawerContainerVariants = createVariants({
base: [
'fixed inset-0',
'z-[var(--z-drawer)]',
'overflow-hidden',
].join(' '),
base: ['fixed inset-0', 'z-[var(--z-drawer)]', 'overflow-hidden'].join(' '),
variants: {},
@ -141,11 +137,7 @@ export const drawerHeaderVariants = createVariants({
* Drawer body variant definitions
*/
export const drawerBodyVariants = createVariants({
base: [
'flex-1',
'px-6 py-4',
'overflow-y-auto',
].join(' '),
base: ['flex-1', 'px-6 py-4', 'overflow-y-auto'].join(' '),
variants: {},
@ -173,11 +165,7 @@ export const drawerFooterVariants = createVariants({
* Drawer title variant definitions
*/
export const drawerTitleVariants = createVariants({
base: [
'font-mono font-semibold text-lg',
'text-foreground',
'lowercase',
].join(' '),
base: ['font-mono font-semibold text-lg', 'text-foreground', 'lowercase'].join(' '),
variants: {},

View file

@ -186,8 +186,8 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
if (!isOpen || !triggerRef.current || !contentRef.current) return;
const updatePosition = () => {
const triggerRect = triggerRef.current!.getBoundingClientRect();
const contentRect = contentRef.current!.getBoundingClientRect();
const triggerRect = triggerRef.current?.getBoundingClientRect();
const contentRect = contentRef.current?.getBoundingClientRect();
const newPosition = calculatePosition(triggerRect, contentRect, placement, offset);
setPosition(newPosition);
};
@ -209,10 +209,7 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (
triggerRef.current?.contains(target) ||
contentRef.current?.contains(target)
) {
if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
return;
}
closeMenu();
@ -236,15 +233,11 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
break;
case 'ArrowDown':
event.preventDefault();
setActiveIndex((prev) =>
prev < itemCountRef.current - 1 ? prev + 1 : 0
);
setActiveIndex((prev) => (prev < itemCountRef.current - 1 ? prev + 1 : 0));
break;
case 'ArrowUp':
event.preventDefault();
setActiveIndex((prev) =>
prev > 0 ? prev - 1 : itemCountRef.current - 1
);
setActiveIndex((prev) => (prev > 0 ? prev - 1 : itemCountRef.current - 1));
break;
case 'Home':
event.preventDefault();
@ -391,16 +384,16 @@ export const DropdownMenuItem = forwardRef<HTMLButtonElement, DropdownMenuItemPr
{...props}
>
{icon && (
<span className={dropdownMenuItemIconVariants({ destructive: destructive ? 'true' : 'false' })}>
<span
className={dropdownMenuItemIconVariants({
destructive: destructive ? 'true' : 'false',
})}
>
{icon}
</span>
)}
<span className="flex-1">{children}</span>
{shortcut && (
<span className={dropdownMenuItemShortcutVariants({})}>
{shortcut}
</span>
)}
{shortcut && <span className={dropdownMenuItemShortcutVariants({})}>{shortcut}</span>}
</button>
);
}
@ -429,11 +422,7 @@ DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
*/
export const DropdownMenuLabel = forwardRef<HTMLDivElement, DropdownMenuLabelProps>(
({ children, className, ...props }, ref) => (
<div
ref={ref}
className={cn(dropdownMenuLabelVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(dropdownMenuLabelVariants({}), className)} {...props}>
{children}
</div>
)

View file

@ -55,16 +55,16 @@ export const dropdownMenuItemVariants = createVariants({
variants: {
disabled: {
'true': 'opacity-50 cursor-not-allowed hover:bg-transparent',
'false': '',
true: 'opacity-50 cursor-not-allowed hover:bg-transparent',
false: '',
},
destructive: {
'true': 'text-error hover:bg-error/10 focus-visible:bg-error/10',
'false': '',
true: 'text-error hover:bg-error/10 focus-visible:bg-error/10',
false: '',
},
active: {
'true': 'bg-accent/10',
'false': '',
true: 'bg-accent/10',
false: '',
},
},
@ -79,16 +79,12 @@ export const dropdownMenuItemVariants = createVariants({
* Dropdown menu item icon variant definitions
*/
export const dropdownMenuItemIconVariants = createVariants({
base: [
'flex-shrink-0',
'w-4 h-4',
'text-foreground-muted',
].join(' '),
base: ['flex-shrink-0', 'w-4 h-4', 'text-foreground-muted'].join(' '),
variants: {
destructive: {
'true': 'text-error',
'false': '',
true: 'text-error',
false: '',
},
},
@ -101,12 +97,7 @@ export const dropdownMenuItemIconVariants = createVariants({
* Dropdown menu item shortcut variant definitions
*/
export const dropdownMenuItemShortcutVariants = createVariants({
base: [
'ml-auto',
'font-mono text-xs',
'text-foreground-muted',
'opacity-60',
].join(' '),
base: ['ml-auto', 'font-mono text-xs', 'text-foreground-muted', 'opacity-60'].join(' '),
variants: {},
@ -117,11 +108,7 @@ export const dropdownMenuItemShortcutVariants = createVariants({
* Dropdown menu separator variant definitions
*/
export const dropdownMenuSeparatorVariants = createVariants({
base: [
'my-1',
'h-px',
'bg-border',
].join(' '),
base: ['my-1', 'h-px', 'bg-border'].join(' '),
variants: {},

View file

@ -63,18 +63,11 @@ const sizeConfig = {
* ```
*/
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
(
{ className, icon, title, description, action, size = 'md', ...props },
ref
) => {
({ className, icon, title, description, action, size = 'md', ...props }, ref) => {
const config = sizeConfig[size];
return (
<div
ref={ref}
className={cn('text-center', config.container, className)}
{...props}
>
<div ref={ref} className={cn('text-center', config.container, className)} {...props}>
<div
className={cn(
'rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4',
@ -83,21 +76,9 @@ export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
>
<Icon icon={icon} size={config.iconSize} className="text-primary" />
</div>
<h3
className={cn(
'font-medium text-foreground mb-2',
config.title
)}
>
{title}
</h3>
<h3 className={cn('font-medium text-foreground mb-2', config.title)}>{title}</h3>
{description && (
<p
className={cn(
'text-foreground-secondary mb-4 max-w-md mx-auto',
config.description
)}
>
<p className={cn('text-foreground-secondary mb-4 max-w-md mx-auto', config.description)}>
{description}
</p>
)}

View file

@ -55,36 +55,14 @@ const sizeConfig = {
* ```
*/
export const ErrorState = forwardRef<HTMLDivElement, ErrorStateProps>(
(
{
className,
title = 'Error',
message,
onRetry,
size = 'md',
...props
},
ref
) => {
({ className, title = 'Error', message, onRetry, size = 'md', ...props }, ref) => {
const config = sizeConfig[size];
return (
<div
ref={ref}
className={cn('text-center', config.container, className)}
{...props}
>
<Icon
icon="alertCircle"
size={config.iconSize}
className="mx-auto text-error mb-4"
/>
<h2 className={cn('font-medium text-foreground mb-2', config.title)}>
{title}
</h2>
<p className={cn('text-foreground-secondary mb-4', config.message)}>
{message}
</p>
<div ref={ref} className={cn('text-center', config.container, className)} {...props}>
<Icon icon="alertCircle" size={config.iconSize} className="mx-auto text-error mb-4" />
<h2 className={cn('font-medium text-foreground mb-2', config.title)}>{title}</h2>
<p className={cn('text-foreground-secondary mb-4', config.message)}>{message}</p>
{onRetry && (
<Button onClick={onRetry} variant="default">
Try Again

View file

@ -1,10 +1,10 @@
import { createVariants } from '../system/variants';
import {
formErrorMessage,
formHelperBase,
formLabelBase,
formRequiredIndicator,
} from '../system/formVariants';
import { createVariants } from '../system/variants';
/**
* FormField container variants

View file

@ -7,8 +7,8 @@ import type { InstallSnippetProps, PackageManager } from './types';
import {
installSnippetCodeVariants,
installSnippetCopyButtonVariants,
installSnippetTabVariants,
installSnippetTabsVariants,
installSnippetTabVariants,
installSnippetVariants,
} from './variants';
@ -127,11 +127,7 @@ export const InstallSnippet = forwardRef<HTMLDivElement, InstallSnippetProps>(
}, [currentCommand]);
return (
<div
ref={ref}
className={cn(installSnippetVariants({ variant }), className)}
{...props}
>
<div ref={ref} className={cn(installSnippetVariants({ variant }), className)} {...props}>
{/* Tabs */}
{showTabs && (
<div className={installSnippetTabsVariants({ variant })}>
@ -159,7 +155,10 @@ export const InstallSnippet = forwardRef<HTMLDivElement, InstallSnippetProps>(
<button
type="button"
onClick={handleCopy}
className={installSnippetCopyButtonVariants({ variant, copied: copied ? 'true' : 'false' })}
className={installSnippetCopyButtonVariants({
variant,
copied: copied ? 'true' : 'false',
})}
aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
>
<Icon icon={copied ? 'check' : 'copy'} size="sm" />

View file

@ -4,12 +4,7 @@ import { createVariants } from '../system/variants';
* InstallSnippet container variant definitions
*/
export const installSnippetVariants = createVariants({
base: [
'w-full',
'border border-border',
'rounded-none',
'overflow-hidden',
].join(' '),
base: ['w-full', 'border border-border', 'rounded-none', 'overflow-hidden'].join(' '),
variants: {
variant: {
@ -28,12 +23,7 @@ export const installSnippetVariants = createVariants({
* InstallSnippet tabs variant definitions
*/
export const installSnippetTabsVariants = createVariants({
base: [
'flex items-center',
'border-b border-border',
'px-1',
'gap-0',
].join(' '),
base: ['flex items-center', 'border-b border-border', 'px-1', 'gap-0'].join(' '),
variants: {
variant: {
@ -68,8 +58,8 @@ export const installSnippetTabVariants = createVariants({
dark: 'text-background/60 hover:text-background',
},
active: {
'true': '',
'false': '',
true: '',
false: '',
},
},
@ -79,9 +69,18 @@ export const installSnippetTabVariants = createVariants({
},
compoundVariants: [
{ conditions: { variant: 'default', active: 'true' }, className: 'text-foreground border-primary' },
{ conditions: { variant: 'minimal', active: 'true' }, className: 'text-foreground border-primary' },
{ conditions: { variant: 'dark', active: 'true' }, className: 'text-background border-background' },
{
conditions: { variant: 'default', active: 'true' },
className: 'text-foreground border-primary',
},
{
conditions: { variant: 'minimal', active: 'true' },
className: 'text-foreground border-primary',
},
{
conditions: { variant: 'dark', active: 'true' },
className: 'text-background border-background',
},
],
});
@ -130,8 +129,8 @@ export const installSnippetCopyButtonVariants = createVariants({
dark: 'text-background/60 hover:text-background hover:bg-background/10',
},
copied: {
'true': '',
'false': '',
true: '',
false: '',
},
},

View file

@ -54,21 +54,13 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
return (
<div
ref={ref}
className={cn(
'flex items-center justify-center',
config.container,
config.gap,
className
)}
className={cn('flex items-center justify-center', config.container, config.gap, className)}
{...props}
>
<Spinner size={config.spinnerSize} />
{message && (
<span
className={cn(
'text-foreground-secondary font-mono tracking-wide',
config.messageClass
)}
className={cn('text-foreground-secondary font-mono tracking-wide', config.messageClass)}
>
{message}
</span>

View file

@ -4,12 +4,7 @@ import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Icon } from '../Icon/Icon';
import type {
ModalBodyProps,
ModalFooterProps,
ModalHeaderProps,
ModalProps,
} from './types';
import type { ModalBodyProps, ModalFooterProps, ModalHeaderProps, ModalProps } from './types';
import {
modalBackdropVariants,
modalBodyVariants,
@ -133,30 +128,27 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
);
// Handle focus trap within modal
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key !== 'Tab') return;
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
if (event.key !== 'Tab') return;
const panel = panelRef.current;
if (!panel) return;
const panel = panelRef.current;
if (!panel) return;
const focusableElements = panel.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const focusableElements = panel.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
},
[]
);
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}, []);
if (!open) return null;
@ -166,10 +158,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
const modalContent = (
<>
{/* Backdrop */}
<div
className={modalBackdropVariants({ state: 'entered' })}
aria-hidden="true"
/>
<div className={modalBackdropVariants({ state: 'entered' })} aria-hidden="true" />
{/* Container */}
<div
@ -193,10 +182,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
aria-labelledby={title ? titleId : undefined}
aria-describedby={description ? descriptionId : undefined}
tabIndex={-1}
className={cn(
modalPanelVariants({ size, state: 'entered' }),
className
)}
className={cn(modalPanelVariants({ size, state: 'entered' }), className)}
{...props}
>
{/* Header */}
@ -228,16 +214,10 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
)}
{/* Body */}
<div className={modalBodyVariants({})}>
{children}
</div>
<div className={modalBodyVariants({})}>{children}</div>
{/* Footer */}
{footer && (
<div className={modalFooterVariants({})}>
{footer}
</div>
)}
{footer && <div className={modalFooterVariants({})}>{footer}</div>}
</div>
</div>
</>
@ -254,11 +234,7 @@ Modal.displayName = 'Modal';
*/
export const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(modalHeaderVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(modalHeaderVariants({}), className)} {...props}>
{children}
</div>
)
@ -271,11 +247,7 @@ ModalHeader.displayName = 'ModalHeader';
*/
export const ModalBody = forwardRef<HTMLDivElement, ModalBodyProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(modalBodyVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(modalBodyVariants({}), className)} {...props}>
{children}
</div>
)
@ -288,11 +260,7 @@ ModalBody.displayName = 'ModalBody';
*/
export const ModalFooter = forwardRef<HTMLDivElement, ModalFooterProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(modalFooterVariants({}), className)}
{...props}
>
<div ref={ref} className={cn(modalFooterVariants({}), className)} {...props}>
{children}
</div>
)

View file

@ -101,11 +101,7 @@ export const modalHeaderVariants = createVariants({
* Modal body variant definitions
*/
export const modalBodyVariants = createVariants({
base: [
'flex-1',
'px-6 py-4',
'overflow-y-auto',
].join(' '),
base: ['flex-1', 'px-6 py-4', 'overflow-y-auto'].join(' '),
variants: {},
@ -132,11 +128,7 @@ export const modalFooterVariants = createVariants({
* Modal title variant definitions
*/
export const modalTitleVariants = createVariants({
base: [
'font-mono font-semibold text-lg',
'text-foreground',
'lowercase',
].join(' '),
base: ['font-mono font-semibold text-lg', 'text-foreground', 'lowercase'].join(' '),
variants: {},

View file

@ -52,28 +52,16 @@ const sizeConfig = {
* ```
*/
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
(
{ className, title, description, actions, size = 'md', ...props },
ref
) => {
({ className, title, description, actions, size = 'md', ...props }, ref) => {
const config = sizeConfig[size];
return (
<div ref={ref} className={cn(config.container, className)} {...props}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="space-y-1">
<h1 className={cn('font-bold text-foreground', config.title)}>
{title}
</h1>
<h1 className={cn('font-bold text-foreground', config.title)}>{title}</h1>
{description && (
<p
className={cn(
'text-foreground-secondary',
config.description
)}
>
{description}
</p>
<p className={cn('text-foreground-secondary', config.description)}>{description}</p>
)}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}

Some files were not shown because too many files have changed in this diff Show more