diff --git a/apps/example/package.json b/apps/example/package.json deleted file mode 100644 index b2fc5ab..0000000 --- a/apps/example/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "react-http-example", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx watch src/app.tsx", - "start": "tsx src/app.tsx", - "start:advanced": "tsx src/advanced.tsx" - }, - "dependencies": { - "react": "^18.2.0", - "react-http": "workspace:*" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "@types/react": "^18.2.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" - } -} diff --git a/apps/example/src/advanced.tsx b/apps/example/src/advanced.tsx deleted file mode 100644 index 7602729..0000000 --- a/apps/example/src/advanced.tsx +++ /dev/null @@ -1,351 +0,0 @@ -import React from 'react'; -import { - createServer, - Server, - Route, - Get, - Post, - Put, - Delete, - Middleware, -} from 'react-http'; -import type { RequestContext, MiddlewareHandler } from 'react-http'; - -// ============================================================================= -// MIDDLEWARES -// ============================================================================= - -const logger: MiddlewareHandler = async (ctx, next) => { - const start = Date.now(); - await next(); - const ms = Date.now() - start; - console.log(`${ctx.method} ${ctx.path} - ${ctx.res.statusCode} - ${ms}ms`); -}; - -const cors: MiddlewareHandler = async (ctx, next) => { - ctx.res.setHeader('Access-Control-Allow-Origin', '*'); - ctx.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); - ctx.res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if (ctx.method === 'OPTIONS') { - ctx.res.statusCode = 204; - ctx.res.end(); - return; - } - await next(); -}; - -const authRequired: MiddlewareHandler = async (ctx, next) => { - const authHeader = ctx.req.headers.authorization; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - ctx.res.statusCode = 401; - ctx.res.setHeader('Content-Type', 'application/json'); - ctx.res.end(JSON.stringify({ error: 'Unauthorized', message: 'Bearer token required' })); - return; - } - - // Simulate token validation - const token = authHeader.slice(7); - if (token !== 'secret-token') { - ctx.res.statusCode = 403; - ctx.res.setHeader('Content-Type', 'application/json'); - ctx.res.end(JSON.stringify({ error: 'Forbidden', message: 'Invalid token' })); - return; - } - - await next(); -}; - -// ============================================================================= -// IN-MEMORY DATABASE -// ============================================================================= - -interface User { - id: number; - name: string; - email: string; - role: 'admin' | 'user'; -} - -interface Post { - id: number; - userId: number; - title: string; - content: string; - createdAt: string; -} - -const db = { - users: [ - { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' as const }, - { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' as const }, - { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'user' as const }, - ], - posts: [ - { id: 1, userId: 1, title: 'Hello World', content: 'My first post', createdAt: '2024-01-01' }, - { id: 2, userId: 1, title: 'React HTTP', content: 'Building servers with JSX', createdAt: '2024-01-02' }, - { id: 3, userId: 2, title: 'Node.js Tips', content: 'Some useful tips', createdAt: '2024-01-03' }, - ], - nextUserId: 4, - nextPostId: 4, -}; - -// ============================================================================= -// HANDLERS -// ============================================================================= - -// Users handlers -function listUsers(ctx: RequestContext) { - const { role, search } = ctx.query; - - let users = db.users; - - if (role) { - users = users.filter(u => u.role === role); - } - if (search) { - users = users.filter(u => - u.name.toLowerCase().includes(search.toLowerCase()) || - u.email.toLowerCase().includes(search.toLowerCase()) - ); - } - - return { users, count: users.length }; -} - -function getUser(ctx: RequestContext) { - const id = parseInt(ctx.params.id); - const user = db.users.find(u => u.id === id); - - if (!user) { - ctx.res.statusCode = 404; - return { error: 'User not found' }; - } - - return user; -} - -function createUser(ctx: RequestContext) { - const { name, email, role = 'user' } = ctx.body || {}; - - if (!name || !email) { - ctx.res.statusCode = 400; - return { error: 'Name and email are required' }; - } - - const user: User = { - id: db.nextUserId++, - name, - email, - role, - }; - - db.users.push(user); - ctx.res.statusCode = 201; - return user; -} - -function updateUser(ctx: RequestContext) { - const id = parseInt(ctx.params.id); - const user = db.users.find(u => u.id === id); - - if (!user) { - ctx.res.statusCode = 404; - return { error: 'User not found' }; - } - - const { name, email, role } = ctx.body || {}; - if (name) user.name = name; - if (email) user.email = email; - if (role) user.role = role; - - return user; -} - -function deleteUser(ctx: RequestContext) { - const id = parseInt(ctx.params.id); - const index = db.users.findIndex(u => u.id === id); - - if (index === -1) { - ctx.res.statusCode = 404; - return { error: 'User not found' }; - } - - db.users.splice(index, 1); - ctx.res.statusCode = 204; - return undefined; -} - -// Posts handlers -function listPosts(ctx: RequestContext) { - const { userId } = ctx.query; - - let posts = db.posts; - if (userId) { - posts = posts.filter(p => p.userId === parseInt(userId)); - } - - return { posts, count: posts.length }; -} - -function getUserPosts(ctx: RequestContext) { - const userId = parseInt(ctx.params.id); - const posts = db.posts.filter(p => p.userId === userId); - return { posts, count: posts.length }; -} - -function getPost(ctx: RequestContext) { - const id = parseInt(ctx.params.postId); - const post = db.posts.find(p => p.id === id); - - if (!post) { - ctx.res.statusCode = 404; - return { error: 'Post not found' }; - } - - // Include author info - const author = db.users.find(u => u.id === post.userId); - return { ...post, author }; -} - -function createPost(ctx: RequestContext) { - const { userId, title, content } = ctx.body || {}; - - if (!userId || !title || !content) { - ctx.res.statusCode = 400; - return { error: 'userId, title and content are required' }; - } - - const post: Post = { - id: db.nextPostId++, - userId, - title, - content, - createdAt: new Date().toISOString().split('T')[0], - }; - - db.posts.push(post); - ctx.res.statusCode = 201; - return post; -} - -// Health & info -function healthCheck() { - return { status: 'ok', timestamp: new Date().toISOString() }; -} - -function serverInfo() { - return { - name: 'react-http-advanced-example', - version: '1.0.0', - endpoints: [ - 'GET /health', - 'GET /api/users', - 'POST /api/users', - 'GET /api/users/:id', - 'PUT /api/users/:id', - 'DELETE /api/users/:id', - 'GET /api/users/:id/posts', - 'GET /api/posts', - 'POST /api/posts', - 'GET /api/posts/:postId', - 'GET /admin/stats (requires auth)', - ], - }; -} - -// Admin handlers (protected) -function getStats() { - return { - users: db.users.length, - posts: db.posts.length, - usersByRole: { - admin: db.users.filter(u => u.role === 'admin').length, - user: db.users.filter(u => u.role === 'user').length, - }, - }; -} - -// ============================================================================= -// APP COMPONENT -// ============================================================================= - -function App() { - return ( - - {/* Global middlewares */} - - - - {/* Public endpoints */} - - - - - - - - - {/* API routes */} - - {/* Users CRUD */} - - - - - - - - - - - - - - - - {/* Posts */} - - - - - - - - - - - {/* Protected admin routes */} - - - - - - - - - ); -} - -// ============================================================================= -// START SERVER -// ============================================================================= - -const server = createServer(); - -server.listen().then(() => { - console.log('\nReact HTTP Advanced Example'); - console.log('============================\n'); - console.log('Try these commands:\n'); - console.log(' curl http://localhost:3000/info'); - console.log(' curl http://localhost:3000/api/users'); - console.log(' curl "http://localhost:3000/api/users?role=admin"'); - console.log(' curl http://localhost:3000/api/users/1'); - console.log(' curl http://localhost:3000/api/users/1/posts'); - console.log(' curl http://localhost:3000/api/posts'); - console.log(' curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d \'{"name":"Dave","email":"dave@example.com"}\''); - console.log(' curl http://localhost:3000/admin/stats # Will return 401'); - console.log(' curl http://localhost:3000/admin/stats -H "Authorization: Bearer secret-token"'); - console.log(''); -}); diff --git a/apps/example/src/app.tsx b/apps/example/src/app.tsx deleted file mode 100644 index 94632a1..0000000 --- a/apps/example/src/app.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { - createServer, - Server, - Route, - Get, - Post, - Middleware, -} from 'react-http'; -import type { RequestContext, MiddlewareHandler } from 'react-http'; - -// Simple logging middleware -const logger: MiddlewareHandler = async (ctx, next) => { - const start = Date.now(); - await next(); - const ms = Date.now() - start; - console.log(`${ctx.method} ${ctx.path} - ${ms}ms`); -}; - -// Handlers -function getToto(ctx: RequestContext) { - return { message: 'Hello from /toto!', query: ctx.query }; -} - -function getUsers(ctx: RequestContext) { - return [ - { id: 1, name: 'Alice' }, - { id: 2, name: 'Bob' }, - ]; -} - -function createUser(ctx: RequestContext) { - return { created: true, user: ctx.body }; -} - -function getUserById(ctx: RequestContext) { - return { id: ctx.params.id, name: `User ${ctx.params.id}` }; -} - -function healthCheck() { - return { status: 'ok' }; -} - -function App() { - return ( - - - - - - - - - - - - - - - - - - - - ); -} - -const server = createServer(); - -server.listen().then(() => { - console.log('React HTTP server is running!'); - console.log('Try: curl http://localhost:3000/toto'); -}); diff --git a/apps/example/tsconfig.json b/apps/example/tsconfig.json deleted file mode 100644 index 9903e5c..0000000 --- a/apps/example/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "noEmit": true - }, - "include": ["src/**/*"] -} diff --git a/apps/todo-app/backend/package.json b/apps/todo-app/backend/package.json new file mode 100644 index 0000000..5f014e0 --- /dev/null +++ b/apps/todo-app/backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "@todo-app/backend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/server.tsx", + "dev": "tsx watch src/server.tsx", + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "clean": "rm -rf dist" + }, + "dependencies": { + "react": "^18.2.0", + "react-http": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/react": "^18.2.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/todo-app/backend/src/__tests__/api.test.tsx b/apps/todo-app/backend/src/__tests__/api.test.tsx new file mode 100644 index 0000000..07287cd --- /dev/null +++ b/apps/todo-app/backend/src/__tests__/api.test.tsx @@ -0,0 +1,367 @@ +import React from 'react'; +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import http from 'http'; +import { + createServer, + Server, + Route, + Get, + Post, + Put, + Delete, + Middleware, + type RequestContext, + type MiddlewareHandler, +} from 'react-http'; + +// ============================================================================ +// Types +// ============================================================================ + +interface Todo { + id: string; + title: string; + completed: boolean; + createdAt: string; +} + +// ============================================================================ +// Test utilities +// ============================================================================ + +const request = async ( + port: number, + method: string, + path: string, + body?: any +): Promise<{ status: number; data: any }> => { + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: 'localhost', + port, + path, + method, + headers: { + 'Content-Type': 'application/json', + }, + }, + (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + try { + resolve({ + status: res.statusCode || 0, + data: data ? JSON.parse(data) : null, + }); + } catch { + resolve({ + status: res.statusCode || 0, + data: data, + }); + } + }); + } + ); + + req.on('error', reject); + + if (body) { + req.write(JSON.stringify(body)); + } + + req.end(); + }); +}; + +// ============================================================================ +// Test app setup +// ============================================================================ + +const createTestApp = () => { + const todos: Map = new Map(); + let nextId = 1; + + const cors: MiddlewareHandler = async (ctx, next) => { + ctx.res.setHeader('Access-Control-Allow-Origin', '*'); + await next(); + }; + + const listTodos = (ctx: RequestContext) => { + const { completed } = ctx.query; + let result = Array.from(todos.values()); + + if (completed !== undefined) { + const isCompleted = completed === 'true'; + result = result.filter((todo) => todo.completed === isCompleted); + } + + return { todos: result }; + }; + + const getTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + return { todo }; + }; + + const createTodo = (ctx: RequestContext) => { + const { title } = ctx.body || {}; + + if (!title || typeof title !== 'string') { + ctx.res.statusCode = 400; + return { error: 'Title is required' }; + } + + const id = String(nextId++); + const todo: Todo = { + id, + title: title.trim(), + completed: false, + createdAt: new Date().toISOString(), + }; + + todos.set(id, todo); + ctx.res.statusCode = 201; + + return { todo }; + }; + + const updateTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + const { title, completed } = ctx.body || {}; + + if (title !== undefined) { + todo.title = String(title).trim(); + } + + if (completed !== undefined) { + todo.completed = Boolean(completed); + } + + return { todo }; + }; + + const deleteTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + todos.delete(ctx.params.id); + ctx.res.statusCode = 204; + }; + + const getStats = () => { + const all = Array.from(todos.values()); + return { + total: all.length, + completed: all.filter((t) => t.completed).length, + pending: all.filter((t) => !t.completed).length, + }; + }; + + const App = () => ( + + + + + ({ status: 'ok' })} /> + + + + + + + + + + + + + + + ); + + return { App, todos, reset: () => { todos.clear(); nextId = 1; } }; +}; + +// ============================================================================ +// Tests +// ============================================================================ + +describe('Todo API', () => { + let port: number; + let server: ReturnType; + let httpServer: http.Server; + let testApp: ReturnType; + + beforeAll(async () => { + testApp = createTestApp(); + server = createServer(); + httpServer = await server.listen(0); + port = (httpServer.address() as any).port; + }); + + afterAll(async () => { + await server.close(); + }); + + beforeEach(() => { + testApp.reset(); + }); + + describe('GET /api/health', () => { + it('should return status ok', async () => { + const res = await request(port, 'GET', '/api/health'); + expect(res.status).toBe(200); + expect(res.data).toEqual({ status: 'ok' }); + }); + }); + + describe('GET /api/todos', () => { + it('should return empty array when no todos exist', async () => { + const res = await request(port, 'GET', '/api/todos'); + expect(res.status).toBe(200); + expect(res.data.todos).toEqual([]); + }); + + it('should return all todos', async () => { + await request(port, 'POST', '/api/todos', { title: 'Test todo 1' }); + await request(port, 'POST', '/api/todos', { title: 'Test todo 2' }); + + const res = await request(port, 'GET', '/api/todos'); + expect(res.status).toBe(200); + expect(res.data.todos).toHaveLength(2); + }); + + it('should filter by completed status', async () => { + await request(port, 'POST', '/api/todos', { title: 'Incomplete' }); + const { data: { todo } } = await request(port, 'POST', '/api/todos', { title: 'Complete' }); + await request(port, 'PUT', `/api/todos/${todo.id}`, { completed: true }); + + const completedRes = await request(port, 'GET', '/api/todos?completed=true'); + expect(completedRes.data.todos).toHaveLength(1); + expect(completedRes.data.todos[0].title).toBe('Complete'); + + const pendingRes = await request(port, 'GET', '/api/todos?completed=false'); + expect(pendingRes.data.todos).toHaveLength(1); + expect(pendingRes.data.todos[0].title).toBe('Incomplete'); + }); + }); + + describe('POST /api/todos', () => { + it('should create a new todo', async () => { + const res = await request(port, 'POST', '/api/todos', { title: 'New todo' }); + expect(res.status).toBe(201); + expect(res.data.todo).toMatchObject({ + title: 'New todo', + completed: false, + }); + expect(res.data.todo.id).toBeDefined(); + expect(res.data.todo.createdAt).toBeDefined(); + }); + + it('should return 400 when title is missing', async () => { + const res = await request(port, 'POST', '/api/todos', {}); + expect(res.status).toBe(400); + expect(res.data.error).toBe('Title is required'); + }); + + it('should trim whitespace from title', async () => { + const res = await request(port, 'POST', '/api/todos', { title: ' Trimmed title ' }); + expect(res.data.todo.title).toBe('Trimmed title'); + }); + }); + + describe('GET /api/todos/:id', () => { + it('should return a todo by id', async () => { + const createRes = await request(port, 'POST', '/api/todos', { title: 'Test' }); + const id = createRes.data.todo.id; + + const res = await request(port, 'GET', `/api/todos/${id}`); + expect(res.status).toBe(200); + expect(res.data.todo.id).toBe(id); + }); + + it('should return 404 for non-existent todo', async () => { + const res = await request(port, 'GET', '/api/todos/999'); + expect(res.status).toBe(404); + expect(res.data.error).toBe('Todo not found'); + }); + }); + + describe('PUT /api/todos/:id', () => { + it('should update todo title', async () => { + const createRes = await request(port, 'POST', '/api/todos', { title: 'Original' }); + const id = createRes.data.todo.id; + + const res = await request(port, 'PUT', `/api/todos/${id}`, { title: 'Updated' }); + expect(res.status).toBe(200); + expect(res.data.todo.title).toBe('Updated'); + }); + + it('should update todo completed status', async () => { + const createRes = await request(port, 'POST', '/api/todos', { title: 'Test' }); + const id = createRes.data.todo.id; + + const res = await request(port, 'PUT', `/api/todos/${id}`, { completed: true }); + expect(res.status).toBe(200); + expect(res.data.todo.completed).toBe(true); + }); + + it('should return 404 for non-existent todo', async () => { + const res = await request(port, 'PUT', '/api/todos/999', { title: 'Test' }); + expect(res.status).toBe(404); + }); + }); + + describe('DELETE /api/todos/:id', () => { + it('should delete a todo', async () => { + const createRes = await request(port, 'POST', '/api/todos', { title: 'To delete' }); + const id = createRes.data.todo.id; + + const deleteRes = await request(port, 'DELETE', `/api/todos/${id}`); + expect(deleteRes.status).toBe(204); + + const getRes = await request(port, 'GET', `/api/todos/${id}`); + expect(getRes.status).toBe(404); + }); + + it('should return 404 for non-existent todo', async () => { + const res = await request(port, 'DELETE', '/api/todos/999'); + expect(res.status).toBe(404); + }); + }); + + describe('GET /api/stats', () => { + it('should return correct statistics', async () => { + await request(port, 'POST', '/api/todos', { title: 'Pending 1' }); + await request(port, 'POST', '/api/todos', { title: 'Pending 2' }); + const { data: { todo } } = await request(port, 'POST', '/api/todos', { title: 'Completed' }); + await request(port, 'PUT', `/api/todos/${todo.id}`, { completed: true }); + + const res = await request(port, 'GET', '/api/stats'); + expect(res.status).toBe(200); + expect(res.data).toEqual({ + total: 3, + completed: 1, + pending: 2, + }); + }); + }); +}); diff --git a/apps/todo-app/backend/src/server.tsx b/apps/todo-app/backend/src/server.tsx new file mode 100644 index 0000000..e350203 --- /dev/null +++ b/apps/todo-app/backend/src/server.tsx @@ -0,0 +1,220 @@ +import React from 'react'; +import { + createServer, + Server, + Route, + Get, + Post, + Put, + Delete, + Middleware, + type RequestContext, + type MiddlewareHandler, +} from 'react-http'; + +// ============================================================================ +// Types +// ============================================================================ + +interface Todo { + id: string; + title: string; + completed: boolean; + createdAt: string; +} + +// ============================================================================ +// In-memory data store +// ============================================================================ + +const todos: Map = new Map(); +let nextId = 1; + +// Seed some initial data +const seedTodos = () => { + const initial: Omit[] = [ + { title: 'Learn React', completed: true }, + { title: 'Build a todo app', completed: false }, + { title: 'Deploy to production', completed: false }, + ]; + + initial.forEach((todo) => { + const id = String(nextId++); + todos.set(id, { + ...todo, + id, + createdAt: new Date().toISOString(), + }); + }); +}; + +seedTodos(); + +// ============================================================================ +// Middleware +// ============================================================================ + +const logger: MiddlewareHandler = async (ctx, next) => { + const start = Date.now(); + console.log(`→ ${ctx.method} ${ctx.path}`); + await next(); + const duration = Date.now() - start; + console.log(`← ${ctx.method} ${ctx.path} ${ctx.res.statusCode} (${duration}ms)`); +}; + +const cors: MiddlewareHandler = async (ctx, next) => { + ctx.res.setHeader('Access-Control-Allow-Origin', '*'); + ctx.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + ctx.res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (ctx.method === 'OPTIONS') { + ctx.res.statusCode = 204; + ctx.res.end(); + return; + } + + await next(); +}; + +// ============================================================================ +// Handlers +// ============================================================================ + +const listTodos = (ctx: RequestContext) => { + const { completed } = ctx.query; + let result = Array.from(todos.values()); + + if (completed !== undefined) { + const isCompleted = completed === 'true'; + result = result.filter((todo) => todo.completed === isCompleted); + } + + return { todos: result }; +}; + +const getTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + return { todo }; +}; + +const createTodo = (ctx: RequestContext) => { + const { title } = ctx.body || {}; + + if (!title || typeof title !== 'string') { + ctx.res.statusCode = 400; + return { error: 'Title is required' }; + } + + const id = String(nextId++); + const todo: Todo = { + id, + title: title.trim(), + completed: false, + createdAt: new Date().toISOString(), + }; + + todos.set(id, todo); + ctx.res.statusCode = 201; + + return { todo }; +}; + +const updateTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + const { title, completed } = ctx.body || {}; + + if (title !== undefined) { + todo.title = String(title).trim(); + } + + if (completed !== undefined) { + todo.completed = Boolean(completed); + } + + return { todo }; +}; + +const deleteTodo = (ctx: RequestContext) => { + const todo = todos.get(ctx.params.id); + + if (!todo) { + ctx.res.statusCode = 404; + return { error: 'Todo not found' }; + } + + todos.delete(ctx.params.id); + ctx.res.statusCode = 204; +}; + +const getStats = () => { + const all = Array.from(todos.values()); + return { + total: all.length, + completed: all.filter((t) => t.completed).length, + pending: all.filter((t) => !t.completed).length, + }; +}; + +// ============================================================================ +// Application +// ============================================================================ + +const App = () => ( + + + + + + {/* Health check */} + ({ status: 'ok' })} /> + + {/* Stats */} + + + {/* Todos CRUD */} + + + + + + + + + + + + +); + +// ============================================================================ +// Start server +// ============================================================================ + +const server = createServer(); + +server.listen().then(() => { + console.log('🚀 Todo API server running at http://localhost:3001'); + console.log(''); + console.log('Available endpoints:'); + console.log(' GET /api/health - Health check'); + console.log(' GET /api/stats - Get todo statistics'); + console.log(' GET /api/todos - List all todos'); + console.log(' POST /api/todos - Create a new todo'); + console.log(' GET /api/todos/:id - Get a todo by ID'); + console.log(' PUT /api/todos/:id - Update a todo'); + console.log(' DELETE /api/todos/:id - Delete a todo'); +}); + +export { App, createTodo, listTodos, getTodo, updateTodo, deleteTodo }; diff --git a/apps/todo-app/backend/tsconfig.json b/apps/todo-app/backend/tsconfig.json new file mode 100644 index 0000000..bf012ec --- /dev/null +++ b/apps/todo-app/backend/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/apps/todo-app/backend/vitest.config.ts b/apps/todo-app/backend/vitest.config.ts new file mode 100644 index 0000000..014f97e --- /dev/null +++ b/apps/todo-app/backend/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, +}); diff --git a/apps/todo-app/frontend/index.html b/apps/todo-app/frontend/index.html new file mode 100644 index 0000000..da149f6 --- /dev/null +++ b/apps/todo-app/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Todo App - React + react-http Example + + +
+ + + diff --git a/apps/todo-app/frontend/package.json b/apps/todo-app/frontend/package.json new file mode 100644 index 0000000..453bc21 --- /dev/null +++ b/apps/todo-app/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "@todo-app/frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "clean": "rm -rf dist" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^14.2.0", + "@testing-library/user-event": "^14.5.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "jsdom": "^24.0.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/todo-app/frontend/src/App.tsx b/apps/todo-app/frontend/src/App.tsx new file mode 100644 index 0000000..988559f --- /dev/null +++ b/apps/todo-app/frontend/src/App.tsx @@ -0,0 +1,67 @@ +import { useTodos } from './hooks/useTodos'; +import { TodoForm } from './components/TodoForm'; +import { TodoList } from './components/TodoList'; +import { TodoFilters } from './components/TodoFilters'; +import './styles.css'; + +export function App() { + const { + todos, + stats, + filter, + setFilter, + loading, + error, + addTodo, + toggleTodo, + updateTodo, + deleteTodo, + } = useTodos(); + + return ( +
+
+

todos

+
+ +
+ + + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+ Loading... +
+ ) : ( + <> + + {stats.total > 0 && ( + + )} + + )} +
+ +
+

