tpmjs/packages/ui/src/ActivityStream/ActivityStream.tsx
Ajax Davis 5e92ae49a3 feat: implement revolutionary dithered landing page with Bayer matrix animation
Core Dithering System:
- Add Bayer matrix 8×8/4×4 ordered dithering algorithm
- Implement DitherEngine with canvas-based text rendering
- Create reveal (one-shot) and pulse (continuous) animation modes
- Add SSR safety checks for Next.js server-side rendering

New UI Components:
- DitherCanvas: Base component with RAF-based animation
- DitherHeadline: Multi-line staggered reveal for hero text
- DitherSectionHeader: Pulsing section headers
- FlowDiagram: Animated SVG showing agent→registry→tools flow
- ActivityStream: Live ticker with mock tool activity

Accessibility Features:
- useReducedMotion hook for prefers-reduced-motion support
- Screen reader compatibility with aria-labels
- Fallback to static rendering when animations disabled

Homepage Redesign:
- Replace generic sections with narrative storytelling
- Add ProblemSection: Fragmented chaos design (before tpmjs)
- Add VisionSection: Dynamic ecosystem visualization (after tpmjs)
- Redesign EcosystemStats with dithered counters and live activity
- Add DeveloperStories: Code-first testimonials
- Update HeroSection with dithered headline and blueprint scanline

Visual Design:
- Minimalist + Blueprint technical aesthetic
- Animated scanline background effect
- Grid patterns with subtle opacity
- Brutalist typography and hard edges

Technical Implementation:
- Export new components in UI package.json
- Add components to tsup.config.ts entry points
- SSR-safe canvas operations with environment checks
- Performance optimizations: frame caching, mobile detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:44:32 +10:00

126 lines
3.4 KiB
TypeScript

/**
* ActivityStream Component
*
* Live ticker showing recent tool activity (invocations, publications, updates).
* Uses mock data to simulate a live ecosystem.
*/
'use client';
import { useEffect, useState } from 'react';
import type { ActivityItem, ActivityStreamProps } from './types';
// Mock data generator
const mockTools = [
'web-scraper',
'sql-query',
'email-sender',
'github-manager',
'image-processor',
'pdf-generator',
'slack-bot',
'calendar-sync',
'data-validator',
'file-converter',
];
const activityTypes: ActivityItem['type'][] = ['invoked', 'published', 'updated'];
function generateMockActivity(): ActivityItem {
const type = activityTypes[Math.floor(Math.random() * activityTypes.length)] || 'invoked';
const tool = mockTools[Math.floor(Math.random() * mockTools.length)] || 'unknown-tool';
const seconds = Math.floor(Math.random() * 30) + 1;
return {
type,
tool,
time: `${seconds}s ago`,
};
}
function getActivityIcon(type: ActivityItem['type']): string {
switch (type) {
case 'invoked':
return '▸';
case 'published':
return '+';
case 'updated':
return '↻';
default:
return '•';
}
}
function getActivityColor(type: ActivityItem['type']): string {
switch (type) {
case 'invoked':
return 'text-brutalist-accent';
case 'published':
return 'text-green-500';
case 'updated':
return 'text-yellow-500';
default:
return 'text-foreground-secondary';
}
}
export function ActivityStream({
activities: initialActivities,
updateInterval = 6000,
maxItems = 5,
className = '',
}: ActivityStreamProps): React.ReactElement {
// Initialize with provided activities or generate mock data
const [activities, setActivities] = useState<ActivityItem[]>(
initialActivities || Array.from({ length: maxItems }, generateMockActivity)
);
// Auto-update with new activities
useEffect(() => {
const interval = setInterval(() => {
const newActivity = generateMockActivity();
setActivities((prev) => {
const updated = [newActivity, ...prev];
return updated.slice(0, maxItems);
});
}, updateInterval);
return () => clearInterval(interval);
}, [updateInterval, maxItems]);
return (
<div
className={`bg-surface border-2 border-foreground p-6 ${className}`}
style={{ borderRadius: 0 }}
>
<h3 className="text-sm font-mono font-bold uppercase tracking-wider text-foreground-secondary mb-4">
Recent Activity
</h3>
<div className="space-y-2 font-mono text-sm">
{activities.map((activity, index) => (
<div
key={`${activity.tool}-${activity.time}-${index}`}
className="flex items-center gap-3 animate-slide-down"
style={{
animationDuration: '300ms',
animationFillMode: 'backwards',
}}
>
<span className={`${getActivityColor(activity.type)} font-bold w-4`}>
{getActivityIcon(activity.type)}
</span>
<span className="text-foreground flex-1">
<span className="font-bold">{activity.tool}</span>
<span className="text-foreground-secondary"> {activity.type}</span>
</span>
<span className="text-foreground-tertiary text-xs">{activity.time}</span>
</div>
))}
</div>
</div>
);
}