From 29e1b9d9ac101a40428d4de0839fab24b4c0f7bb Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Sun, 28 Dec 2025 23:41:15 +1000 Subject: [PATCH] feat: add interactive tutorial slideshow app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create apps/tutorial - a beautiful, animated Next.js slideshow that explains TPMJS concepts from basic to advanced: Slides: - Welcome: Gradient TPMJS title with animated floating orbs - Problem: Chaotic floating icons showing fragmented AI tools - Solution: Icons organizing into a neat grid - How It Works: Flow diagram (npm → Discovery → Registry → AI Agent) - Discovery: Animated counters showing real-time sync stats - Integration: Typewriter code animation showing SDK usage - Quality Scoring: Animated gauge and progress bars - Tool Detail: Mock tool card with all metadata - Get Started: CTA buttons linking to tpmjs.com Features: - Framer Motion animations with spring physics - Keyboard navigation (arrows, space, enter) - Mouse navigation (arrows, progress dots) - Touch-friendly swipe support - Dark theme with cyan/purple gradient accents - Responsive design --- apps/tutorial/next-env.d.ts | 6 + apps/tutorial/next.config.ts | 8 ++ apps/tutorial/package.json | 35 +++++ apps/tutorial/postcss.config.js | 6 + apps/tutorial/src/app/globals.css | 133 +++++++++++++++++ apps/tutorial/src/app/layout.tsx | 38 +++++ apps/tutorial/src/app/page.tsx | 11 ++ apps/tutorial/src/components/Slide.tsx | 46 ++++++ .../src/components/SlideNavigation.tsx | 69 +++++++++ .../tutorial/src/components/SlideProgress.tsx | 46 ++++++ apps/tutorial/src/components/Slideshow.tsx | 73 ++++++++++ .../src/components/slides/DiscoverySlide.tsx | 114 +++++++++++++++ .../src/components/slides/GetStartedSlide.tsx | 128 +++++++++++++++++ .../src/components/slides/HowItWorksSlide.tsx | 90 ++++++++++++ .../components/slides/IntegrationSlide.tsx | 130 +++++++++++++++++ .../src/components/slides/ProblemSlide.tsx | 105 ++++++++++++++ .../src/components/slides/QualitySlide.tsx | 135 ++++++++++++++++++ .../src/components/slides/SolutionSlide.tsx | 86 +++++++++++ .../src/components/slides/ToolDetailSlide.tsx | 109 ++++++++++++++ .../src/components/slides/WelcomeSlide.tsx | 95 ++++++++++++ apps/tutorial/src/hooks/useSlideshow.ts | 129 +++++++++++++++++ apps/tutorial/tailwind.config.ts | 11 ++ apps/tutorial/tsconfig.json | 12 ++ pnpm-lock.yaml | 101 ++++++++++++- 24 files changed, 1712 insertions(+), 4 deletions(-) create mode 100644 apps/tutorial/next-env.d.ts create mode 100644 apps/tutorial/next.config.ts create mode 100644 apps/tutorial/package.json create mode 100644 apps/tutorial/postcss.config.js create mode 100644 apps/tutorial/src/app/globals.css create mode 100644 apps/tutorial/src/app/layout.tsx create mode 100644 apps/tutorial/src/app/page.tsx create mode 100644 apps/tutorial/src/components/Slide.tsx create mode 100644 apps/tutorial/src/components/SlideNavigation.tsx create mode 100644 apps/tutorial/src/components/SlideProgress.tsx create mode 100644 apps/tutorial/src/components/Slideshow.tsx create mode 100644 apps/tutorial/src/components/slides/DiscoverySlide.tsx create mode 100644 apps/tutorial/src/components/slides/GetStartedSlide.tsx create mode 100644 apps/tutorial/src/components/slides/HowItWorksSlide.tsx create mode 100644 apps/tutorial/src/components/slides/IntegrationSlide.tsx create mode 100644 apps/tutorial/src/components/slides/ProblemSlide.tsx create mode 100644 apps/tutorial/src/components/slides/QualitySlide.tsx create mode 100644 apps/tutorial/src/components/slides/SolutionSlide.tsx create mode 100644 apps/tutorial/src/components/slides/ToolDetailSlide.tsx create mode 100644 apps/tutorial/src/components/slides/WelcomeSlide.tsx create mode 100644 apps/tutorial/src/hooks/useSlideshow.ts create mode 100644 apps/tutorial/tailwind.config.ts create mode 100644 apps/tutorial/tsconfig.json diff --git a/apps/tutorial/next-env.d.ts b/apps/tutorial/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/apps/tutorial/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/tutorial/next.config.ts b/apps/tutorial/next.config.ts new file mode 100644 index 0000000..b5fe066 --- /dev/null +++ b/apps/tutorial/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + transpilePackages: ['@tpmjs/ui', '@tpmjs/utils'], + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/apps/tutorial/package.json b/apps/tutorial/package.json new file mode 100644 index 0000000..6e0b386 --- /dev/null +++ b/apps/tutorial/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tpmjs/tutorial", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start", + "lint": "eslint .", + "type-check": "tsc --noEmit", + "clean": "rm -rf .next .turbo" + }, + "dependencies": { + "@tpmjs/ui": "workspace:*", + "@tpmjs/utils": "workspace:*", + "framer-motion": "^11.15.0", + "next": "^16.0.8", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tpmjs/eslint-config": "workspace:*", + "@tpmjs/tailwind-config": "workspace:*", + "@tpmjs/tsconfig": "workspace:*", + "@types/node": "^22.10.2", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", + "eslint-config-next": "^16.0.4", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.9.3" + } +} diff --git a/apps/tutorial/postcss.config.js b/apps/tutorial/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/apps/tutorial/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/tutorial/src/app/globals.css b/apps/tutorial/src/app/globals.css new file mode 100644 index 0000000..0501fbe --- /dev/null +++ b/apps/tutorial/src/app/globals.css @@ -0,0 +1,133 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Dark mode by default for dramatic slideshow */ + --background: 210 10% 5%; + --surface: 210 10% 8%; + --surface-elevated: 210 10% 12%; + --foreground: 210 10% 90%; + --foreground-secondary: 210 8% 60%; + --foreground-tertiary: 210 6% 40%; + --foreground-muted: 210 6% 30%; + --border: 210 10% 20%; + --border-strong: 210 12% 30%; + --border-subtle: 210 8% 15%; + + /* Accent colors - cyan and purple */ + --accent-cyan: 186 100% 50%; + --accent-purple: 260 80% 60%; + --accent-gradient: linear-gradient(135deg, hsl(186 100% 50%) 0%, hsl(260 80% 60%) 100%); + + --primary: 210 10% 98%; + --primary-foreground: 210 10% 5%; + + /* Radii and shadows */ + --radius: 8px; + --radius-lg: 12px; + } + + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + } + + html, body, #__next { + height: 100%; + } +} + +@layer utilities { + .text-gradient { + background: var(--accent-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .bg-gradient-accent { + background: var(--accent-gradient); + } + + .glow-cyan { + box-shadow: 0 0 20px hsl(var(--accent-cyan) / 0.3), + 0 0 40px hsl(var(--accent-cyan) / 0.2), + 0 0 60px hsl(var(--accent-cyan) / 0.1); + } + + .glow-purple { + box-shadow: 0 0 20px hsl(var(--accent-purple) / 0.3), + 0 0 40px hsl(var(--accent-purple) / 0.2), + 0 0 60px hsl(var(--accent-purple) / 0.1); + } + + /* Animated background gradient */ + @keyframes gradient-shift { + 0%, 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + } + + .animate-gradient { + background-size: 200% 200%; + animation: gradient-shift 15s ease infinite; + } + + /* Floating animation */ + @keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } + } + + .animate-float { + animation: float 3s ease-in-out infinite; + } + + /* Pulse glow */ + @keyframes pulse-glow { + 0%, 100% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + } + + .animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; + } + + /* Stagger delays */ + .delay-100 { animation-delay: 100ms; } + .delay-200 { animation-delay: 200ms; } + .delay-300 { animation-delay: 300ms; } + .delay-400 { animation-delay: 400ms; } + .delay-500 { animation-delay: 500ms; } + + /* Motion Preferences */ + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } +} diff --git a/apps/tutorial/src/app/layout.tsx b/apps/tutorial/src/app/layout.tsx new file mode 100644 index 0000000..7202a0f --- /dev/null +++ b/apps/tutorial/src/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import { Space_Grotesk, Space_Mono } from 'next/font/google'; +import './globals.css'; + +const spaceGrotesk = Space_Grotesk({ + subsets: ['latin'], + variable: '--font-sans', + display: 'swap', +}); + +const spaceMono = Space_Mono({ + subsets: ['latin'], + weight: ['400', '700'], + variable: '--font-mono', + display: 'swap', +}); + +export const metadata: Metadata = { + title: 'TPMJS Tutorial - Learn How It Works', + description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents', + openGraph: { + title: 'TPMJS Tutorial', + description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents', + type: 'website', + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}): React.ReactElement { + return ( + + {children} + + ); +} diff --git a/apps/tutorial/src/app/page.tsx b/apps/tutorial/src/app/page.tsx new file mode 100644 index 0000000..4792ff8 --- /dev/null +++ b/apps/tutorial/src/app/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { Slideshow } from '@/components/Slideshow'; + +export default function TutorialPage(): React.ReactElement { + return ( +
+ +
+ ); +} diff --git a/apps/tutorial/src/components/Slide.tsx b/apps/tutorial/src/components/Slide.tsx new file mode 100644 index 0000000..d9842bf --- /dev/null +++ b/apps/tutorial/src/components/Slide.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { type Variants, motion } from 'framer-motion'; +import type { ReactNode } from 'react'; + +interface SlideProps { + children: ReactNode; + isActive: boolean; + direction: 'left' | 'right' | null; +} + +const variants: Variants = { + enter: (direction: 'left' | 'right') => ({ + x: direction === 'right' ? '100%' : '-100%', + opacity: 0, + }), + center: { + x: 0, + opacity: 1, + }, + exit: (direction: 'left' | 'right') => ({ + x: direction === 'right' ? '-100%' : '100%', + opacity: 0, + }), +}; + +export function Slide({ children, isActive, direction }: SlideProps): React.ReactElement | null { + if (!isActive) return null; + + return ( + + {children} + + ); +} diff --git a/apps/tutorial/src/components/SlideNavigation.tsx b/apps/tutorial/src/components/SlideNavigation.tsx new file mode 100644 index 0000000..1f5b9bb --- /dev/null +++ b/apps/tutorial/src/components/SlideNavigation.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { motion } from 'framer-motion'; + +interface SlideNavigationProps { + onNext: () => void; + onPrev: () => void; + isFirst: boolean; + isLast: boolean; +} + +export function SlideNavigation({ + onNext, + onPrev, + isFirst, + isLast, +}: SlideNavigationProps): React.ReactElement { + return ( + <> + {/* Previous button */} + + + + + + + {/* Next button */} + + + + + + + ); +} diff --git a/apps/tutorial/src/components/SlideProgress.tsx b/apps/tutorial/src/components/SlideProgress.tsx new file mode 100644 index 0000000..62c3401 --- /dev/null +++ b/apps/tutorial/src/components/SlideProgress.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { motion } from 'framer-motion'; + +interface SlideProgressProps { + currentSlide: number; + totalSlides: number; + goTo: (index: number) => void; +} + +export function SlideProgress({ + currentSlide, + totalSlides, + goTo, +}: SlideProgressProps): React.ReactElement { + return ( +
+ {Array.from({ length: totalSlides }).map((_, index) => ( + + ))} +
+ ); +} diff --git a/apps/tutorial/src/components/Slideshow.tsx b/apps/tutorial/src/components/Slideshow.tsx new file mode 100644 index 0000000..99691a2 --- /dev/null +++ b/apps/tutorial/src/components/Slideshow.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { useSlideshow } from '@/hooks/useSlideshow'; +import { AnimatePresence } from 'framer-motion'; +import { Slide } from './Slide'; +import { SlideNavigation } from './SlideNavigation'; +import { SlideProgress } from './SlideProgress'; +import { DiscoverySlide } from './slides/DiscoverySlide'; +import { GetStartedSlide } from './slides/GetStartedSlide'; +import { HowItWorksSlide } from './slides/HowItWorksSlide'; +import { IntegrationSlide } from './slides/IntegrationSlide'; +import { ProblemSlide } from './slides/ProblemSlide'; +import { QualitySlide } from './slides/QualitySlide'; +import { SolutionSlide } from './slides/SolutionSlide'; +import { ToolDetailSlide } from './slides/ToolDetailSlide'; +import { WelcomeSlide } from './slides/WelcomeSlide'; + +const SLIDES = [ + WelcomeSlide, + ProblemSlide, + SolutionSlide, + HowItWorksSlide, + DiscoverySlide, + IntegrationSlide, + QualitySlide, + ToolDetailSlide, + GetStartedSlide, +]; + +export function Slideshow(): React.ReactElement { + const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({ + totalSlides: SLIDES.length, + }); + + return ( +
+ {/* Background gradient */} +
+ + {/* Subtle grid pattern */} +
+ + {/* Slides */} + + {SLIDES.map((SlideContent, index) => ( + + + + ))} + + + {/* Navigation */} + + + {/* Progress */} + + + {/* Keyboard hint */} +
+ Use arrow keys or click to navigate +
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/DiscoverySlide.tsx b/apps/tutorial/src/components/slides/DiscoverySlide.tsx new file mode 100644 index 0000000..976d814 --- /dev/null +++ b/apps/tutorial/src/components/slides/DiscoverySlide.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { animate, motion, useMotionValue, useTransform } from 'framer-motion'; +import { useEffect, useState } from 'react'; + +function AnimatedCounter({ value, delay = 0 }: { value: number; delay?: number }) { + const count = useMotionValue(0); + const rounded = useTransform(count, (latest) => Math.round(latest)); + const [displayValue, setDisplayValue] = useState(0); + + useEffect(() => { + const timeout = setTimeout(() => { + const controls = animate(count, value, { + duration: 2, + ease: 'easeOut', + }); + return () => controls.stop(); + }, delay * 1000); + + return () => clearTimeout(timeout); + }, [count, value, delay]); + + useEffect(() => { + const unsubscribe = rounded.on('change', (v) => setDisplayValue(v)); + return unsubscribe; + }, [rounded]); + + return {displayValue}; +} + +export function DiscoverySlide(): React.ReactElement { + return ( +
+ {/* Background pulse */} + + + + + 🔍 + + +

Automatic Discovery

+

+ npm changes feed synced every{' '} + 2 minutes +

+ + {/* Stats grid */} +
+ +
+ + +
+
Tools Registered
+
+ + +
2m
+
Sync Interval
+
+ + +
24/7
+
Always Running
+
+
+ + {/* Animated sync indicator */} + + + Live sync active + + +
+ ); +} diff --git a/apps/tutorial/src/components/slides/GetStartedSlide.tsx b/apps/tutorial/src/components/slides/GetStartedSlide.tsx new file mode 100644 index 0000000..a1a4b2b --- /dev/null +++ b/apps/tutorial/src/components/slides/GetStartedSlide.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const links = [ + { + label: 'Browse Tools', + href: 'https://tpmjs.com/tool-search', + description: 'Explore the registry', + icon: '🔍', + gradient: 'from-cyan-500 to-blue-500', + }, + { + label: 'Publish Your Tool', + href: 'https://tpmjs.com/publish', + description: 'Share with the community', + icon: '📦', + gradient: 'from-purple-500 to-pink-500', + }, + { + label: 'Read the Docs', + href: 'https://tpmjs.com/docs', + description: 'Learn everything', + icon: '📚', + gradient: 'from-emerald-500 to-teal-500', + }, +]; + +export function GetStartedSlide(): React.ReactElement { + return ( +
+ {/* Celebratory background */} +
+ +
+ + + + 🚀 + + +

Get Started

+

Join the future of AI tooling

+ + {/* CTA buttons */} +
+ {links.map((link, index) => ( + +
+ {link.icon} +
+
{link.label}
+
{link.description}
+
+
+ + {/* Hover arrow */} + + + + + +
+ ))} +
+ + {/* Footer */} + + Made with ❤️ for the AI community + +
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/HowItWorksSlide.tsx b/apps/tutorial/src/components/slides/HowItWorksSlide.tsx new file mode 100644 index 0000000..5041aab --- /dev/null +++ b/apps/tutorial/src/components/slides/HowItWorksSlide.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const steps = [ + { icon: '📦', label: 'npm publish', description: 'Publish your tool to npm' }, + { icon: '🔍', label: 'Discovery', description: 'Automatic detection' }, + { icon: '📋', label: 'Registry', description: 'Added to TPMJS' }, + { icon: '🤖', label: 'AI Agent', description: 'Ready to use' }, +]; + +export function HowItWorksSlide(): React.ReactElement { + return ( +
+ {/* Background gradient */} +
+ + +

How It Works

+

From npm to AI in 4 simple steps

+ + {/* Flow diagram */} +
+ {steps.map((step, index) => ( +
+ {/* Step card */} + + + {step.icon} + + {step.label} + {step.description} + + + {/* Arrow */} + {index < steps.length - 1 && ( + +
+ + + + + )} +
+ ))} +
+ + {/* Animated highlight line */} + + +
+ ); +} diff --git a/apps/tutorial/src/components/slides/IntegrationSlide.tsx b/apps/tutorial/src/components/slides/IntegrationSlide.tsx new file mode 100644 index 0000000..a876f59 --- /dev/null +++ b/apps/tutorial/src/components/slides/IntegrationSlide.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { useEffect, useState } from 'react'; + +const codeLines = [ + { text: "import { tool } from '@tpmjs/sdk';", delay: 0.5 }, + { text: '', delay: 0.8 }, + { text: '// Get any tool from the registry', delay: 1.0 }, + { text: "const webSearch = await tool('web-search');", delay: 1.3 }, + { text: '', delay: 1.6 }, + { text: '// Use it with your AI agent', delay: 1.8 }, + { text: 'const result = await webSearch.execute({', delay: 2.1 }, + { text: " query: 'latest AI news'", delay: 2.4 }, + { text: '});', delay: 2.7 }, +]; + +function TypewriterLine({ text, delay }: { text: string; delay: number }) { + const [displayText, setDisplayText] = useState(''); + + useEffect(() => { + if (!text) { + setDisplayText(''); + return; + } + + const timeout = setTimeout(() => { + let i = 0; + const interval = setInterval(() => { + if (i <= text.length) { + setDisplayText(text.slice(0, i)); + i++; + } else { + clearInterval(interval); + } + }, 30); + + return () => clearInterval(interval); + }, delay * 1000); + + return () => clearTimeout(timeout); + }, [text, delay]); + + // Syntax highlighting + const highlightedText = displayText + .replace(/(import|from|const|await)/g, '$1') + .replace(/('.*?')/g, '$1') + .replace(/(\/\/.*)/g, '$1') + .replace(/(\{|\}|\(|\))/g, '$1'); + + return ( +
+ ); +} + +export function IntegrationSlide(): React.ReactElement { + return ( +
+ {/* Background */} +
+ + + + 🔌 + + +

Simple Integration

+

Three lines of code. That's it.

+ + {/* Code block */} + + {/* Window chrome */} +
+
+
+
+ agent.ts +
+ + {/* Code content */} +
+ {codeLines.map((line, index) => ( + + ))} + +
+ + + {/* Framework badges */} + + {['Vercel AI SDK', 'LangChain', 'Claude', 'OpenAI'].map((framework) => ( + + Works with {framework} + + ))} + + +
+ ); +} diff --git a/apps/tutorial/src/components/slides/ProblemSlide.tsx b/apps/tutorial/src/components/slides/ProblemSlide.tsx new file mode 100644 index 0000000..3998d54 --- /dev/null +++ b/apps/tutorial/src/components/slides/ProblemSlide.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const floatingIcons = [ + { icon: '🔧', x: '15%', y: '20%', delay: 0 }, + { icon: '⚙️', x: '75%', y: '15%', delay: 0.2 }, + { icon: '🔌', x: '85%', y: '60%', delay: 0.4 }, + { icon: '📦', x: '10%', y: '70%', delay: 0.6 }, + { icon: '🛠️', x: '60%', y: '80%', delay: 0.8 }, + { icon: '🔗', x: '25%', y: '45%', delay: 1 }, + { icon: '📡', x: '80%', y: '35%', delay: 1.2 }, + { icon: '💾', x: '40%', y: '25%', delay: 1.4 }, +]; + +export function ProblemSlide(): React.ReactElement { + return ( +
+ {/* Floating chaotic icons */} + {floatingIcons.map((item, index) => ( + + {item.icon} + + ))} + + {/* Red warning gradient */} +
+ + {/* Content */} + + + 😵 + + +

The Problem

+ +

+ AI tools are everywhere. +
+ Finding and trusting them is hard. +

+ + + {['Scattered', 'Inconsistent', 'Unverified', 'Fragmented'].map((word, i) => ( + + {word} + + ))} + +
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/QualitySlide.tsx b/apps/tutorial/src/components/slides/QualitySlide.tsx new file mode 100644 index 0000000..3efb9f2 --- /dev/null +++ b/apps/tutorial/src/components/slides/QualitySlide.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { animate, motion, useMotionValue } from 'framer-motion'; +import { useEffect, useState } from 'react'; + +function AnimatedGauge({ value, delay = 0 }: { value: number; delay?: number }) { + const progress = useMotionValue(0); + const [displayValue, setDisplayValue] = useState(0); + + useEffect(() => { + const timeout = setTimeout(() => { + animate(progress, value, { + duration: 1.5, + ease: 'easeOut', + }); + }, delay * 1000); + + return () => clearTimeout(timeout); + }, [progress, value, delay]); + + useEffect(() => { + const unsubscribe = progress.on('change', (v) => setDisplayValue(Math.round(v))); + return unsubscribe; + }, [progress]); + + const circumference = 2 * Math.PI * 45; + const strokeDashoffset = circumference - (displayValue / 100) * circumference; + + return ( +
+ + {/* Background circle */} + + {/* Progress circle */} + + + + + + + + +
+ {displayValue}% +
+
+ ); +} + +const qualityFactors = [ + { name: 'Schema Completeness', icon: '📋', score: 95, color: 'cyan' }, + { name: 'npm Downloads', icon: '📈', score: 78, color: 'purple' }, + { name: 'Documentation', icon: '📚', score: 88, color: 'emerald' }, + { name: 'Type Safety', icon: '🛡️', score: 100, color: 'yellow' }, +]; + +export function QualitySlide(): React.ReactElement { + return ( +
+ {/* Background */} +
+ + + + ⭐ + + +

Quality Scoring

+

Every tool is scored for reliability

+ + {/* Main gauge */} + + + + + {/* Quality factors */} +
+ {qualityFactors.map((factor, index) => ( + +
{factor.icon}
+
{factor.name}
+
+ +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/SolutionSlide.tsx b/apps/tutorial/src/components/slides/SolutionSlide.tsx new file mode 100644 index 0000000..2a362e3 --- /dev/null +++ b/apps/tutorial/src/components/slides/SolutionSlide.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const gridItems = Array.from({ length: 12 }, (_, i) => ({ + icon: ['🔧', '⚙️', '📦', '🔌', '🛠️', '📡', '💾', '🔗', '🎯', '⚡', '🌐', '🔐'][i], + delay: i * 0.05, +})); + +export function SolutionSlide(): React.ReactElement { + return ( +
+ {/* Green success gradient */} +
+ + {/* Content */} + + + ✨ + + +

The Solution

+ +

+ One registry. +
+ Curated tools. Quality guaranteed. +

+ + {/* Organized grid of icons */} + + {gridItems.map((item, index) => ( + + {item.icon} + + ))} + + + + {['Organized', 'Consistent', 'Verified', 'Unified'].map((word, i) => ( + + {word} + + ))} + +
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/ToolDetailSlide.tsx b/apps/tutorial/src/components/slides/ToolDetailSlide.tsx new file mode 100644 index 0000000..85a48d1 --- /dev/null +++ b/apps/tutorial/src/components/slides/ToolDetailSlide.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { motion } from 'framer-motion'; + +export function ToolDetailSlide(): React.ReactElement { + return ( +
+ {/* Background */} +
+ + +

Rich Tool Data

+

Everything you need to know about each tool

+ + {/* Mock tool card */} + + {/* Header */} +
+
+
+ + 🔍 + +
+

webSearch

+ @tpmjs/web-tools +
+
+
+ + Verified + +
+ + {/* Description */} + + Search the web and return structured results. Supports query filtering, result limits, + and domain restrictions. + + + {/* Metadata grid */} + +
+
Downloads
+
12.4k
+
+
+
Quality
+
94%
+
+
+
Category
+
Search
+
+
+ + {/* Schema preview */} + +
// Input Schema
+
+ query: string +
+
+ limit?: number +
+
+ domains?: string[] +
+
+
+
+
+ ); +} diff --git a/apps/tutorial/src/components/slides/WelcomeSlide.tsx b/apps/tutorial/src/components/slides/WelcomeSlide.tsx new file mode 100644 index 0000000..a0ea09b --- /dev/null +++ b/apps/tutorial/src/components/slides/WelcomeSlide.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { motion } from 'framer-motion'; + +export function WelcomeSlide(): React.ReactElement { + return ( +
+ {/* Animated background orbs */} +
+ + +
+ + {/* Logo / Title */} + +

+ + TPMJS + +

+
+ + {/* Tagline */} + + Tool Package Manager for AI Agents + + + {/* Decorative line */} + + + {/* CTA hint */} + + + + + + + Press any key to begin + +
+ ); +} diff --git a/apps/tutorial/src/hooks/useSlideshow.ts b/apps/tutorial/src/hooks/useSlideshow.ts new file mode 100644 index 0000000..7ca121b --- /dev/null +++ b/apps/tutorial/src/hooks/useSlideshow.ts @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useState } from 'react'; + +export interface UseSlideshowOptions { + totalSlides: number; + autoPlay?: boolean; + autoPlayInterval?: number; + loop?: boolean; +} + +export interface UseSlideshowReturn { + currentSlide: number; + totalSlides: number; + next: () => void; + prev: () => void; + goTo: (index: number) => void; + progress: number; + direction: 'left' | 'right' | null; + isFirst: boolean; + isLast: boolean; + isAutoPlaying: boolean; + toggleAutoPlay: () => void; +} + +export function useSlideshow({ + totalSlides, + autoPlay = false, + autoPlayInterval = 5000, + loop = false, +}: UseSlideshowOptions): UseSlideshowReturn { + const [currentSlide, setCurrentSlide] = useState(0); + const [direction, setDirection] = useState<'left' | 'right' | null>(null); + const [isAutoPlaying, setIsAutoPlaying] = useState(autoPlay); + + const isFirst = currentSlide === 0; + const isLast = currentSlide === totalSlides - 1; + const progress = totalSlides > 1 ? currentSlide / (totalSlides - 1) : 0; + + const next = useCallback(() => { + if (isLast && !loop) return; + setDirection('right'); + setCurrentSlide((prev) => (prev + 1) % totalSlides); + }, [isLast, loop, totalSlides]); + + const prev = useCallback(() => { + if (isFirst && !loop) return; + setDirection('left'); + setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides); + }, [isFirst, loop, totalSlides]); + + const goTo = useCallback( + (index: number) => { + if (index < 0 || index >= totalSlides) return; + setDirection(index > currentSlide ? 'right' : 'left'); + setCurrentSlide(index); + }, + [currentSlide, totalSlides] + ); + + const toggleAutoPlay = useCallback(() => { + setIsAutoPlaying((prev) => !prev); + }, []); + + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Pause auto-play on any key press + if (isAutoPlaying) { + setIsAutoPlaying(false); + } + + switch (e.key) { + case 'ArrowRight': + case ' ': + case 'Enter': + e.preventDefault(); + next(); + break; + case 'ArrowLeft': + e.preventDefault(); + prev(); + break; + case 'Home': + e.preventDefault(); + goTo(0); + break; + case 'End': + e.preventDefault(); + goTo(totalSlides - 1); + break; + case 'Escape': + e.preventDefault(); + setIsAutoPlaying(false); + break; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [next, prev, goTo, totalSlides, isAutoPlaying]); + + // Auto-play + useEffect(() => { + if (!isAutoPlaying) return; + + const timer = setInterval(() => { + if (isLast && !loop) { + setIsAutoPlaying(false); + return; + } + next(); + }, autoPlayInterval); + + return () => clearInterval(timer); + }, [isAutoPlaying, autoPlayInterval, next, isLast, loop]); + + return { + currentSlide, + totalSlides, + next, + prev, + goTo, + progress, + direction, + isFirst, + isLast, + isAutoPlaying, + toggleAutoPlay, + }; +} diff --git a/apps/tutorial/tailwind.config.ts b/apps/tutorial/tailwind.config.ts new file mode 100644 index 0000000..a4aea36 --- /dev/null +++ b/apps/tutorial/tailwind.config.ts @@ -0,0 +1,11 @@ +import baseConfig from '@tpmjs/tailwind-config/base'; +import type { Config } from 'tailwindcss'; + +export default { + ...baseConfig, + content: [ + './src/app/**/*.{ts,tsx}', + './src/components/**/*.{ts,tsx}', + '../../packages/ui/src/**/*.ts', + ], +} satisfies Config; diff --git a/apps/tutorial/tsconfig.json b/apps/tutorial/tsconfig.json new file mode 100644 index 0000000..cff0c6e --- /dev/null +++ b/apps/tutorial/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tpmjs/tsconfig/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "~/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d54513a..791c130 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,10 +134,10 @@ importers: version: 10.4.22(postcss@8.5.6) eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-next: specifier: ^16.0.4 - version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8.5.1 version: 8.5.6 @@ -157,6 +157,64 @@ importers: specifier: ^4.18.2 version: 4.22.1 + apps/tutorial: + dependencies: + '@tpmjs/ui': + specifier: workspace:* + version: link:../../packages/ui + '@tpmjs/utils': + specifier: workspace:* + version: link:../../packages/utils + framer-motion: + specifier: ^11.15.0 + version: 11.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: + specifier: ^16.0.8 + version: 16.0.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: + specifier: ^19.0.0 + version: 19.2.0 + react-dom: + specifier: ^19.0.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@tpmjs/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint + '@tpmjs/tailwind-config': + specifier: workspace:* + version: link:../../packages/config/tailwind + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + '@types/react': + specifier: ^19.0.2 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.0.2 + version: 19.2.3(@types/react@19.2.7) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.22(postcss@8.5.6) + eslint: + specifier: ^9.39.1 + version: 9.39.1(jiti@2.6.1) + eslint-config-next: + specifier: ^16.0.4 + version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + postcss: + specifier: ^8.5.1 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.18(tsx@4.21.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + apps/web: dependencies: '@ai-sdk/openai': @@ -267,10 +325,10 @@ importers: version: 17.2.3 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-next: specifier: ^16.0.4 - version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8.5.1 version: 8.5.6 @@ -4010,6 +4068,20 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@11.18.2: + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -4952,6 +5024,12 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -10131,6 +10209,15 @@ snapshots: fraction.js@5.3.4: {} + framer-motion@11.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + fresh@0.5.2: {} fs-extra@7.0.1: @@ -11359,6 +11446,12 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 + + motion-utils@11.18.1: {} + mri@1.2.0: {} ms@2.0.0: {}