Double-click to edit a todo

+

+ Built with React + react-http +

+
+
+ ); +} diff --git a/apps/todo-app/frontend/src/__tests__/TodoFilters.test.tsx b/apps/todo-app/frontend/src/__tests__/TodoFilters.test.tsx new file mode 100644 index 0000000..e58a4c9 --- /dev/null +++ b/apps/todo-app/frontend/src/__tests__/TodoFilters.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { TodoFilters } from '../components/TodoFilters'; + +describe('TodoFilters', () => { + const defaultProps = { + filter: 'all' as const, + onFilterChange: vi.fn(), + stats: { total: 5, completed: 2, pending: 3 }, + }; + + it('displays pending count', () => { + render(); + + expect(screen.getByText('3 items left')).toBeInTheDocument(); + }); + + it('renders all filter buttons', () => { + render(); + + expect(screen.getByTestId('filter-all')).toBeInTheDocument(); + expect(screen.getByTestId('filter-active')).toBeInTheDocument(); + expect(screen.getByTestId('filter-completed')).toBeInTheDocument(); + }); + + it('highlights active filter', () => { + render(); + + expect(screen.getByTestId('filter-active')).toHaveClass('active'); + expect(screen.getByTestId('filter-all')).not.toHaveClass('active'); + expect(screen.getByTestId('filter-completed')).not.toHaveClass('active'); + }); + + it('calls onFilterChange when filter button is clicked', async () => { + const onFilterChange = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByTestId('filter-completed')); + + expect(onFilterChange).toHaveBeenCalledWith('completed'); + }); + + it('updates pending count based on stats', () => { + render( + + ); + + expect(screen.getByText('3 items left')).toBeInTheDocument(); + }); + + it('shows 0 items left when all completed', () => { + render( + + ); + + expect(screen.getByText('0 items left')).toBeInTheDocument(); + }); +}); diff --git a/apps/todo-app/frontend/src/__tests__/TodoForm.test.tsx b/apps/todo-app/frontend/src/__tests__/TodoForm.test.tsx new file mode 100644 index 0000000..e60dece --- /dev/null +++ b/apps/todo-app/frontend/src/__tests__/TodoForm.test.tsx @@ -0,0 +1,70 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { TodoForm } from '../components/TodoForm'; + +describe('TodoForm', () => { + it('renders input and submit button', () => { + render(); + + expect(screen.getByTestId('todo-input')).toBeInTheDocument(); + expect(screen.getByTestId('submit-btn')).toBeInTheDocument(); + }); + + it('has correct placeholder text', () => { + render(); + + expect(screen.getByPlaceholderText('What needs to be done?')).toBeInTheDocument(); + }); + + it('submit button is disabled when input is empty', () => { + render(); + + expect(screen.getByTestId('submit-btn')).toBeDisabled(); + }); + + it('submit button is enabled when input has value', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('todo-input'), 'New todo'); + + expect(screen.getByTestId('submit-btn')).not.toBeDisabled(); + }); + + it('calls onSubmit with trimmed value on form submission', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('todo-input'), ' New todo '); + await user.click(screen.getByTestId('submit-btn')); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith('New todo'); + }); + }); + + it('clears input after successful submission', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('todo-input'), 'New todo'); + await user.click(screen.getByTestId('submit-btn')); + + await waitFor(() => { + expect(screen.getByTestId('todo-input')).toHaveValue(''); + }); + }); + + it('does not submit when input is only whitespace', async () => { + const onSubmit = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('todo-input'), ' '); + + expect(screen.getByTestId('submit-btn')).toBeDisabled(); + }); +}); diff --git a/apps/todo-app/frontend/src/__tests__/TodoItem.test.tsx b/apps/todo-app/frontend/src/__tests__/TodoItem.test.tsx new file mode 100644 index 0000000..7675be9 --- /dev/null +++ b/apps/todo-app/frontend/src/__tests__/TodoItem.test.tsx @@ -0,0 +1,106 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { TodoItem } from '../components/TodoItem'; +import type { Todo } from '../types'; + +const createTodo = (overrides: Partial = {}): Todo => ({ + id: '1', + title: 'Test todo', + completed: false, + createdAt: '2024-01-01T00:00:00.000Z', + ...overrides, +}); + +describe('TodoItem', () => { + const defaultProps = { + onToggle: vi.fn(), + onUpdate: vi.fn().mockResolvedValue(undefined), + onDelete: vi.fn(), + }; + + it('renders todo title', () => { + render(); + + expect(screen.getByTestId('todo-title')).toHaveTextContent('Test todo'); + }); + + it('renders unchecked checkbox for incomplete todo', () => { + render(); + + expect(screen.getByTestId('todo-checkbox')).not.toBeChecked(); + }); + + it('renders checked checkbox for completed todo', () => { + render(); + + expect(screen.getByTestId('todo-checkbox')).toBeChecked(); + }); + + it('applies completed class when todo is completed', () => { + const { container } = render( + + ); + + expect(container.querySelector('.todo-item')).toHaveClass('completed'); + }); + + it('calls onToggle when checkbox is clicked', async () => { + const onToggle = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByTestId('todo-checkbox')); + + expect(onToggle).toHaveBeenCalledWith('1'); + }); + + it('calls onDelete when delete button is clicked', async () => { + const onDelete = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByTestId('delete-btn')); + + expect(onDelete).toHaveBeenCalledWith('1'); + }); + + it('enters edit mode on double click', async () => { + const user = userEvent.setup(); + render(); + + await user.dblClick(screen.getByTestId('todo-title')); + + expect(screen.getByTestId('edit-input')).toBeInTheDocument(); + }); + + it('shows current title in edit input', async () => { + const user = userEvent.setup(); + render(); + + await user.dblClick(screen.getByTestId('todo-title')); + + expect(screen.getByTestId('edit-input')).toHaveValue('Current title'); + }); + + it('exits edit mode on blur', async () => { + const user = userEvent.setup(); + render(); + + await user.dblClick(screen.getByTestId('todo-title')); + fireEvent.blur(screen.getByTestId('edit-input')); + + expect(screen.queryByTestId('edit-input')).not.toBeInTheDocument(); + expect(screen.getByTestId('todo-title')).toBeInTheDocument(); + }); + + it('exits edit mode on Escape key', async () => { + const user = userEvent.setup(); + render(); + + await user.dblClick(screen.getByTestId('todo-title')); + await user.keyboard('{Escape}'); + + expect(screen.queryByTestId('edit-input')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/todo-app/frontend/src/__tests__/TodoList.test.tsx b/apps/todo-app/frontend/src/__tests__/TodoList.test.tsx new file mode 100644 index 0000000..e851da9 --- /dev/null +++ b/apps/todo-app/frontend/src/__tests__/TodoList.test.tsx @@ -0,0 +1,63 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { TodoList } from '../components/TodoList'; +import type { Todo } from '../types'; + +const createTodo = (id: string, title: string, completed = false): Todo => ({ + id, + title, + completed, + createdAt: '2024-01-01T00:00:00.000Z', +}); + +describe('TodoList', () => { + const defaultProps = { + onToggle: vi.fn(), + onUpdate: vi.fn().mockResolvedValue(undefined), + onDelete: vi.fn(), + }; + + it('renders empty state when no todos', () => { + render(); + + expect(screen.getByTestId('empty-state')).toHaveTextContent('No todos to display'); + }); + + it('renders list of todos', () => { + const todos = [ + createTodo('1', 'First todo'), + createTodo('2', 'Second todo'), + createTodo('3', 'Third todo'), + ]; + + render(); + + expect(screen.getByTestId('todo-list')).toBeInTheDocument(); + expect(screen.getAllByTestId('todo-title')).toHaveLength(3); + }); + + it('renders todos with correct titles', () => { + const todos = [ + createTodo('1', 'Learn React'), + createTodo('2', 'Build app'), + ]; + + render(); + + expect(screen.getByText('Learn React')).toBeInTheDocument(); + expect(screen.getByText('Build app')).toBeInTheDocument(); + }); + + it('renders mix of completed and incomplete todos', () => { + const todos = [ + createTodo('1', 'Completed', true), + createTodo('2', 'Incomplete', false), + ]; + + render(); + + const checkboxes = screen.getAllByTestId('todo-checkbox'); + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + }); +}); diff --git a/apps/todo-app/frontend/src/__tests__/api.test.ts b/apps/todo-app/frontend/src/__tests__/api.test.ts new file mode 100644 index 0000000..cd513c9 --- /dev/null +++ b/apps/todo-app/frontend/src/__tests__/api.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { api } from '../api'; + +describe('api', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('getTodos', () => { + it('fetches todos without filter', async () => { + const mockTodos = [{ id: '1', title: 'Test', completed: false, createdAt: '' }]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todos: mockTodos }), + }); + + const result = await api.getTodos(); + + expect(fetch).toHaveBeenCalledWith('/api/todos'); + expect(result).toEqual(mockTodos); + }); + + it('fetches completed todos', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todos: [] }), + }); + + await api.getTodos(true); + + expect(fetch).toHaveBeenCalledWith('/api/todos?completed=true'); + }); + + it('fetches active todos', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todos: [] }), + }); + + await api.getTodos(false); + + expect(fetch).toHaveBeenCalledWith('/api/todos?completed=false'); + }); + }); + + describe('getTodo', () => { + it('fetches single todo by id', async () => { + const mockTodo = { id: '1', title: 'Test', completed: false, createdAt: '' }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todo: mockTodo }), + }); + + const result = await api.getTodo('1'); + + expect(fetch).toHaveBeenCalledWith('/api/todos/1'); + expect(result).toEqual(mockTodo); + }); + + it('throws error for not found', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'Todo not found' }), + }); + + await expect(api.getTodo('999')).rejects.toThrow('Todo not found'); + }); + }); + + describe('createTodo', () => { + it('creates todo with title', async () => { + const mockTodo = { id: '1', title: 'New', completed: false, createdAt: '' }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todo: mockTodo }), + }); + + const result = await api.createTodo('New'); + + expect(fetch).toHaveBeenCalledWith('/api/todos', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'New' }), + }); + expect(result).toEqual(mockTodo); + }); + + it('throws error for invalid input', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'Title is required' }), + }); + + await expect(api.createTodo('')).rejects.toThrow('Title is required'); + }); + }); + + describe('updateTodo', () => { + it('updates todo title', async () => { + const mockTodo = { id: '1', title: 'Updated', completed: false, createdAt: '' }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todo: mockTodo }), + }); + + const result = await api.updateTodo('1', { title: 'Updated' }); + + expect(fetch).toHaveBeenCalledWith('/api/todos/1', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Updated' }), + }); + expect(result).toEqual(mockTodo); + }); + + it('updates todo completed status', async () => { + const mockTodo = { id: '1', title: 'Test', completed: true, createdAt: '' }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ todo: mockTodo }), + }); + + const result = await api.updateTodo('1', { completed: true }); + + expect(fetch).toHaveBeenCalledWith('/api/todos/1', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ completed: true }), + }); + expect(result).toEqual(mockTodo); + }); + }); + + describe('deleteTodo', () => { + it('deletes todo', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 204, + }); + + await api.deleteTodo('1'); + + expect(fetch).toHaveBeenCalledWith('/api/todos/1', { + method: 'DELETE', + }); + }); + + it('throws error for not found', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'Todo not found' }), + }); + + await expect(api.deleteTodo('999')).rejects.toThrow('Todo not found'); + }); + }); + + describe('getStats', () => { + it('fetches statistics', async () => { + const mockStats = { total: 5, completed: 2, pending: 3 }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockStats), + }); + + const result = await api.getStats(); + + expect(fetch).toHaveBeenCalledWith('/api/stats'); + expect(result).toEqual(mockStats); + }); + }); +}); diff --git a/apps/todo-app/frontend/src/api.ts b/apps/todo-app/frontend/src/api.ts new file mode 100644 index 0000000..27c95bd --- /dev/null +++ b/apps/todo-app/frontend/src/api.ts @@ -0,0 +1,63 @@ +import type { Todo, TodoStats } from './types'; + +const API_BASE = '/api'; + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Request failed' })); + throw new Error(error.error || 'Request failed'); + } + + if (response.status === 204) { + return undefined as T; + } + + return response.json(); +} + +export const api = { + async getTodos(completed?: boolean): Promise { + const params = completed !== undefined ? `?completed=${completed}` : ''; + const response = await fetch(`${API_BASE}/todos${params}`); + const data = await handleResponse<{ todos: Todo[] }>(response); + return data.todos; + }, + + async getTodo(id: string): Promise { + const response = await fetch(`${API_BASE}/todos/${id}`); + const data = await handleResponse<{ todo: Todo }>(response); + return data.todo; + }, + + async createTodo(title: string): Promise { + const response = await fetch(`${API_BASE}/todos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }), + }); + const data = await handleResponse<{ todo: Todo }>(response); + return data.todo; + }, + + async updateTodo(id: string, updates: Partial>): Promise { + const response = await fetch(`${API_BASE}/todos/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + const data = await handleResponse<{ todo: Todo }>(response); + return data.todo; + }, + + async deleteTodo(id: string): Promise { + const response = await fetch(`${API_BASE}/todos/${id}`, { + method: 'DELETE', + }); + await handleResponse(response); + }, + + async getStats(): Promise { + const response = await fetch(`${API_BASE}/stats`); + return handleResponse(response); + }, +}; diff --git a/apps/todo-app/frontend/src/components/TodoFilters.tsx b/apps/todo-app/frontend/src/components/TodoFilters.tsx new file mode 100644 index 0000000..402b721 --- /dev/null +++ b/apps/todo-app/frontend/src/components/TodoFilters.tsx @@ -0,0 +1,36 @@ +import type { FilterType } from '../hooks/useTodos'; +import type { TodoStats } from '../types'; + +interface TodoFiltersProps { + filter: FilterType; + onFilterChange: (filter: FilterType) => void; + stats: TodoStats; +} + +export function TodoFilters({ filter, onFilterChange, stats }: TodoFiltersProps) { + const filters: { value: FilterType; label: string }[] = [ + { value: 'all', label: 'All' }, + { value: 'active', label: 'Active' }, + { value: 'completed', label: 'Completed' }, + ]; + + return ( +
+ + {stats.pending} items left + +
+ {filters.map(({ value, label }) => ( + + ))} +
+
+ ); +} diff --git a/apps/todo-app/frontend/src/components/TodoForm.tsx b/apps/todo-app/frontend/src/components/TodoForm.tsx new file mode 100644 index 0000000..71da265 --- /dev/null +++ b/apps/todo-app/frontend/src/components/TodoForm.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; + +interface TodoFormProps { + onSubmit: (title: string) => Promise; +} + +export function TodoForm({ onSubmit }: TodoFormProps) { + const [title, setTitle] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!title.trim() || submitting) return; + + setSubmitting(true); + try { + await onSubmit(title.trim()); + setTitle(''); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ setTitle(e.target.value)} + placeholder="What needs to be done?" + disabled={submitting} + data-testid="todo-input" + /> + +
+ ); +} diff --git a/apps/todo-app/frontend/src/components/TodoItem.tsx b/apps/todo-app/frontend/src/components/TodoItem.tsx new file mode 100644 index 0000000..c1a36f1 --- /dev/null +++ b/apps/todo-app/frontend/src/components/TodoItem.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import type { Todo } from '../types'; + +interface TodoItemProps { + todo: Todo; + onToggle: (id: string) => void; + onUpdate: (id: string, title: string) => Promise; + onDelete: (id: string) => void; +} + +export function TodoItem({ todo, onToggle, onUpdate, onDelete }: TodoItemProps) { + const [isEditing, setIsEditing] = useState(false); + const [editTitle, setEditTitle] = useState(todo.title); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (editTitle.trim() && editTitle !== todo.title) { + await onUpdate(todo.id, editTitle.trim()); + } + setIsEditing(false); + }; + + const handleCancel = () => { + setEditTitle(todo.title); + setIsEditing(false); + }; + + if (isEditing) { + return ( +
  • +
    + setEditTitle(e.target.value)} + onBlur={handleCancel} + onKeyDown={(e) => e.key === 'Escape' && handleCancel()} + autoFocus + data-testid="edit-input" + /> +
    +
  • + ); + } + + return ( +
  • + onToggle(todo.id)} + data-testid="todo-checkbox" + /> + setIsEditing(true)} + data-testid="todo-title" + > + {todo.title} + + +
  • + ); +} diff --git a/apps/todo-app/frontend/src/components/TodoList.tsx b/apps/todo-app/frontend/src/components/TodoList.tsx new file mode 100644 index 0000000..58500b0 --- /dev/null +++ b/apps/todo-app/frontend/src/components/TodoList.tsx @@ -0,0 +1,33 @@ +import type { Todo } from '../types'; +import { TodoItem } from './TodoItem'; + +interface TodoListProps { + todos: Todo[]; + onToggle: (id: string) => void; + onUpdate: (id: string, title: string) => Promise; + onDelete: (id: string) => void; +} + +export function TodoList({ todos, onToggle, onUpdate, onDelete }: TodoListProps) { + if (todos.length === 0) { + return ( +
    + No todos to display +
    + ); + } + + return ( +
      + {todos.map((todo) => ( + + ))} +
    + ); +} diff --git a/apps/todo-app/frontend/src/hooks/useTodos.ts b/apps/todo-app/frontend/src/hooks/useTodos.ts new file mode 100644 index 0000000..8e7f851 --- /dev/null +++ b/apps/todo-app/frontend/src/hooks/useTodos.ts @@ -0,0 +1,96 @@ +import { useState, useCallback, useEffect } from 'react'; +import type { Todo, TodoStats } from '../types'; +import { api } from '../api'; + +export type FilterType = 'all' | 'active' | 'completed'; + +export function useTodos() { + const [todos, setTodos] = useState([]); + const [stats, setStats] = useState({ total: 0, completed: 0, pending: 0 }); + const [filter, setFilter] = useState('all'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchTodos = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const completed = filter === 'all' ? undefined : filter === 'completed'; + const [todosData, statsData] = await Promise.all([ + api.getTodos(completed), + api.getStats(), + ]); + + setTodos(todosData); + setStats(statsData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load todos'); + } finally { + setLoading(false); + } + }, [filter]); + + useEffect(() => { + fetchTodos(); + }, [fetchTodos]); + + const addTodo = useCallback(async (title: string) => { + try { + setError(null); + await api.createTodo(title); + await fetchTodos(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to add todo'); + throw err; + } + }, [fetchTodos]); + + const toggleTodo = useCallback(async (id: string) => { + try { + setError(null); + const todo = todos.find((t) => t.id === id); + if (!todo) return; + + await api.updateTodo(id, { completed: !todo.completed }); + await fetchTodos(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update todo'); + } + }, [todos, fetchTodos]); + + const updateTodo = useCallback(async (id: string, title: string) => { + try { + setError(null); + await api.updateTodo(id, { title }); + await fetchTodos(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update todo'); + throw err; + } + }, [fetchTodos]); + + const deleteTodo = useCallback(async (id: string) => { + try { + setError(null); + await api.deleteTodo(id); + await fetchTodos(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete todo'); + } + }, [fetchTodos]); + + return { + todos, + stats, + filter, + setFilter, + loading, + error, + addTodo, + toggleTodo, + updateTodo, + deleteTodo, + refresh: fetchTodos, + }; +} diff --git a/apps/todo-app/frontend/src/main.tsx b/apps/todo-app/frontend/src/main.tsx new file mode 100644 index 0000000..185abac --- /dev/null +++ b/apps/todo-app/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/apps/todo-app/frontend/src/styles.css b/apps/todo-app/frontend/src/styles.css new file mode 100644 index 0000000..18816ab --- /dev/null +++ b/apps/todo-app/frontend/src/styles.css @@ -0,0 +1,218 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + padding: 40px 20px; +} + +.app { + width: 100%; + max-width: 550px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 80px; + font-weight: 100; + color: rgba(255, 255, 255, 0.3); + text-transform: lowercase; +} + +.todo-container { + background: white; + border-radius: 8px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +.todo-form { + display: flex; + border-bottom: 1px solid #e6e6e6; +} + +.todo-form input { + flex: 1; + padding: 16px 20px; + font-size: 18px; + border: none; + outline: none; +} + +.todo-form input::placeholder { + color: #ccc; + font-style: italic; +} + +.todo-form button { + padding: 16px 24px; + font-size: 14px; + font-weight: 600; + background: #667eea; + color: white; + border: none; + cursor: pointer; + transition: background 0.2s; +} + +.todo-form button:hover:not(:disabled) { + background: #5a67d8; +} + +.todo-form button:disabled { + background: #ccc; + cursor: not-allowed; +} + +.todo-list { + list-style: none; +} + +.todo-item { + display: flex; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e6e6e6; + transition: background 0.2s; +} + +.todo-item:hover { + background: #fafafa; +} + +.todo-item input[type='checkbox'] { + width: 20px; + height: 20px; + margin-right: 16px; + cursor: pointer; +} + +.todo-title { + flex: 1; + font-size: 18px; + color: #333; + cursor: pointer; +} + +.todo-item.completed .todo-title { + text-decoration: line-through; + color: #aaa; +} + +.todo-item.editing { + padding: 8px 20px; +} + +.todo-item.editing input { + width: 100%; + padding: 12px; + font-size: 18px; + border: 2px solid #667eea; + border-radius: 4px; + outline: none; +} + +.delete-btn { + width: 30px; + height: 30px; + font-size: 20px; + color: #cc9a9a; + background: none; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s, color 0.2s; +} + +.todo-item:hover .delete-btn { + opacity: 1; +} + +.delete-btn:hover { + color: #af5b5e; +} + +.todo-filters { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + font-size: 14px; + color: #777; +} + +.stats { + flex-shrink: 0; +} + +.filter-buttons { + display: flex; + gap: 4px; +} + +.filter-buttons button { + padding: 4px 8px; + font-size: 14px; + color: #777; + background: none; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + transition: border-color 0.2s; +} + +.filter-buttons button:hover { + border-color: #ddd; +} + +.filter-buttons button.active { + border-color: #667eea; + color: #667eea; +} + +.empty-state { + padding: 40px 20px; + text-align: center; + color: #999; + font-style: italic; +} + +.loading { + padding: 40px 20px; + text-align: center; + color: #667eea; +} + +.error { + padding: 12px 20px; + background: #fff5f5; + color: #c53030; + border-bottom: 1px solid #feb2b2; +} + +footer { + text-align: center; + margin-top: 30px; + color: rgba(255, 255, 255, 0.6); + font-size: 12px; +} + +footer p { + margin: 8px 0; +} + +footer strong { + color: rgba(255, 255, 255, 0.8); +} diff --git a/apps/todo-app/frontend/src/test-setup.ts b/apps/todo-app/frontend/src/test-setup.ts new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/apps/todo-app/frontend/src/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/apps/todo-app/frontend/src/types.ts b/apps/todo-app/frontend/src/types.ts new file mode 100644 index 0000000..f164c24 --- /dev/null +++ b/apps/todo-app/frontend/src/types.ts @@ -0,0 +1,12 @@ +export interface Todo { + id: string; + title: string; + completed: boolean; + createdAt: string; +} + +export interface TodoStats { + total: number; + completed: number; + pending: number; +} diff --git a/apps/todo-app/frontend/tsconfig.json b/apps/todo-app/frontend/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/apps/todo-app/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/apps/todo-app/frontend/vite.config.ts b/apps/todo-app/frontend/vite.config.ts new file mode 100644 index 0000000..d8ef605 --- /dev/null +++ b/apps/todo-app/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + }, + }, +}); diff --git a/apps/todo-app/frontend/vitest.config.ts b/apps/todo-app/frontend/vitest.config.ts new file mode 100644 index 0000000..3f210ec --- /dev/null +++ b/apps/todo-app/frontend/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/test-setup.ts'], + }, +}); diff --git a/packages/react-http/src/components.tsx b/packages/react-http/src/components.tsx index 0fb199a..1ace554 100644 --- a/packages/react-http/src/components.tsx +++ b/packages/react-http/src/components.tsx @@ -7,8 +7,8 @@ export interface ServerProps { children?: ReactNode; } -export function Server({ children }: ServerProps): React.ReactElement { - return React.createElement('server', {}, children); +export function Server({ port, children }: ServerProps): React.ReactElement { + return React.createElement('server', { port }, children); } export interface RouteProps { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51c6269..33df798 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,71 @@ importers: specifier: ^5.3.0 version: 5.9.3 + apps/todo-app/backend: + dependencies: + react: + specifier: ^18.2.0 + version: 18.3.1 + react-http: + specifier: workspace:* + version: link:../../../packages/react-http + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.19.32 + '@types/react': + specifier: ^18.2.0 + version: 18.3.28 + tsx: + specifier: ^4.7.0 + version: 4.21.0 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.19.32)(jsdom@24.1.3) + + apps/todo-app/frontend: + dependencies: + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.4.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^14.2.0 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.0 + version: 14.6.1(@testing-library/dom@9.3.4) + '@types/react': + specifier: ^18.2.0 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.7.0(vite@5.4.21(@types/node@20.19.32)) + jsdom: + specifier: ^24.0.0 + version: 24.1.3 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + vite: + specifier: ^5.0.0 + version: 5.4.21(@types/node@20.19.32) + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.19.32)(jsdom@24.1.3) + packages/react-http: dependencies: react: @@ -57,10 +122,131 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@20.19.32) + version: 2.1.9(@types/node@20.19.32)(jsdom@24.1.3) packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -355,9 +541,25 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -483,6 +685,42 @@ packages: cpu: [x64] os: [win32] + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@14.3.1': + resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -492,6 +730,11 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + '@types/react-reconciler@0.33.0': resolution: {integrity: sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==} peerDependencies: @@ -500,6 +743,12 @@ packages: '@types/react@18.3.28': resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -529,25 +778,112 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001768: + resolution: {integrity: sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -557,13 +893,68 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -574,6 +965,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -581,17 +976,179 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.3: resolution: {integrity: sha512-vp8Cj/+9Q/ibZUrq1rhy8mCTQpCk31A3uu9wc1C50yAb3x2pFHOsGdAZQ7jD86ARayyxZUViYeIztW+GE8dcrg==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -599,9 +1156,35 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -610,6 +1193,31 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -620,20 +1228,61 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-reconciler@0.29.2: resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} engines: {node: '>=0.10.0'} peerDependencies: react: ^18.3.1 + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -642,9 +1291,54 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -658,6 +1352,21 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -676,6 +1385,14 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -723,6 +1440,19 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -784,13 +1514,212 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + snapshots: + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -938,8 +1867,27 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -1015,6 +1963,63 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 9.3.4 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)': + dependencies: + '@testing-library/dom': 9.3.4 + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/estree@1.0.8': {} '@types/node@20.19.32': @@ -1023,6 +2028,10 @@ snapshots: '@types/prop-types@15.7.15': {} + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + '@types/react-reconciler@0.33.0(@types/react@18.3.28)': dependencies: '@types/react': 18.3.28 @@ -1032,6 +2041,18 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.32))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@20.19.32) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -1072,10 +2093,66 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + baseline-browser-mapping@2.9.19: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001768 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001768: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -1084,18 +2161,125 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + check-error@2.1.3: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.286: {} + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -1151,57 +2335,332 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 expect-type@1.3.0: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.3: dependencies: resolve-pkg-maps: 1.0.0 + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + indent-string@4.0.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + js-tokens@4.0.0: {} + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json5@2.2.3: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 loupe@3.2.1: {} + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-indent@1.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} + node-releases@2.0.27: {} + + nwsapi@2.2.23: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + pathe@1.1.2: {} pathval@2.0.1: {} picocolors@1.1.1: {} + possible-typed-array-names@1.1.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@17.0.2: {} + react-reconciler@0.29.2(react@18.3.1): dependencies: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 + react-refresh@0.17.0: {} + react@18.3.1: dependencies: loose-envify: 1.4.0 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + requires-port@1.0.0: {} + resolve-pkg-maps@1.0.0: {} rollup@4.57.1: @@ -1235,10 +2694,72 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 + semver@6.3.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -1247,6 +2768,21 @@ snapshots: std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1257,6 +2793,17 @@ snapshots: tinyspy@3.0.2: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -1295,6 +2842,19 @@ snapshots: undici-types@6.21.0: {} + universalify@0.2.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + vite-node@2.1.9(@types/node@20.19.32): dependencies: cac: 6.7.14 @@ -1322,7 +2882,7 @@ snapshots: '@types/node': 20.19.32 fsevents: 2.3.3 - vitest@2.1.9(@types/node@20.19.32): + vitest@2.1.9(@types/node@20.19.32)(jsdom@24.1.3): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.32)) @@ -1346,6 +2906,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.32 + jsdom: 24.1.3 transitivePeerDependencies: - less - lightningcss @@ -1357,7 +2918,57 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 63c8cfc..b4e96f7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - "packages/*" - "apps/*" + - "apps/todo-app/*" onlyBuiltDependencies: - esbuild