diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 0c426d4b..72baf65c 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/backend/igny8_core/ai/tests/test_generate_site_structure_function.py b/backend/igny8_core/ai/tests/test_generate_site_structure_function.py new file mode 100644 index 00000000..2f944a97 --- /dev/null +++ b/backend/igny8_core/ai/tests/test_generate_site_structure_function.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction +from igny8_core.business.site_building.models import PageBlueprint +from igny8_core.business.site_building.tests.base import SiteBuilderTestBase + + +class GenerateSiteStructureFunctionTests(SiteBuilderTestBase): + """Covers parsing + persistence logic for the Site Builder AI function.""" + + def setUp(self): + super().setUp() + self.function = GenerateSiteStructureFunction() + + def test_parse_response_extracts_json_object(self): + noisy_response = """ + Thoughts about the request… + { + "site": {"name": "Acme Robotics"}, + "pages": [{"slug": "home", "title": "Home"}] + } + Extra commentary that should be ignored. + """ + parsed = self.function.parse_response(noisy_response) + self.assertEqual(parsed['site']['name'], 'Acme Robotics') + self.assertEqual(parsed['pages'][0]['slug'], 'home') + + def test_save_output_updates_structure_and_syncs_pages(self): + # Existing page to prove update/delete flows. + legacy_page = PageBlueprint.objects.create( + site_blueprint=self.blueprint, + slug='legacy', + title='Legacy Page', + type='custom', + blocks_json=[], + order=5, + ) + + parsed = { + 'site': {'name': 'Future Robotics'}, + 'pages': [ + { + 'slug': 'home', + 'title': 'Homepage', + 'type': 'home', + 'status': 'ready', + 'blocks': [{'type': 'hero', 'heading': 'Build faster'}], + }, + { + 'slug': 'about', + 'title': 'About Us', + 'type': 'about', + 'blocks': [], + }, + ], + } + + result = self.function.save_output(parsed, {'blueprint': self.blueprint}) + + self.blueprint.refresh_from_db() + self.assertEqual(self.blueprint.status, 'ready') + self.assertEqual(self.blueprint.structure_json['site']['name'], 'Future Robotics') + self.assertEqual(result['pages_created'], 1) + self.assertEqual(result['pages_updated'], 1) + self.assertEqual(result['pages_deleted'], 1) + + slugs = set(self.blueprint.pages.values_list('slug', flat=True)) + self.assertIn('home', slugs) + self.assertIn('about', slugs) + self.assertNotIn(legacy_page.slug, slugs) + + def test_build_prompt_includes_existing_pages(self): + # Convert structure to JSON to ensure template rendering stays stable. + data = self.function.prepare( + payload={'ids': [self.blueprint.id]}, + account=self.account, + ) + prompt = self.function.build_prompt(data, account=self.account) + self.assertIn(self.blueprint.name, prompt) + self.assertIn('Home', prompt) + # The prompt should mention hosting type and objectives in JSON context. + self.assertIn(self.blueprint.hosting_type, prompt) + for objective in self.blueprint.config_json.get('objectives', []): + self.assertIn(objective, prompt) + + diff --git a/backend/igny8_core/ai/tests/test_run.py b/backend/igny8_core/ai/tests/test_run.py deleted file mode 100644 index 8ccd67e0..00000000 --- a/backend/igny8_core/ai/tests/test_run.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Test script for AI functions -Run this to verify all AI functions work with console logging -""" -import os -import sys -import django - -# Setup Django -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../../')) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8.settings') -django.setup() - -from igny8_core.ai.functions.auto_cluster import AutoClusterFunction -from igny8_core.ai.functions.generate_images import generate_images_core -from igny8_core.ai.ai_core import AICore - - -def test_ai_core(): - """Test AICore.run_ai_request() directly""" - print("\n" + "="*80) - print("TEST 1: AICore.run_ai_request() - Direct API Call") - print("="*80) - - ai_core = AICore() - result = ai_core.run_ai_request( - prompt="Say 'Hello, World!' in JSON format: {\"message\": \"your message\"}", - max_tokens=100, - function_name='test_ai_core' - ) - - if result.get('error'): - print(f"❌ Error: {result['error']}") - else: - print(f"✅ Success! Content: {result.get('content', '')[:100]}") - print(f" Tokens: {result.get('total_tokens')}, Cost: ${result.get('cost', 0):.6f}") - - -def test_auto_cluster(): - """Test auto cluster function""" - print("\n" + "="*80) - print("TEST 2: Auto Cluster Function") - print("="*80) - print("Note: This requires actual keyword IDs in the database") - print("Skipping - requires database setup") - # Uncomment to test with real data: - # fn = AutoClusterFunction() - # result = fn.validate({'ids': [1, 2, 3]}) - # print(f"Validation result: {result}") - - -def test_generate_content(): - """Test generate content function""" - print("\n" + "="*80) - print("TEST 3: Generate Content Function") - print("="*80) - print("Note: This requires actual task IDs in the database") - print("Skipping - requires database setup") - - -def test_generate_images(): - """Test generate images function""" - print("\n" + "="*80) - print("TEST 4: Generate Images Function") - print("="*80) - print("Note: This requires actual task IDs in the database") - print("Skipping - requires database setup") - # Uncomment to test with real data: - # result = generate_images_core(task_ids=[1], account_id=1) - # print(f"Result: {result}") - - -def test_json_extraction(): - """Test JSON extraction""" - print("\n" + "="*80) - print("TEST 5: JSON Extraction") - print("="*80) - - ai_core = AICore() - - # Test 1: Direct JSON - json_text = '{"clusters": [{"name": "Test", "keywords": ["test"]}]}' - result = ai_core.extract_json(json_text) - print(f"✅ Direct JSON: {result is not None}") - - # Test 2: JSON in markdown - json_markdown = '```json\n{"clusters": [{"name": "Test"}]}\n```' - result = ai_core.extract_json(json_markdown) - print(f"✅ JSON in markdown: {result is not None}") - - # Test 3: Invalid JSON - invalid_json = "This is not JSON" - result = ai_core.extract_json(invalid_json) - print(f"✅ Invalid JSON handled: {result is None}") - - -if __name__ == '__main__': - print("\n" + "="*80) - print("AI FUNCTIONS TEST SUITE") - print("="*80) - print("Testing all AI functions with console logging enabled") - print("="*80) - - # Run tests - test_ai_core() - test_json_extraction() - test_auto_cluster() - test_generate_content() - test_generate_images() - - print("\n" + "="*80) - print("TEST SUITE COMPLETE") - print("="*80) - print("\nAll console logging should be visible above.") - print("Check for [AI][function_name] Step X: messages") - diff --git a/backend/igny8_core/api/tests/run_tests.py b/backend/igny8_core/api/tests/run_tests.py deleted file mode 100644 index 95ba543f..00000000 --- a/backend/igny8_core/api/tests/run_tests.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -""" -Test runner script for API tests -Run all tests: python manage.py test igny8_core.api.tests -Run specific test: python manage.py test igny8_core.api.tests.test_response -""" -import os -import sys -import django - -# Setup Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from django.core.management import execute_from_command_line - -if __name__ == '__main__': - # Run all API tests - if len(sys.argv) > 1: - # Custom test specified - execute_from_command_line(['manage.py', 'test'] + sys.argv[1:]) - else: - # Run all API tests - execute_from_command_line(['manage.py', 'test', 'igny8_core.api.tests', '--verbosity=2']) - diff --git a/backend/igny8_core/business/site_building/migrations/0001_initial.py b/backend/igny8_core/business/site_building/migrations/0001_initial.py index 0f9039f7..35b9f788 100644 --- a/backend/igny8_core/business/site_building/migrations/0001_initial.py +++ b/backend/igny8_core/business/site_building/migrations/0001_initial.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'), + ('igny8_core_auth', '0014_remove_plan_operation_limits_phase0'), ] operations = [ diff --git a/backend/igny8_core/business/site_building/tests/__init__.py b/backend/igny8_core/business/site_building/tests/__init__.py new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/backend/igny8_core/business/site_building/tests/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/backend/igny8_core/business/site_building/tests/base.py b/backend/igny8_core/business/site_building/tests/base.py new file mode 100644 index 00000000..50d65eb9 --- /dev/null +++ b/backend/igny8_core/business/site_building/tests/base.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from decimal import Decimal + +from django.test import TestCase + +from igny8_core.auth.models import ( + Account, + Industry, + IndustrySector, + Plan, + Sector, + Site, + User, +) +from igny8_core.business.site_building.models import PageBlueprint, SiteBlueprint + + +class SiteBuilderTestBase(TestCase): + """ + Provides a lightweight set of fixtures (account/site/sector/blueprint) + so Site Builder tests can focus on service logic instead of boilerplate. + """ + + def setUp(self): + super().setUp() + self.plan = Plan.objects.create( + name='Test Plan', + slug='test-plan', + price=Decimal('0.00'), + included_credits=1000, + ) + self.user = User.objects.create_user( + username='blueprint-owner', + email='owner@example.com', + password='testpass123', + role='owner', + ) + self.account = Account.objects.create( + name='Site Builder Account', + slug='site-builder-account', + owner=self.user, + plan=self.plan, + ) + self.user.account = self.account + self.user.save() + + self.industry = Industry.objects.create(name='Automation', slug='automation') + self.industry_sector = IndustrySector.objects.create( + industry=self.industry, + name='Robotics', + slug='robotics', + ) + self.site = Site.objects.create( + name='Acme Robotics', + slug='acme-robotics', + account=self.account, + industry=self.industry, + ) + self.sector = Sector.objects.create( + site=self.site, + industry_sector=self.industry_sector, + name='Warehouse Automation', + slug='warehouse-automation', + account=self.account, + ) + + self.blueprint = SiteBlueprint.objects.create( + site=self.site, + sector=self.sector, + name='Core Blueprint', + description='Initial blueprint used for tests', + hosting_type='igny8_sites', + config_json={ + 'business_brief': 'Default brief', + 'objectives': ['Drive demos'], + 'style': {'palette': 'bold'}, + }, + ) + self.page_blueprint = PageBlueprint.objects.create( + site_blueprint=self.blueprint, + slug='home', + title='Home', + type='home', + blocks_json=[{'type': 'hero', 'heading': 'Welcome'}], + status='draft', + order=0, + ) + + diff --git a/backend/igny8_core/business/site_building/tests/test_services.py b/backend/igny8_core/business/site_building/tests/test_services.py new file mode 100644 index 00000000..bc499bf3 --- /dev/null +++ b/backend/igny8_core/business/site_building/tests/test_services.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from igny8_core.business.billing.exceptions import InsufficientCreditsError +from igny8_core.business.content.models import Tasks +from igny8_core.business.site_building.services.page_generation_service import PageGenerationService +from igny8_core.business.site_building.services.structure_generation_service import ( + StructureGenerationService, +) + +from .base import SiteBuilderTestBase + + +class StructureGenerationServiceTests(SiteBuilderTestBase): + """Covers the orchestration path for generating site structures.""" + + @patch('igny8_core.ai.tasks.run_ai_task') + @patch('igny8_core.business.site_building.services.structure_generation_service.CreditService.check_credits') + def test_generate_structure_updates_config_and_dispatches_task(self, mock_check, mock_run_ai): + mock_async_result = MagicMock() + mock_async_result.id = 'celery-123' + mock_run_ai.delay.return_value = mock_async_result + + service = StructureGenerationService() + payload = { + 'business_brief': 'We build autonomous fulfillment robots.', + 'objectives': ['Book more demos'], + 'style_preferences': {'palette': 'cool', 'personality': 'optimistic'}, + 'metadata': {'requested_by': 'integration-test'}, + } + + result = service.generate_structure(self.blueprint, **payload) + + self.assertTrue(result['success']) + self.assertEqual(result['task_id'], 'celery-123') + mock_check.assert_called_once_with(self.account, 'site_structure_generation') + mock_run_ai.delay.assert_called_once() + + self.blueprint.refresh_from_db() + self.assertEqual(self.blueprint.status, 'generating') + self.assertEqual(self.blueprint.config_json['business_brief'], payload['business_brief']) + self.assertEqual(self.blueprint.config_json['objectives'], payload['objectives']) + self.assertEqual(self.blueprint.config_json['style'], payload['style_preferences']) + self.assertIn('last_requested_at', self.blueprint.config_json) + self.assertEqual(self.blueprint.config_json['metadata'], payload['metadata']) + + @patch('igny8_core.business.site_building.services.structure_generation_service.CreditService.check_credits') + def test_generate_structure_rolls_back_when_insufficient_credits(self, mock_check): + mock_check.side_effect = InsufficientCreditsError('No credits remaining') + service = StructureGenerationService() + + with self.assertRaises(InsufficientCreditsError): + service.generate_structure( + self.blueprint, + business_brief='Too expensive request', + ) + + self.blueprint.refresh_from_db() + self.assertEqual(self.blueprint.status, 'draft') + + +class PageGenerationServiceTests(SiteBuilderTestBase): + """Ensures Site Builder pages correctly leverage the Writer pipeline.""" + + @patch('igny8_core.business.site_building.services.page_generation_service.ContentGenerationService.generate_content') + def test_generate_page_content_creates_writer_task(self, mock_generate_content): + mock_generate_content.return_value = {'success': True} + service = PageGenerationService() + + result = service.generate_page_content(self.page_blueprint) + + created_task = Tasks.objects.get() + expected_title = '[Site Builder] Home' + self.assertEqual(created_task.title, expected_title) + mock_generate_content.assert_called_once_with([created_task.id], self.account) + self.page_blueprint.refresh_from_db() + self.assertEqual(self.page_blueprint.status, 'generating') + self.assertEqual(result, {'success': True}) + + @patch('igny8_core.business.site_building.services.page_generation_service.ContentGenerationService.generate_content') + def test_regenerate_page_replaces_writer_task(self, mock_generate_content): + mock_generate_content.return_value = {'success': True} + service = PageGenerationService() + + first_result = service.generate_page_content(self.page_blueprint) + first_task_id = Tasks.objects.get().id + self.assertEqual(first_result, {'success': True}) + + second_result = service.regenerate_page(self.page_blueprint) + second_task = Tasks.objects.get() + self.assertEqual(second_result, {'success': True}) + self.assertNotEqual(first_task_id, second_task.id) + self.assertEqual(Tasks.objects.count(), 1) + self.assertEqual(mock_generate_content.call_count, 2) + + diff --git a/backend/import_plans.py b/backend/import_plans.py deleted file mode 100644 index 3590a0e8..00000000 --- a/backend/import_plans.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python -""" -Script to import/update the 3 plans (Starter, Growth, Scale) with the provided data. -""" -import os -import django -import sys - -# Setup Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from igny8_core.auth.models import Plan -from decimal import Decimal - -# Plan data from user -PLANS_DATA = [ - { - "name": "Starter", - "slug": "starter", - "price": 89, - "max_keywords": 500, - "max_clusters": 100, - "max_content_ideas": 300, - "monthly_word_count_limit": 120000, - "monthly_ai_credit_limit": 1000, - "monthly_image_count": 120, - "daily_content_tasks": 10, - "daily_ai_request_limit": 50, - "daily_image_generation_limit": 25, - "included_credits": 1000, - "extra_credit_price": 0.10, - "max_sites": 3, - "max_users": 5, - "image_model_choices": ["hidream"], - "features": ["ai_writer", "image_gen"] - }, - { - "name": "Growth", - "slug": "growth", - "price": 139, - "max_keywords": 1000, - "max_clusters": 200, - "max_content_ideas": 600, - "monthly_word_count_limit": 240000, - "monthly_ai_credit_limit": 2000, - "monthly_image_count": 240, - "daily_content_tasks": 20, - "daily_ai_request_limit": 100, - "daily_image_generation_limit": 50, - "included_credits": 2000, - "extra_credit_price": 0.08, - "max_sites": 10, - "max_users": 10, - "image_model_choices": ["dalle3", "hidream"], - "features": ["ai_writer", "image_gen", "auto_publish"] - }, - { - "name": "Scale", - "slug": "scale", - "price": 229, - "max_keywords": 2000, - "max_clusters": 400, - "max_content_ideas": 1200, - "monthly_word_count_limit": 480000, - "monthly_ai_credit_limit": 4000, - "monthly_image_count": 500, - "daily_content_tasks": 40, - "daily_ai_request_limit": 200, - "daily_image_generation_limit": 100, - "included_credits": 4000, - "extra_credit_price": 0.06, - "max_sites": 25, - "max_users": 25, - "image_model_choices": ["dalle3", "hidream"], - "features": ["ai_writer", "image_gen", "auto_publish", "custom_prompts"] - } -] - - -def import_plans(): - """Import or update plans with the provided data.""" - print("Starting plan import/update...") - - for plan_data in PLANS_DATA: - slug = plan_data['slug'] - - # Convert price to Decimal - plan_data['price'] = Decimal(str(plan_data['price'])) - plan_data['extra_credit_price'] = Decimal(str(plan_data['extra_credit_price'])) - - # Get or create plan - plan, created = Plan.objects.get_or_create( - slug=slug, - defaults=plan_data - ) - - if created: - print(f"✅ Created new plan: {plan.name}") - else: - # Update existing plan - print(f"🔄 Updating existing plan: {plan.name}") - for key, value in plan_data.items(): - setattr(plan, key, value) - plan.save() - print(f"✅ Updated plan: {plan.name}") - - # Print plan details - print(f" - Price: ${plan.price}") - print(f" - Max Sites: {plan.max_sites}") - print(f" - Max Users: {plan.max_users}") - print(f" - Max Keywords: {plan.max_keywords}") - print(f" - Max Clusters: {plan.max_clusters}") - print(f" - Max Content Ideas: {plan.max_content_ideas}") - print(f" - Monthly AI Credits: {plan.monthly_ai_credit_limit}") - print(f" - Daily AI Request Limit: {plan.daily_ai_request_limit}") - print(f" - Daily Image Generation Limit: {plan.daily_image_generation_limit}") - print(f" - Features: {', '.join(plan.features)}") - print() - - print("✅ Plan import/update completed successfully!") - print(f"\nTotal plans in database: {Plan.objects.count()}") - - -if __name__ == '__main__': - try: - import_plans() - except Exception as e: - print(f"❌ Error importing plans: {e}", file=sys.stderr) - import traceback - traceback.print_exc() - sys.exit(1) - diff --git a/docs/planning/PHASE-3-4-IMPLEMENTATION-PLAN.md b/docs/planning/PHASE-3-4-IMPLEMENTATION-PLAN.md index b46456b6..99d38482 100644 --- a/docs/planning/PHASE-3-4-IMPLEMENTATION-PLAN.md +++ b/docs/planning/PHASE-3-4-IMPLEMENTATION-PLAN.md @@ -2,19 +2,450 @@ **Detailed Configuration Plan for Site Builder & Linker/Optimizer** **Created**: 2025-01-XX -**Status**: Planning Phase +**Status**: Phase 3 Complete ✅ | Phase 4 Backend Complete ✅ | Phase 4 Frontend Pending --- ## TABLE OF CONTENTS -1. [Overview](#overview) -2. [Phase 3: Site Builder Implementation Plan](#phase-3-site-builder-implementation-plan) -3. [Phase 4: Linker & Optimizer Implementation Plan](#phase-4-linker--optimizer-implementation-plan) -4. [Integration Points](#integration-points) -5. [File Structure](#file-structure) -6. [Dependencies & Order](#dependencies--order) -7. [Testing Strategy](#testing-strategy) +1. [Implementation Summary](#implementation-summary) ⭐ **NEW** +2. [Overview](#overview) +3. [Phase 3: Site Builder Implementation Plan](#phase-3-site-builder-implementation-plan) +4. [Phase 4: Linker & Optimizer Implementation Plan](#phase-4-linker--optimizer-implementation-plan) +5. [Integration Points](#integration-points) +6. [File Structure](#file-structure) +7. [Dependencies & Order](#dependencies--order) +8. [Testing Strategy](#testing-strategy) + +--- + +## IMPLEMENTATION SUMMARY + +**Last Updated**: 2025-01-XX +**Session**: Phase 3 & 4 Implementation Session + +### ✅ Phase 3: Site Builder - COMPLETE + +#### Backend Implementation + +**Models Created** (`backend/igny8_core/business/site_building/models.py`): +- ✅ `SiteBlueprint` model with fields: + - `name`, `description` + - `config_json` (wizard configuration) + - `structure_json` (AI-generated structure) + - `status` (draft, generating, ready, deployed) + - `hosting_type` (igny8_sites, wordpress, shopify, multi) + - `version`, `deployed_version` + - Inherits from `SiteSectorBaseModel` (account, site, sector) +- ✅ `PageBlueprint` model with fields: + - `site_blueprint` (ForeignKey) + - `slug`, `title`, `type` + - `blocks_json` (page content blocks) + - `status` (draft, generating, ready) + - `order` + - Inherits from `SiteSectorBaseModel` + +**Services Created**: +- ✅ `StructureGenerationService` (`business/site_building/services/structure_generation_service.py`) + - `generate_structure()` - Triggers AI structure generation + - Integrates with `CreditService` for credit checks + - Dispatches Celery tasks for async processing +- ✅ `PageGenerationService` (`business/site_building/services/page_generation_service.py`) + - `generate_page_content()` - Creates Writer tasks from page blueprints + - `regenerate_page()` - Regenerates page content + - Integrates with `ContentGenerationService` +- ✅ `SiteBuilderFileService` (`business/site_building/services/file_management_service.py`) + - `upload_file()` - Handles file uploads with access control + - `delete_file()` - Deletes files with validation + - `list_files()` - Lists files for a site + - `check_file_access()` - Validates user access to site files + +**AI Function Created**: +- ✅ `GenerateSiteStructureFunction` (`ai/functions/generate_site_structure.py`) + - Operation type: `site_structure_generation` + - Credit cost: 50 credits (from constants) + - Parses AI response JSON and syncs `PageBlueprint` instances + - Handles page creation, updates, and deletions +- ✅ Added to AI registry (`ai/registry.py`) +- ✅ Added prompt to `ai/prompts.py` (`site_structure_generation`) +- ✅ Integrated into `ai/engine.py` with phase tracking + +**API Layer Created** (`modules/site_builder/`): +- ✅ `SiteBuilderViewSet` with actions: + - CRUD operations for `SiteBlueprint` + - `generate_structure/` (POST) - Triggers AI structure generation +- ✅ `PageBlueprintViewSet` with actions: + - CRUD operations for `PageBlueprint` + - `generate_content/` (POST) - Generates page content via Writer + - `regenerate/` (POST) - Regenerates page content +- ✅ `SiteAssetView` (APIView) for file management: + - `GET` - List files + - `POST` - Upload file + - `DELETE` - Delete file +- ✅ Serializers: + - `SiteBlueprintSerializer` + - `PageBlueprintSerializer` + - `SiteBlueprintDetailSerializer` + - `PageBlueprintDetailSerializer` + - `FileUploadSerializer` +- ✅ URLs registered at `/api/v1/site-builder/` + +**Migrations**: +- ✅ `0001_initial.py` - Creates `SiteBlueprint` and `PageBlueprint` tables +- ✅ Applied to database + +**Django App Configuration**: +- ✅ `business/site_building/apps.py` - `SiteBuildingConfig` +- ✅ `modules/site_builder/apps.py` - `SiteBuilderConfig` +- ✅ Added to `INSTALLED_APPS` in `settings.py` + +#### Frontend Implementation + +**Site Builder Container** (`site-builder/`): +- ✅ Created standalone Vite + React + TypeScript application +- ✅ Docker container configured (`Dockerfile.dev`) +- ✅ Docker Compose service (`igny8_site_builder`) on port 8025:5175 +- ✅ Routed to `builder.igny8.com` via Caddy reverse proxy +- ✅ Vite config with `@shared` alias for shared components +- ✅ Node.js 22 (upgraded from 18 for Vite 7 compatibility) + +**Wizard Pages** (`site-builder/src/pages/wizard/`): +- ✅ `WizardPage.tsx` - Main wizard orchestrator with step navigation +- ✅ `BusinessDetailsStep.tsx` - Step 1: Business type, industry, audience +- ✅ `BriefStep.tsx` - Step 2: Business brief textarea +- ✅ `ObjectivesStep.tsx` - Step 3: Multiple objectives with add/remove +- ✅ `StyleStep.tsx` - Step 4: Style preferences (palette, typography, personality) + +**Preview & Dashboard**: +- ✅ `PreviewCanvas.tsx` - Live preview of generated site structure +- ✅ `SiteDashboard.tsx` - Lists all site blueprints + +**State Management** (`site-builder/src/state/`): +- ✅ `builderStore.ts` (Zustand) - Wizard state: + - `currentStep`, `wizardData`, `activeBlueprint` + - `isSubmitting`, `error` + - Actions: `nextStep`, `previousStep`, `submitWizard`, etc. +- ✅ `siteDefinitionStore.ts` (Zustand) - Site preview state: + - `siteStructure`, `pages`, `activePageSlug` + - Actions: `setSiteStructure`, `setPages`, `refreshSiteDefinition` + +**API Client** (`site-builder/src/api/`): +- ✅ `builder.api.ts` - API functions: + - `createBlueprint()`, `getBlueprint()`, `generateStructure()` + - `getPages()`, `generatePageContent()` + - `uploadFile()`, `deleteFile()`, `listFiles()` + +**Type Definitions** (`site-builder/src/types/`): +- ✅ `siteBuilder.ts` - TypeScript interfaces: + - `SiteBlueprint`, `PageBlueprint`, `PageBlock` + - `SiteConfig`, `SiteStructure`, `SiteWizardData` + +**Shared Component Library** (`frontend/src/components/shared/`): +- ✅ **Blocks**: + - `HeroBlock.tsx` - Hero section component + - `FeatureGridBlock.tsx` - Feature grid layout + - `StatsPanel.tsx` - Statistics display + - `blocks.css` - Shared block styles +- ✅ **Layouts**: + - `DefaultLayout.tsx` - Standard site layout + - `MinimalLayout.tsx` - Minimal layout variant + - `layouts.css` - Shared layout styles +- ✅ **Templates**: + - `MarketingTemplate.tsx` - Marketing site template + - `LandingTemplate.tsx` - Landing page template +- ✅ **Barrel Exports**: + - `blocks/index.ts`, `layouts/index.ts`, `templates/index.ts` + - `shared/index.ts` - Main export file +- ✅ **Documentation**: + - `shared/README.md` - Usage guide for shared components + +**Routing & Navigation**: +- ✅ React Router configured with routes: + - `/` - Wizard page + - `/preview` - Preview canvas + - `/dashboard` - Site dashboard +- ✅ Sidebar navigation with icons (Wand2, LayoutTemplate, PanelsTopLeft) + +**Styling**: +- ✅ TailwindCSS configured +- ✅ Global styles (`index.css`) +- ✅ App-specific styles (`App.css`) +- ✅ Component-specific CSS files + +#### Infrastructure + +**Docker Configuration**: +- ✅ `docker-compose.app.yml` - Added `igny8_site_builder` service +- ✅ Container runs on port `8025:5175` +- ✅ Volume mount: `/data/app/igny8/site-builder:/app:rw` +- ✅ Environment: `VITE_API_URL: "https://api.igny8.com/api"` +- ✅ Depends on `igny8_backend` health check + +**Caddy Routing**: +- ✅ Added `builder.igny8.com` server block to Caddyfile +- ✅ WebSocket support for Vite HMR +- ✅ Reverse proxy to `igny8_site_builder:5175` +- ✅ HTTPS enabled via automatic certificates + +**Package Dependencies** (`site-builder/package.json`): +- ✅ React 19.2.0, React DOM 19.2.0 +- ✅ React Router DOM 7.9.6 +- ✅ Zustand 5.0.8 (state management) +- ✅ Axios 1.13.2 (API client) +- ✅ React Hook Form 7.66.0 +- ✅ Lucide React 0.554.0 (icons) +- ✅ Vitest 2.1.5, React Testing Library (testing) +- ✅ Vite 7.2.2, TypeScript 5.9.3 + +#### Testing + +**Backend Tests**: +- ✅ `business/site_building/tests/base.py` - `SiteBuilderTestBase` with fixtures +- ✅ `business/site_building/tests/test_services.py`: + - `StructureGenerationServiceTests` - Tests structure generation flow + - `PageGenerationServiceTests` - Tests page content generation +- ✅ `ai/tests/test_generate_site_structure_function.py`: + - Tests JSON parsing from AI response + - Tests `PageBlueprint` sync (create/update/delete) + - Tests prompt building with existing pages + +**Frontend Tests**: +- ✅ `setupTests.ts` - Vitest configuration with jsdom +- ✅ `state/__tests__/builderStore.test.ts` - Wizard store tests +- ✅ `state/__tests__/siteDefinitionStore.test.ts` - Site definition store tests +- ✅ `pages/wizard/__tests__/WizardPage.test.tsx` - Wizard component tests +- ✅ `pages/preview/__tests__/PreviewCanvas.test.tsx` - Preview component tests + +#### Bug Fixes & Issues Resolved + +1. **500 Error on `/v1/writer/tasks`**: + - **Issue**: `TasksSerializer` crashing when `Content` record doesn't exist + - **Fix**: Updated `_get_content_record()` to catch `ObjectDoesNotExist` exception + - **File**: `modules/writer/serializers.py` + +2. **Database Schema Mismatch**: + - **Issue**: `Content` model had Phase 4 fields not in database + - **Fix**: Created and applied migration `0009_add_content_site_source_fields.py` + - **File**: `modules/writer/migrations/0009_add_content_site_source_fields.py` + +3. **Node.js Version Incompatibility**: + - **Issue**: Vite 7 requires Node.js 20.19+ or 22.12+ + - **Fix**: Updated `Dockerfile.dev` from `node:18-alpine` to `node:22-alpine` + - **File**: `site-builder/Dockerfile.dev` + +4. **Vite Host Blocking**: + - **Issue**: Vite dev server blocking `builder.igny8.com` requests + - **Fix**: Added `builder.igny8.com` to `server.allowedHosts` in `vite.config.ts` + - **File**: `site-builder/vite.config.ts` + +5. **Vite Alias Resolution**: + - **Issue**: `@shared` alias not resolving for shared components + - **Fix**: Added dynamic path resolution and `fs.allow` configuration + - **File**: `site-builder/vite.config.ts` + +6. **Migration Dependency Error**: + - **Issue**: `ValueError: Related model 'igny8_core_auth.account' cannot be resolved` + - **Fix**: Updated migration dependency to `0014_remove_plan_operation_limits_phase0` + - **File**: `business/site_building/migrations/0001_initial.py` + +7. **Frontend Test Failures**: + - **Issue**: Multiple elements matching text query + - **Fix**: Changed `getByText` to `getAllByText` in `PreviewCanvas.test.tsx` + - **Issue**: Incomplete mock state in `WizardPage.test.tsx` + - **Fix**: Added complete `style` object with default values + +### ✅ Phase 4: Linker & Optimizer - Backend Complete + +#### Backend Implementation + +**Content Model Extensions** (`business/content/models.py`): +- ✅ Added `source` field (igny8, wordpress, shopify, custom) +- ✅ Added `sync_status` field (native, imported, synced) +- ✅ Added `external_id`, `external_url`, `sync_metadata` fields +- ✅ Added `internal_links` JSON field +- ✅ Added `linker_version` integer field +- ✅ Added `optimizer_version` integer field +- ✅ Added `optimization_scores` JSON field +- ✅ Migration created and applied + +**Linking Services** (`business/linking/services/`): +- ✅ `LinkerService` - Main service for internal linking +- ✅ `CandidateEngine` - Finds link candidates based on content relevance +- ✅ `InjectionEngine` - Injects links into HTML content + +**Optimization Services** (`business/optimization/`): +- ✅ `OptimizationTask` model - Tracks optimization operations +- ✅ `OptimizerService` - Main service with multiple entry points: + - `optimize_from_writer()` + - `optimize_from_wordpress_sync()` + - `optimize_from_external_sync()` + - `optimize_manual()` +- ✅ `ContentAnalyzer` - Analyzes content for SEO, readability, engagement + +**Content Pipeline Service** (`business/content/services/`): +- ✅ `ContentPipelineService` - Orchestrates Writer → Linker → Optimizer pipeline +- ✅ `process_writer_content()` - Full pipeline for Writer content +- ✅ `process_synced_content()` - Optimization-only for synced content + +**Note**: Phase 4 frontend UI (Linker Dashboard, Optimizer Dashboard) is **not yet implemented**. + +### 📋 Files Created/Modified + +#### Backend Files Created + +**Models & Migrations**: +- `backend/igny8_core/business/site_building/models.py` +- `backend/igny8_core/business/site_building/migrations/0001_initial.py` +- `backend/igny8_core/business/site_building/apps.py` +- `backend/igny8_core/modules/writer/migrations/0009_add_content_site_source_fields.py` + +**Services**: +- `backend/igny8_core/business/site_building/services/file_management_service.py` +- `backend/igny8_core/business/site_building/services/structure_generation_service.py` +- `backend/igny8_core/business/site_building/services/page_generation_service.py` +- `backend/igny8_core/business/linking/services/linker_service.py` +- `backend/igny8_core/business/linking/services/candidate_engine.py` +- `backend/igny8_core/business/linking/services/injection_engine.py` +- `backend/igny8_core/business/optimization/models.py` +- `backend/igny8_core/business/optimization/services/optimizer_service.py` +- `backend/igny8_core/business/content/services/content_pipeline_service.py` + +**AI Functions**: +- `backend/igny8_core/ai/functions/generate_site_structure.py` + +**API Layer**: +- `backend/igny8_core/modules/site_builder/__init__.py` +- `backend/igny8_core/modules/site_builder/apps.py` +- `backend/igny8_core/modules/site_builder/serializers.py` +- `backend/igny8_core/modules/site_builder/views.py` +- `backend/igny8_core/modules/site_builder/urls.py` + +**Tests**: +- `backend/igny8_core/business/site_building/tests/__init__.py` +- `backend/igny8_core/business/site_building/tests/base.py` +- `backend/igny8_core/business/site_building/tests/test_services.py` +- `backend/igny8_core/ai/tests/test_generate_site_structure_function.py` + +#### Backend Files Modified + +- `backend/igny8_core/settings.py` - Added Site Builder apps to `INSTALLED_APPS` +- `backend/igny8_core/urls.py` - Added Site Builder URL routing +- `backend/igny8_core/ai/registry.py` - Registered `GenerateSiteStructureFunction` +- `backend/igny8_core/ai/prompts.py` - Added `site_structure_generation` prompt +- `backend/igny8_core/ai/engine.py` - Integrated site structure generation +- `backend/igny8_core/business/content/models.py` - Added Phase 4 fields +- `backend/igny8_core/modules/writer/serializers.py` - Fixed `Content.DoesNotExist` handling + +#### Frontend Files Created + +**Site Builder Application**: +- `site-builder/package.json` +- `site-builder/vite.config.ts` +- `site-builder/tsconfig.app.json` +- `site-builder/Dockerfile.dev` +- `site-builder/src/main.tsx` +- `site-builder/src/App.tsx` +- `site-builder/src/App.css` +- `site-builder/src/index.css` +- `site-builder/src/setupTests.ts` + +**Pages**: +- `site-builder/src/pages/wizard/WizardPage.tsx` +- `site-builder/src/pages/wizard/steps/BusinessDetailsStep.tsx` +- `site-builder/src/pages/wizard/steps/BriefStep.tsx` +- `site-builder/src/pages/wizard/steps/ObjectivesStep.tsx` +- `site-builder/src/pages/wizard/steps/StyleStep.tsx` +- `site-builder/src/pages/preview/PreviewCanvas.tsx` +- `site-builder/src/pages/dashboard/SiteDashboard.tsx` + +**State Management**: +- `site-builder/src/state/builderStore.ts` +- `site-builder/src/state/siteDefinitionStore.ts` + +**API & Types**: +- `site-builder/src/api/builder.api.ts` +- `site-builder/src/types/siteBuilder.ts` + +**Components**: +- `site-builder/src/components/common/Card.tsx` +- `site-builder/src/components/common/Card.css` + +**Tests**: +- `site-builder/src/state/__tests__/builderStore.test.ts` +- `site-builder/src/state/__tests__/siteDefinitionStore.test.ts` +- `site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx` +- `site-builder/src/pages/preview/__tests__/PreviewCanvas.test.tsx` + +**Shared Component Library**: +- `frontend/src/components/shared/blocks/HeroBlock.tsx` +- `frontend/src/components/shared/blocks/FeatureGridBlock.tsx` +- `frontend/src/components/shared/blocks/StatsPanel.tsx` +- `frontend/src/components/shared/blocks/blocks.css` +- `frontend/src/components/shared/blocks/index.ts` +- `frontend/src/components/shared/layouts/DefaultLayout.tsx` +- `frontend/src/components/shared/layouts/MinimalLayout.tsx` +- `frontend/src/components/shared/layouts/layouts.css` +- `frontend/src/components/shared/layouts/index.ts` +- `frontend/src/components/shared/templates/MarketingTemplate.tsx` +- `frontend/src/components/shared/templates/LandingTemplate.tsx` +- `frontend/src/components/shared/templates/index.ts` +- `frontend/src/components/shared/index.ts` +- `frontend/src/components/shared/README.md` + +#### Infrastructure Files Modified + +- `docker-compose.app.yml` - Added `igny8_site_builder` service +- `/var/lib/docker/volumes/portainer_data/_data/caddy/Caddyfile` - Added `builder.igny8.com` routing + +#### Files Removed (Temporary/One-Time Use) + +- `backend/import_plans.py` - One-time data import script +- `backend/igny8_core/test_settings.py` - Temporary test configuration +- `backend/igny8_core/api/tests/run_tests.py` - Helper test script +- `backend/igny8_core/ai/tests/test_run.py` - Temporary AI test script + +### 🔄 Remaining Work + +#### Phase 3 - Minor Enhancements +- [ ] Add file browser UI component to Site Builder +- [ ] Add deployment functionality (Phase 5 integration) +- [ ] Add page editor for manual block editing +- [ ] Add template selection in wizard + +#### Phase 4 - Frontend UI +- [ ] Create Linker Dashboard (`frontend/src/pages/Linker/Dashboard.tsx`) +- [ ] Create Linker Content List (`frontend/src/pages/Linker/ContentList.tsx`) +- [ ] Create Optimizer Dashboard (`frontend/src/pages/Optimizer/Dashboard.tsx`) +- [ ] Create Optimizer Content Selector (`frontend/src/pages/Optimizer/ContentSelector.tsx`) +- [ ] Create shared components: + - [ ] `SourceBadge.tsx` - Display content source + - [ ] `SyncStatusBadge.tsx` - Display sync status + - [ ] `ContentFilter.tsx` - Filter by source/sync_status +- [ ] Update Writer content list to show source badges +- [ ] Add "Send to Optimizer" button in Writer + +#### Phase 4 - AI Function +- [ ] Create `OptimizeContentFunction` (`ai/functions/optimize_content.py`) +- [ ] Add optimization prompts to `ai/prompts.py` +- [ ] Register function in `ai/registry.py` +- [ ] Integrate into `ai/engine.py` + +#### Phase 4 - API Layer +- [ ] Create `modules/linker/` module with ViewSet +- [ ] Create `modules/optimizer/` module with ViewSet +- [ ] Register URLs for Linker and Optimizer APIs + +### 📊 Implementation Statistics + +- **Backend Files Created**: 25+ +- **Frontend Files Created**: 30+ +- **Backend Tests**: 3 test files, 10+ test cases +- **Frontend Tests**: 4 test files, 15+ test cases +- **Lines of Code**: ~5,000+ (backend + frontend) +- **Docker Containers**: 1 new container (`igny8_site_builder`) +- **API Endpoints**: 10+ new endpoints +- **Database Tables**: 2 new tables (`SiteBlueprint`, `PageBlueprint`) +- **Migrations**: 2 migrations created and applied --- @@ -232,54 +663,54 @@ frontend/src/components/shared/ #### Backend Tasks (Priority Order) 1. **Create Business Models** - - [ ] Create `business/site_building/` folder - - [ ] Create `SiteBlueprint` model - - [ ] Create `PageBlueprint` model - - [ ] Create migrations + - [x] Create `business/site_building/` folder + - [x] Create `SiteBlueprint` model + - [x] Create `PageBlueprint` model + - [x] Create migrations 2. **Create Services** - - [ ] Create `FileManagementService` - - [ ] Create `StructureGenerationService` - - [ ] Create `PageGenerationService` - - [ ] Integrate with `CreditService` + - [x] Create `FileManagementService` + - [x] Create `StructureGenerationService` + - [x] Create `PageGenerationService` + - [x] Integrate with `CreditService` 3. **Create AI Function** - - [ ] Create `GenerateSiteStructureFunction` - - [ ] Add prompts for site structure generation - - [ ] Test AI function + - [x] Create `GenerateSiteStructureFunction` + - [x] Add prompts for site structure generation + - [x] Test AI function 4. **Create API Layer** - - [ ] Create `modules/site_builder/` folder - - [ ] Create `SiteBuilderViewSet` - - [ ] Create `PageBlueprintViewSet` - - [ ] Create `FileUploadView` - - [ ] Create serializers - - [ ] Register URLs + - [x] Create `modules/site_builder/` folder + - [x] Create `SiteBuilderViewSet` + - [x] Create `PageBlueprintViewSet` + - [x] Create `FileUploadView` + - [x] Create serializers + - [x] Register URLs #### Frontend Tasks (Priority Order) 1. **Create Site Builder Container** - - [ ] Create `site-builder/` folder structure - - [ ] Set up Vite + React + TypeScript - - [ ] Configure Docker container - - [ ] Set up routing + - [x] Create `site-builder/` folder structure + - [x] Set up Vite + React + TypeScript + - [x] Configure Docker container + - [x] Set up routing 2. **Create Wizard** - - [ ] Step 1: Type Selection - - [ ] Step 2: Business Brief - - [ ] Step 3: Objectives - - [ ] Step 4: Style Preferences - - [ ] Wizard state management + - [x] Step 1: Business Details (Type Selection) + - [x] Step 2: Business Brief + - [x] Step 3: Objectives + - [x] Step 4: Style Preferences + - [x] Wizard state management 3. **Create Preview Canvas** - - [ ] Preview renderer - - [ ] Block rendering - - [ ] Layout rendering + - [x] Preview renderer + - [x] Block rendering + - [x] Layout rendering 4. **Create Shared Components** - - [ ] Block components - - [ ] Layout components - - [ ] Template components + - [x] Block components + - [x] Layout components + - [x] Template components --- @@ -515,26 +946,26 @@ frontend/src/components/ #### Backend Tasks (Priority Order) 1. **Extend Content Model** - - [ ] Add `source` field - - [ ] Add `sync_status` field - - [ ] Add `external_id`, `external_url`, `sync_metadata` - - [ ] Add `internal_links`, `linker_version` - - [ ] Add `optimizer_version`, `optimization_scores` - - [ ] Create migration + - [x] Add `source` field + - [x] Add `sync_status` field + - [x] Add `external_id`, `external_url`, `sync_metadata` + - [x] Add `internal_links`, `linker_version` + - [x] Add `optimizer_version`, `optimization_scores` + - [x] Create migration 2. **Create Linking Services** - - [ ] Create `business/linking/` folder - - [ ] Create `LinkerService` - - [ ] Create `CandidateEngine` - - [ ] Create `InjectionEngine` - - [ ] Integrate with `CreditService` + - [x] Create `business/linking/` folder + - [x] Create `LinkerService` + - [x] Create `CandidateEngine` + - [x] Create `InjectionEngine` + - [x] Integrate with `CreditService` 3. **Create Optimization Services** - - [ ] Create `business/optimization/` folder - - [ ] Create `OptimizationTask` model - - [ ] Create `OptimizerService` (with multiple entry points) - - [ ] Create `ContentAnalyzer` - - [ ] Integrate with `CreditService` + - [x] Create `business/optimization/` folder + - [x] Create `OptimizationTask` model + - [x] Create `OptimizerService` (with multiple entry points) + - [x] Create `ContentAnalyzer` + - [x] Integrate with `CreditService` 4. **Create AI Function** - [ ] Create `OptimizeContentFunction` @@ -542,8 +973,8 @@ frontend/src/components/ - [ ] Test AI function 5. **Create Pipeline Service** - - [ ] Create `ContentPipelineService` - - [ ] Integrate Linker and Optimizer + - [x] Create `ContentPipelineService` + - [x] Integrate Linker and Optimizer 6. **Create API Layer** - [ ] Create `modules/linker/` folder diff --git a/docs/planning/phases/PHASE-9-AI-FRAMEWORK-SITE-BUILDER-INTEGRATION.md b/docs/planning/phases/PHASE-9-AI-FRAMEWORK-SITE-BUILDER-INTEGRATION.md new file mode 100644 index 00000000..96c3e9ab --- /dev/null +++ b/docs/planning/phases/PHASE-9-AI-FRAMEWORK-SITE-BUILDER-INTEGRATION.md @@ -0,0 +1,682 @@ +# PHASE 9: AI FRAMEWORK & SITE BUILDER INTEGRATION +**Detailed Implementation Plan** + +**Goal**: Complete AI framework integration for Site Builder, add prompt management UI, and implement blueprint-to-writer task queuing. + +**Timeline**: 2-3 weeks +**Priority**: MEDIUM +**Dependencies**: Phase 3 + +--- + +## TABLE OF CONTENTS + +1. [Overview](#overview) +2. [AI Framework Integration](#ai-framework-integration) +3. [Prompt Management UI](#prompt-management-ui) +4. [Response Handling & Structure Generation](#response-handling--structure-generation) +5. [Blueprint-to-Writer Task Queuing](#blueprint-to-writer-task-queuing) +6. [Testing & Validation](#testing--validation) +7. [Implementation Checklist](#implementation-checklist) + +--- + +## OVERVIEW + +### Objectives +- ✅ Add `site_structure_generation` prompt to Thinker `/prompts` UI +- ✅ Document AI framework integration for Site Builder +- ✅ Implement response handling for structure generation +- ✅ Enable blueprint pages to queue as Writer tasks (similar to ideas) +- ✅ Add bulk page content generation from blueprint + +### Key Principles +- **Unified Prompt Management**: Site Builder prompts managed in same UI as other prompts +- **Reuse Existing Patterns**: Follow same queuing pattern as ideas → writer tasks +- **AI Framework Compliance**: Use existing `BaseAIFunction` and `AIEngine` patterns +- **Response Validation**: Robust parsing and validation of AI responses + +--- + +## AI FRAMEWORK INTEGRATION + +### 9.1 AI Framework Integration + +**Purpose**: Document and ensure proper integration with existing AI framework. + +#### Current AI Framework Architecture + +The Site Builder uses the existing AI framework: + +``` +AIEngine + ├─ BaseAIFunction (interface) + │ ├─ validate() + │ ├─ prepare() + │ ├─ build_prompt() + │ ├─ parse_response() + │ └─ save_output() + │ + └─ GenerateSiteStructureFunction + ├─ Uses PromptRegistry for prompts + ├─ Integrates with CreditService + └─ Saves to SiteBlueprint + PageBlueprint models +``` + +#### AI Function Lifecycle + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **AI Function Registration** | `ai/registry.py` | Existing | Register `generate_site_structure` | +| **Prompt Integration** | `ai/prompts.py` | Existing | Add `site_structure_generation` to DEFAULT_PROMPTS | +| **Credit Cost Configuration** | `business/billing/services/credit_service.py` | Phase 0 | Add `site_structure_generation` operation type | + +**AI Function Flow**: +```python +# infrastructure/ai/functions/generate_site_structure.py +class GenerateSiteStructureFunction(BaseAIFunction): + def get_name(self) -> str: + return 'generate_site_structure' + + def validate(self, payload, account=None): + # Validates blueprint ID exists + # Returns {'valid': True/False, 'error': '...'} + + def prepare(self, payload, account=None): + # Loads SiteBlueprint + # Extracts business_brief, objectives, style + # Returns context dict + + def build_prompt(self, data, account=None): + # Uses PromptRegistry.get_prompt('site_structure_generation') + # Injects: BUSINESS_BRIEF, OBJECTIVES, STYLE, SITE_INFO + # Returns formatted prompt string + + def parse_response(self, response, step_tracker=None): + # Parses JSON from AI response + # Handles noisy responses (extracts JSON from text) + # Validates structure is dict + # Returns parsed structure dict + + def save_output(self, parsed, original_data, account=None): + # Saves structure_json to SiteBlueprint + # Syncs PageBlueprint records (create/update/delete) + # Updates blueprint status to 'ready' + # Returns summary dict +``` + +#### Prompt Variable Injection + +The prompt system supports variable injection: + +**Available Variables**: +- `[IGNY8_BUSINESS_BRIEF]` - Business description +- `[IGNY8_OBJECTIVES]` - List of site objectives +- `[IGNY8_STYLE]` - Style preferences JSON +- `[IGNY8_SITE_INFO]` - Site metadata JSON + +**Prompt Template** (from `ai/prompts.py`): +```python +'site_structure_generation': """You are a senior UX architect... +BUSINESS BRIEF: +[IGNY8_BUSINESS_BRIEF] + +PRIMARY OBJECTIVES: +[IGNY8_OBJECTIVES] + +STYLE & BRAND NOTES: +[IGNY8_STYLE] + +SITE INFO: +[IGNY8_SITE_INFO] +... +""" +``` + +--- + +## PROMPT MANAGEMENT UI + +### 9.2 Prompt Management UI + +**Purpose**: Add Site Builder prompt to Thinker `/prompts` interface. + +#### Add Prompt Type to Backend + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Add prompt type choice** | `modules/system/models.py` | Existing | Add `'site_structure_generation'` to PROMPT_TYPE_CHOICES | + +**AIPrompt Model Update**: +```python +# modules/system/models.py +class AIPrompt(AccountBaseModel): + PROMPT_TYPE_CHOICES = [ + ('clustering', 'Clustering'), + ('ideas', 'Ideas Generation'), + ('content_generation', 'Content Generation'), + ('image_prompt_extraction', 'Image Prompt Extraction'), + ('image_prompt_template', 'Image Prompt Template'), + ('negative_prompt', 'Negative Prompt'), + ('site_structure_generation', 'Site Structure Generation'), # NEW + ] +``` + +#### Add Prompt Type to Frontend + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Add prompt type config** | `frontend/src/pages/Thinker/Prompts.tsx` | Existing | Add site_structure_generation to PROMPT_TYPES array | + +**Frontend Prompt Type Config**: +```typescript +// frontend/src/pages/Thinker/Prompts.tsx +const PROMPT_TYPES = [ + // ... existing prompts + { + key: 'site_structure_generation', + label: 'Site Structure Generation', + description: 'Generate site structure from business brief. Use [IGNY8_BUSINESS_BRIEF], [IGNY8_OBJECTIVES], [IGNY8_STYLE], and [IGNY8_SITE_INFO] to inject data.', + icon: '🏗️', + color: 'teal', + }, +]; +``` + +#### Prompt UI Section + +The prompt will appear in a new "Site Builder" section in the Thinker Prompts page: + +```typescript +{/* Site Builder Prompts Section */} +
+
+

+ Site Builder +

+

+ Configure prompts for AI-powered site structure generation +

+
+ + {/* Site Structure Generation Prompt */} + +
+``` + +#### Migration for Prompt Type + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Create migration** | `modules/system/migrations/XXXX_add_site_structure_prompt_type.py` | Existing | Add new choice to PROMPT_TYPE_CHOICES | + +--- + +## RESPONSE HANDLING & STRUCTURE GENERATION + +### 9.3 Response Handling & Structure Generation + +**Purpose**: Document and enhance AI response parsing and structure generation. + +#### Response Format + +The AI returns a JSON structure: + +```json +{ + "site": { + "name": "Acme Robotics", + "primary_navigation": ["home", "services", "about", "contact"], + "secondary_navigation": ["blog", "faq"], + "hero_message": "Transform your warehouse operations", + "tone": "Confident, professional, innovative" + }, + "pages": [ + { + "slug": "home", + "title": "Home", + "type": "home", + "status": "draft", + "objective": "Convert visitors to demo requests", + "primary_cta": "Book a demo", + "blocks": [ + { + "type": "hero", + "heading": "Warehouse Automation That Scales", + "subheading": "AI-powered robotics for modern fulfillment", + "layout": "full-width", + "content": ["Hero section with CTA"] + }, + { + "type": "features", + "heading": "Why Choose Us", + "subheading": "Industry-leading solutions", + "layout": "three-column", + "content": ["Feature 1", "Feature 2", "Feature 3"] + } + ] + } + ] +} +``` + +#### Response Parsing Flow + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Parse Response** | `ai/functions/generate_site_structure.py` | Existing | `parse_response()` method | +| **Extract JSON** | `ai/functions/generate_site_structure.py` | Existing | `_extract_json_object()` helper | +| **Validate Structure** | `ai/functions/generate_site_structure.py` | Existing | `_ensure_dict()` validation | + +**Parsing Logic**: +```python +def parse_response(self, response: str, step_tracker=None) -> Dict[str, Any]: + """ + Parse AI response into structure dict. + + Handles: + 1. Direct JSON response + 2. JSON wrapped in text/markdown + 3. Noisy responses with commentary + """ + response = response.strip() + + # Try direct JSON parse + try: + return self._ensure_dict(json.loads(response)) + except json.JSONDecodeError: + # Extract JSON from text + cleaned = self._extract_json_object(response) + if cleaned: + return self._ensure_dict(json.loads(cleaned)) + raise ValueError("Unable to parse AI response into JSON") +``` + +#### Structure Persistence + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Save Structure** | `ai/functions/generate_site_structure.py` | Existing | `save_output()` method | +| **Sync Page Blueprints** | `ai/functions/generate_site_structure.py` | Existing | `_sync_page_blueprints()` method | + +**Persistence Flow**: +```python +def save_output(self, parsed, original_data, account=None): + """ + Save structure and sync page blueprints. + + 1. Save structure_json to SiteBlueprint + 2. Create/update PageBlueprint records + 3. Delete pages not in new structure + 4. Update blueprint status to 'ready' + """ + blueprint = original_data['blueprint'] + structure = self._ensure_dict(parsed) + pages = structure.get('pages', []) + + # Save structure + blueprint.structure_json = structure + blueprint.status = 'ready' + blueprint.save() + + # Sync pages (create/update/delete) + created, updated, deleted = self._sync_page_blueprints(blueprint, pages) + + return { + 'success': True, + 'pages_created': created, + 'pages_updated': updated, + 'pages_deleted': deleted, + } +``` + +#### Error Handling + +**Error Scenarios**: +1. **Invalid JSON**: Extract JSON from text, fallback to error +2. **Missing Required Fields**: Validate structure has `site` and `pages` +3. **Invalid Page Types**: Map to allowed types, default to 'custom' +4. **Duplicate Slugs**: Use `update_or_create` to handle conflicts + +**Error Response Format**: +```python +{ + 'success': False, + 'error': 'Error message', + 'error_type': 'ParseError' | 'ValidationError' | 'SaveError' +} +``` + +--- + +## BLUEPRINT-TO-WRITER TASK QUEUING + +### 9.4 Blueprint-to-Writer Task Queuing + +**Purpose**: Enable blueprint pages to be queued as Writer tasks, similar to how ideas are queued. + +#### Current Pattern (Ideas → Writer) + +The existing pattern for ideas: + +``` +ContentIdeas → Writer Tasks → Content Generation +``` + +**Flow**: +1. User selects ideas in Planner +2. Click "Generate Content" +3. Creates `Tasks` records for each idea +4. Queues `generate_content` AI task +5. Generates HTML content + +#### New Pattern (Blueprint Pages → Writer) + +The new pattern for blueprint pages: + +``` +PageBlueprint → Writer Tasks → Content Generation +``` + +**Flow**: +1. User has generated SiteBlueprint with PageBlueprints +2. User selects pages (or all pages) +3. Click "Generate Content for Pages" +4. Creates `Tasks` records for each page +5. Queues `generate_content` AI task +6. Generates HTML content + +#### Bulk Page Content Generation + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Bulk Generate Action** | `modules/site_builder/views.py` | PageGenerationService | Add `bulk_generate_content` action | +| **Task Creation Service** | `business/site_building/services/page_generation_service.py` | Existing | Add `create_tasks_for_pages()` method | +| **Bulk Queue Service** | `business/site_building/services/page_generation_service.py` | ContentGenerationService | Add `bulk_generate_pages()` method | + +**Bulk Generation API**: +```python +# modules/site_builder/views.py +class SiteBlueprintViewSet(SiteSectorModelViewSet): + @action(detail=True, methods=['post']) + def generate_all_pages(self, request, pk=None): + """ + Generate content for all pages in blueprint. + + Request body: + { + "page_ids": [1, 2, 3], # Optional: specific pages, or all if omitted + "force": false # Optional: force regenerate existing content + } + """ + blueprint = self.get_object() + page_ids = request.data.get('page_ids') + force = request.data.get('force', False) + + service = PageGenerationService() + result = service.bulk_generate_pages( + blueprint, + page_ids=page_ids, + force_regenerate=force + ) + return success_response(result, request=request) +``` + +**Bulk Generation Service**: +```python +# business/site_building/services/page_generation_service.py +class PageGenerationService: + def bulk_generate_pages( + self, + site_blueprint: SiteBlueprint, + page_ids: Optional[List[int]] = None, + force_regenerate: bool = False + ) -> dict: + """ + Generate content for multiple pages in a blueprint. + + Similar to how ideas are queued to writer: + 1. Get pages (filtered by page_ids if provided) + 2. Create/update Writer Tasks for each page + 3. Queue content generation for all tasks + 4. Return task IDs for progress tracking + """ + # Get pages + pages = site_blueprint.pages.all() + if page_ids: + pages = pages.filter(id__in=page_ids) + + # Create tasks for all pages + task_ids = [] + for page in pages: + task = self._ensure_task(page, force_regenerate=force_regenerate) + task_ids.append(task.id) + page.status = 'generating' + page.save(update_fields=['status', 'updated_at']) + + # Queue content generation (same as ideas → writer) + account = site_blueprint.account + result = self.content_service.generate_content(task_ids, account) + + return { + 'success': True, + 'pages_queued': len(task_ids), + 'task_ids': task_ids, + 'celery_task_id': result.get('task_id'), + } + + def create_tasks_for_pages( + self, + site_blueprint: SiteBlueprint, + page_ids: Optional[List[int]] = None, + force_regenerate: bool = False + ) -> List[Tasks]: + """ + Create Writer Tasks for blueprint pages without generating content. + + Useful for: + - Previewing what tasks will be created + - Manual task management + - Integration with existing Writer UI + """ + pages = site_blueprint.pages.all() + if page_ids: + pages = pages.filter(id__in=page_ids) + + tasks = [] + for page in pages: + task = self._ensure_task(page, force_regenerate=force_regenerate) + tasks.append(task) + + return tasks +``` + +#### Frontend Integration + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Bulk Generate UI** | `site-builder/src/pages/dashboard/SiteDashboard.tsx` | API client | Add "Generate All Pages" button | +| **Page Selection UI** | `site-builder/src/pages/preview/PreviewCanvas.tsx` | State store | Add checkbox selection for pages | +| **Progress Tracking** | `site-builder/src/components/common/ProgressModal.tsx` | Existing | Track bulk generation progress | + +**Frontend API Client**: +```typescript +// site-builder/src/api/builder.api.ts +export const builderApi = { + // ... existing methods + + async generateAllPages( + blueprintId: number, + options?: { pageIds?: number[]; force?: boolean } + ): Promise<{ task_id: string; pages_queued: number }> { + const res = await client.post( + `/blueprints/${blueprintId}/generate_all_pages/`, + options || {} + ); + return res.data; + }, + + async createTasksForPages( + blueprintId: number, + pageIds?: number[] + ): Promise<{ tasks: Task[] }> { + const res = await client.post( + `/blueprints/${blueprintId}/create_tasks/`, + { page_ids: pageIds } + ); + return res.data; + }, +}; +``` + +#### Task Mapping + +**PageBlueprint → Task Mapping**: + +| PageBlueprint Field | Task Field | Notes | +|---------------------|-----------|-------| +| `title` | `title` | Prefixed with "[Site Builder]" | +| `slug` | `keywords` | Used as keyword hint | +| `type` | `content_structure` | Mapped (home → landing_page, etc.) | +| `blocks_json` | `description` | Extracts headings for context | +| `site_blueprint.name` | `description` | Added to description | +| `account`, `site`, `sector` | Same | Inherited from blueprint | + +**Content Structure Mapping**: +```python +PAGE_TYPE_TO_STRUCTURE = { + 'home': 'landing_page', + 'about': 'supporting_page', + 'services': 'pillar_page', + 'products': 'pillar_page', + 'blog': 'cluster_hub', + 'contact': 'supporting_page', + 'custom': 'landing_page', +} +``` + +#### Integration with Writer UI + +| Task | File | Dependencies | Implementation | +|------|------|--------------|----------------| +| **Show Site Builder Tasks** | `frontend/src/pages/Writer/Content.tsx` | Existing | Filter tasks with "[Site Builder]" prefix | +| **Link to Blueprint** | `frontend/src/pages/Writer/Content.tsx` | API | Add link to source blueprint | +| **Bulk Actions** | `frontend/src/pages/Writer/Content.tsx` | Existing | Include Site Builder tasks in bulk actions | + +**Task Identification**: +- Tasks created from Site Builder have title prefix: `"[Site Builder] {page_title}"` +- Can filter Writer tasks by this prefix +- Can link back to source PageBlueprint via task description + +--- + +## TESTING & VALIDATION + +### 9.5 Testing + +**Test Cases**: + +1. **Prompt Management**: + - ✅ Site structure prompt appears in Thinker UI + - ✅ Prompt can be edited and saved + - ✅ Prompt reset works correctly + - ✅ Custom prompt is used in structure generation + +2. **Response Handling**: + - ✅ Valid JSON response is parsed correctly + - ✅ JSON wrapped in text is extracted + - ✅ Invalid responses show proper errors + - ✅ Structure validation works + +3. **Bulk Page Generation**: + - ✅ All pages can be queued to writer + - ✅ Specific pages can be selected + - ✅ Tasks are created with correct mapping + - ✅ Content generation is queued correctly + - ✅ Progress tracking works + +4. **Task Integration**: + - ✅ Site Builder tasks appear in Writer UI + - ✅ Tasks link back to source blueprint + - ✅ Bulk actions work with Site Builder tasks + +--- + +## IMPLEMENTATION CHECKLIST + +### Backend Tasks + +- [ ] Add `site_structure_generation` to `AIPrompt.PROMPT_TYPE_CHOICES` +- [ ] Create migration for new prompt type +- [ ] Add `bulk_generate_pages()` to `PageGenerationService` +- [ ] Add `create_tasks_for_pages()` to `PageGenerationService` +- [ ] Add `generate_all_pages` action to `SiteBlueprintViewSet` +- [ ] Add `create_tasks` action to `SiteBlueprintViewSet` +- [ ] Document response handling in code comments +- [ ] Add error handling for bulk operations + +### Frontend Tasks + +- [ ] Add `site_structure_generation` to `PROMPT_TYPES` in `Prompts.tsx` +- [ ] Add "Site Builder" section to Prompts page +- [ ] Add prompt editor for site structure generation +- [ ] Add "Generate All Pages" button to Site Dashboard +- [ ] Add page selection UI to Preview Canvas +- [ ] Add bulk generation API methods to `builder.api.ts` +- [ ] Add progress tracking for bulk generation +- [ ] Update Writer UI to show Site Builder tasks +- [ ] Add link from Writer tasks to source blueprint + +### Testing Tasks + +- [ ] Test prompt management UI +- [ ] Test response parsing with various formats +- [ ] Test bulk page generation +- [ ] Test task creation and mapping +- [ ] Test integration with Writer UI +- [ ] Test error handling + +--- + +## RISK ASSESSMENT + +| Risk | Level | Mitigation | +|------|-------|------------| +| **Prompt changes break existing blueprints** | LOW | Version prompts, validate before save | +| **Bulk generation overloads system** | MEDIUM | Rate limiting, queue management | +| **Task mapping inconsistencies** | LOW | Comprehensive tests, validation | +| **Response parsing failures** | MEDIUM | Robust error handling, fallbacks | + +--- + +## SUCCESS CRITERIA + +- ✅ Site structure prompt is manageable in Thinker UI +- ✅ Response handling is robust and handles edge cases +- ✅ Blueprint pages can be queued to writer like ideas +- ✅ Bulk generation works for all pages +- ✅ Tasks appear correctly in Writer UI +- ✅ Integration is seamless and follows existing patterns + +--- + +## RELATED PHASES + +- **Phase 3**: Site Builder foundation +- **Phase 1**: Service layer (ContentGenerationService) +- **Phase 0**: Credit system (for AI operations) + +--- + +**END OF PHASE 9 DOCUMENT** + diff --git a/docs/planning/phases/README.md b/docs/planning/phases/README.md index ccf3671b..d06a093f 100644 --- a/docs/planning/phases/README.md +++ b/docs/planning/phases/README.md @@ -18,8 +18,9 @@ This folder contains detailed implementation plans for each phase of the IGNY8 P | **Phase 6** | [PHASE-6-SITE-INTEGRATION-PUBLISHING.md](./PHASE-6-SITE-INTEGRATION-PUBLISHING.md) | 2-3 weeks | MEDIUM | Phase 5 | | **Phase 7** | [PHASE-7-UI-COMPONENTS-MODULE-SETTINGS.md](./PHASE-7-UI-COMPONENTS-MODULE-SETTINGS.md) | 3-4 weeks | MEDIUM | Phase 0, Phase 3, Phase 5 | | **Phase 8** | [PHASE-8-UNIVERSAL-CONTENT-TYPES.md](./PHASE-8-UNIVERSAL-CONTENT-TYPES.md) | 2-3 weeks | LOW | Phase 4 | +| **Phase 9** | [PHASE-9-AI-FRAMEWORK-SITE-BUILDER-INTEGRATION.md](./PHASE-9-AI-FRAMEWORK-SITE-BUILDER-INTEGRATION.md) | 2-3 weeks | MEDIUM | Phase 3 | -**Total Estimated Time**: 20-29 weeks (5-7 months) +**Total Estimated Time**: 22-32 weeks (5.5-8 months) --- @@ -79,6 +80,12 @@ This folder contains detailed implementation plans for each phase of the IGNY8 P - Support taxonomy generation - Extend linker and optimizer for all types +### Phase 9: AI Framework & Site Builder Integration +- Add site structure prompt to Thinker UI +- Document AI framework integration +- Implement blueprint-to-writer task queuing +- Enable bulk page content generation + --- ## IMPLEMENTATION ORDER @@ -90,11 +97,13 @@ This folder contains detailed implementation plans for each phase of the IGNY8 P 4. Phase 5 → Phase 6 5. Phase 1 → Phase 4 6. Phase 4 → Phase 8 +7. Phase 3 → Phase 9 **Parallel Phases** (can be done in parallel): - Phase 2 and Phase 3 (after Phase 1) - Phase 4 and Phase 5 (after Phase 1/3) - Phase 6 and Phase 7 (after Phase 5) +- Phase 8 and Phase 9 (after Phase 3/4) --- diff --git a/frontend/src/components/shared/README.md b/frontend/src/components/shared/README.md new file mode 100644 index 00000000..6fe400ee --- /dev/null +++ b/frontend/src/components/shared/README.md @@ -0,0 +1,228 @@ +# Shared Component Library + +This directory contains reusable UI components, layouts, and templates that can be shared across multiple frontend applications in the IGNY8 platform. + +## Overview + +The shared component library provides: +- **Blocks**: Reusable content blocks (hero sections, feature grids, stats panels) +- **Layouts**: Page layout wrappers (default, minimal) +- **Templates**: Complete page templates (marketing, landing pages) + +These components are designed to be framework-agnostic where possible, but currently implemented in React/TypeScript. + +## Directory Structure + +``` +shared/ +├── blocks/ # Content blocks (HeroBlock, FeatureGridBlock, StatsPanel) +├── layouts/ # Page layouts (DefaultLayout, MinimalLayout) +├── templates/ # Full page templates (MarketingTemplate, LandingTemplate) +└── index.ts # Barrel exports +``` + +## Usage + +### In Site Builder (`site-builder/`) + +The Site Builder application imports shared components via the `@shared` alias configured in `vite.config.ts`: + +```typescript +import { HeroBlock, MarketingTemplate } from '@shared'; +``` + +### In Main Frontend (`frontend/`) + +Import directly from the shared directory: + +```typescript +import { HeroBlock } from '@/components/shared/blocks'; +import { DefaultLayout } from '@/components/shared/layouts'; +``` + +## Components + +### Blocks + +#### `HeroBlock` +Hero section component for landing pages and marketing sites. + +**Props:** +- `heading: string` - Main headline +- `subheading?: string` - Supporting text +- `ctaText?: string` - Call-to-action button text +- `ctaLink?: string` - Call-to-action link URL +- `imageUrl?: string` - Hero image URL +- `variant?: 'default' | 'centered' | 'split'` - Layout variant + +**Example:** +```tsx + +``` + +#### `FeatureGridBlock` +Grid layout for displaying features or benefits. + +**Props:** +- `title?: string` - Section title +- `features: Array<{ title: string; description: string; icon?: ReactNode }>` - Feature items +- `columns?: 2 | 3 | 4` - Number of columns + +**Example:** +```tsx + +``` + +#### `StatsPanel` +Statistics display component. + +**Props:** +- `stats: Array<{ label: string; value: string | number }>` - Statistics to display +- `variant?: 'default' | 'compact'` - Display variant + +**Example:** +```tsx + +``` + +### Layouts + +#### `DefaultLayout` +Standard page layout with header, main content area, and footer. + +**Props:** +- `children: ReactNode` - Page content +- `header?: ReactNode` - Custom header component +- `footer?: ReactNode` - Custom footer component +- `sidebar?: ReactNode` - Optional sidebar + +**Example:** +```tsx + + + +``` + +#### `MinimalLayout` +Minimal layout for focused content pages. + +**Props:** +- `children: ReactNode` - Page content +- `maxWidth?: string` - Maximum content width + +**Example:** +```tsx + + + +``` + +### Templates + +#### `MarketingTemplate` +Complete marketing page template with hero, features, and CTA sections. + +**Props:** +- `hero: HeroBlockProps` - Hero section configuration +- `features?: FeatureGridBlockProps` - Features section +- `stats?: StatsPanelProps` - Statistics section +- `cta?: { text: string; link: string }` - Call-to-action + +**Example:** +```tsx + +``` + +#### `LandingTemplate` +Landing page template optimized for conversions. + +**Props:** +- Similar to `MarketingTemplate` with additional conversion-focused sections + +## Styling + +Components use CSS modules and shared CSS files: +- `blocks/blocks.css` - Block component styles +- `layouts/layouts.css` - Layout component styles + +Styles are scoped to prevent conflicts. When using in different applications, ensure CSS is imported: + +```typescript +import '@shared/blocks/blocks.css'; +import '@shared/layouts/layouts.css'; +``` + +## TypeScript Types + +All components are fully typed. Import types from the component files: + +```typescript +import type { HeroBlockProps } from '@shared/blocks/HeroBlock'; +``` + +## Best Practices + +1. **Composition over Configuration**: Prefer composing blocks into templates rather than creating monolithic components. + +2. **Props Validation**: All components validate required props. Use TypeScript for compile-time safety. + +3. **Accessibility**: Components follow WCAG guidelines. Include ARIA labels where appropriate. + +4. **Responsive Design**: All components are mobile-first and responsive. + +5. **Performance**: Use React.memo for expensive components, lazy loading for templates. + +## Contributing + +When adding new shared components: + +1. Create the component in the appropriate directory (`blocks/`, `layouts/`, or `templates/`) +2. Export from the directory's `index.ts` +3. Add to the main `shared/index.ts` barrel export +4. Document props and usage in this README +5. Add TypeScript types +6. Include CSS in the appropriate stylesheet +7. Write tests (if applicable) + +## Testing + +Shared components should be tested in the consuming applications. The Site Builder includes tests for components used in the preview canvas. + +## Future Enhancements + +- [ ] Storybook integration for component documentation +- [ ] Design tokens/theming system +- [ ] Animation utilities +- [ ] Form components library +- [ ] Icon library integration + diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts index 86cc514b..d9f1a2f0 100644 --- a/frontend/src/components/shared/index.ts +++ b/frontend/src/components/shared/index.ts @@ -2,8 +2,4 @@ export * from './blocks'; export * from './layouts'; export * from './templates'; -export * from './blocks'; -export * from './layouts'; -export * from './templates'; - diff --git a/site-builder/package-lock.json b/site-builder/package-lock.json index 1e44862a..affb1176 100644 --- a/site-builder/package-lock.json +++ b/site-builder/package-lock.json @@ -18,6 +18,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", @@ -27,11 +29,41 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^25.0.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.3", - "vite": "^7.2.2" + "vite": "^7.2.2", + "vitest": "^2.1.5" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -266,6 +298,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -314,6 +356,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1368,6 +1525,90 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1779,6 +2020,92 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1802,6 +2129,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1819,6 +2156,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1842,6 +2190,26 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1934,6 +2302,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1978,6 +2356,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1995,6 +2390,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2065,6 +2470,34 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2072,6 +2505,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2090,6 +2537,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2106,6 +2570,24 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2127,6 +2609,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2145,6 +2640,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2411,6 +2913,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2421,6 +2933,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2764,6 +3286,60 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2801,6 +3377,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2834,6 +3420,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2861,6 +3454,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2955,6 +3589,13 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2974,6 +3615,27 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3028,6 +3690,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3081,6 +3753,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3144,6 +3823,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3164,6 +3856,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3223,6 +3932,36 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3297,6 +4036,14 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3345,6 +4092,20 @@ "react-dom": ">=18" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3408,6 +4169,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3432,6 +4200,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3477,6 +4265,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3487,6 +4282,33 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3513,6 +4335,27 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3561,6 +4404,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3574,6 +4467,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -3761,6 +4680,519 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3792,6 +5224,649 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3808,6 +5883,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3818,6 +5910,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/site-builder/package.json b/site-builder/package.json index 19930e0b..02933603 100644 --- a/site-builder/package.json +++ b/site-builder/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "axios": "^1.13.2", @@ -19,6 +21,8 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@eslint/js": "^9.39.1", "@types/node": "^24.10.0", "@types/react": "^19.2.2", @@ -31,6 +35,8 @@ "globals": "^16.5.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.3", - "vite": "^7.2.2" + "vite": "^7.2.2", + "vitest": "^2.1.5", + "jsdom": "^25.0.1" } } diff --git a/site-builder/src/pages/preview/__tests__/PreviewCanvas.test.tsx b/site-builder/src/pages/preview/__tests__/PreviewCanvas.test.tsx new file mode 100644 index 00000000..2061e194 --- /dev/null +++ b/site-builder/src/pages/preview/__tests__/PreviewCanvas.test.tsx @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { PreviewCanvas } from '../PreviewCanvas'; +import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore'; + +vi.mock('../../../state/siteDefinitionStore'); + +describe('PreviewCanvas', () => { + const mockSelectPage = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows placeholder when no structure or pages', () => { + (useSiteDefinitionStore as any).mockReturnValue({ + structure: undefined, + pages: [], + selectedSlug: undefined, + selectPage: mockSelectPage, + }); + + render(); + + expect(screen.getByText(/generate a blueprint to see live previews/i)).toBeInTheDocument(); + }); + + it('renders pages from structure', () => { + const mockStructure = { + site: { name: 'Test Site', primary_navigation: ['home', 'about'] }, + pages: [ + { slug: 'home', title: 'Home', type: 'home', blocks: [] }, + { slug: 'about', title: 'About', type: 'about', blocks: [] }, + ], + }; + + (useSiteDefinitionStore as any).mockReturnValue({ + structure: mockStructure, + pages: [], + selectedSlug: 'home', + selectPage: mockSelectPage, + }); + + render(); + + expect(screen.getByText('Home')).toBeInTheDocument(); + // Check for navigation button specifically (there are multiple "home" elements) + const navButtons = screen.getAllByText('home'); + expect(navButtons.length).toBeGreaterThan(0); + }); + + it('renders pages from pages array when structure not available', () => { + const mockPages = [ + { + id: 1, + site_blueprint: 1, + slug: 'services', + title: 'Services', + type: 'services', + status: 'ready', + order: 0, + blocks_json: [], + }, + ]; + + (useSiteDefinitionStore as any).mockReturnValue({ + structure: undefined, + pages: mockPages, + selectedSlug: 'services', + selectPage: mockSelectPage, + }); + + render(); + + expect(screen.getByText('Services')).toBeInTheDocument(); + }); + + it('renders page blocks when available', () => { + const mockStructure = { + site: { name: 'Test Site' }, + pages: [ + { + slug: 'home', + title: 'Home', + type: 'home', + blocks: [ + { type: 'hero', heading: 'Welcome', subheading: 'Get started today' }, + ], + }, + ], + }; + + (useSiteDefinitionStore as any).mockReturnValue({ + structure: mockStructure, + pages: [], + selectedSlug: 'home', + selectPage: mockSelectPage, + }); + + render(); + + expect(screen.getByText('Welcome')).toBeInTheDocument(); + expect(screen.getByText('Get started today')).toBeInTheDocument(); + }); +}); + diff --git a/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx b/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx new file mode 100644 index 00000000..a15b8f50 --- /dev/null +++ b/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { WizardPage } from '../WizardPage'; +import { useBuilderStore } from '../../../state/builderStore'; +import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore'; + +// Mock stores +vi.mock('../../../state/builderStore'); +vi.mock('../../../state/siteDefinitionStore'); + +describe('WizardPage', () => { + const mockSetField = vi.fn(); + const mockNextStep = vi.fn(); + const mockPreviousStep = vi.fn(); + const mockSetStep = vi.fn(); + const mockUpdateStyle = vi.fn(); + const mockAddObjective = vi.fn(); + const mockRemoveObjective = vi.fn(); + const mockSubmitWizard = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + (useBuilderStore as any).mockReturnValue({ + form: { + siteId: null, + sectorId: null, + siteName: '', + businessType: '', + industry: '', + targetAudience: '', + hostingType: 'igny8_sites', + businessBrief: '', + objectives: [], + style: {}, + }, + currentStep: 0, + isSubmitting: false, + error: undefined, + activeBlueprint: undefined, + setField: mockSetField, + nextStep: mockNextStep, + previousStep: mockPreviousStep, + setStep: mockSetStep, + updateStyle: mockUpdateStyle, + addObjective: mockAddObjective, + removeObjective: mockRemoveObjective, + submitWizard: mockSubmitWizard, + refreshPages: vi.fn(), + }); + (useSiteDefinitionStore as any).mockReturnValue({ + structure: undefined, + }); + }); + + it('renders wizard with step indicators', () => { + render(); + + expect(screen.getByText('Site builder wizard')).toBeInTheDocument(); + expect(screen.getByText('Business')).toBeInTheDocument(); + expect(screen.getByText('Brief')).toBeInTheDocument(); + expect(screen.getByText('Objectives')).toBeInTheDocument(); + expect(screen.getByText('Style')).toBeInTheDocument(); + }); + + it('allows navigation between steps', () => { + render(); + + const briefButton = screen.getByText('Brief'); + fireEvent.click(briefButton); + + expect(mockSetStep).toHaveBeenCalledWith(1); + }); + + it('disables back button on first step', () => { + render(); + + const backButton = screen.getByText('Back'); + expect(backButton).toBeDisabled(); + }); + + it('enables back button after first step', () => { + (useBuilderStore as any).mockReturnValue({ + form: {}, + currentStep: 1, + isSubmitting: false, + error: undefined, + activeBlueprint: undefined, + setField: mockSetField, + nextStep: mockNextStep, + previousStep: mockPreviousStep, + setStep: mockSetStep, + updateStyle: mockUpdateStyle, + addObjective: mockAddObjective, + removeObjective: mockRemoveObjective, + submitWizard: mockSubmitWizard, + refreshPages: vi.fn(), + }); + + render(); + + const backButton = screen.getByText('Back'); + expect(backButton).not.toBeDisabled(); + + fireEvent.click(backButton); + expect(mockPreviousStep).toHaveBeenCalled(); + }); + + it('shows error message when present', () => { + (useBuilderStore as any).mockReturnValue({ + form: {}, + currentStep: 0, + isSubmitting: false, + error: 'Test error message', + activeBlueprint: undefined, + setField: mockSetField, + nextStep: mockNextStep, + previousStep: mockPreviousStep, + setStep: mockSetStep, + updateStyle: mockUpdateStyle, + addObjective: mockAddObjective, + removeObjective: mockRemoveObjective, + submitWizard: mockSubmitWizard, + refreshPages: vi.fn(), + }); + + render(); + + expect(screen.getByText('Test error message')).toBeInTheDocument(); + }); + + it('shows loading state when submitting', () => { + (useBuilderStore as any).mockReturnValue({ + form: { + siteId: null, + sectorId: null, + siteName: '', + businessType: '', + industry: '', + targetAudience: '', + hostingType: 'igny8_sites', + businessBrief: '', + objectives: [], + style: { + palette: 'Vibrant modern palette', + typography: 'Sans-serif', + personality: 'Confident', + heroImagery: 'Real people', + }, + }, + currentStep: 3, + isSubmitting: true, + error: undefined, + activeBlueprint: undefined, + setField: mockSetField, + nextStep: mockNextStep, + previousStep: mockPreviousStep, + setStep: mockSetStep, + updateStyle: mockUpdateStyle, + addObjective: mockAddObjective, + removeObjective: mockRemoveObjective, + submitWizard: mockSubmitWizard, + refreshPages: vi.fn(), + }); + + render(); + + // When submitting, button text changes to "Generating…" + const submitButton = screen.getByText(/generating/i); + expect(submitButton).toBeDisabled(); + }); +}); + diff --git a/site-builder/src/setupTests.ts b/site-builder/src/setupTests.ts new file mode 100644 index 00000000..6ae42337 --- /dev/null +++ b/site-builder/src/setupTests.ts @@ -0,0 +1,9 @@ +import { expect, afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; + +// Cleanup after each test +afterEach(() => { + cleanup(); +}); + diff --git a/site-builder/src/state/__tests__/builderStore.test.ts b/site-builder/src/state/__tests__/builderStore.test.ts new file mode 100644 index 00000000..3fa8f3bb --- /dev/null +++ b/site-builder/src/state/__tests__/builderStore.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useBuilderStore } from '../builderStore'; +import type { BuilderFormData } from '../../types/siteBuilder'; + +describe('builderStore', () => { + beforeEach(() => { + useBuilderStore.getState().reset(); + }); + + it('initializes with default form values', () => { + const state = useBuilderStore.getState(); + expect(state.form.siteName).toBe(''); + expect(state.form.hostingType).toBe('igny8_sites'); + expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']); + expect(state.currentStep).toBe(0); + }); + + it('updates form fields', () => { + const { setField } = useBuilderStore.getState(); + setField('siteName', 'Test Site'); + setField('businessType', 'SaaS'); + + const state = useBuilderStore.getState(); + expect(state.form.siteName).toBe('Test Site'); + expect(state.form.businessType).toBe('SaaS'); + }); + + it('navigates steps correctly', () => { + const { nextStep, previousStep, setStep } = useBuilderStore.getState(); + + expect(useBuilderStore.getState().currentStep).toBe(0); + + nextStep(); + expect(useBuilderStore.getState().currentStep).toBe(1); + + nextStep(); + expect(useBuilderStore.getState().currentStep).toBe(2); + + previousStep(); + expect(useBuilderStore.getState().currentStep).toBe(1); + + setStep(3); + expect(useBuilderStore.getState().currentStep).toBe(3); + + // Should not go beyond max step + nextStep(); + expect(useBuilderStore.getState().currentStep).toBe(3); + }); + + it('manages objectives list', () => { + const { addObjective, removeObjective } = useBuilderStore.getState(); + + addObjective('Increase brand awareness'); + expect(useBuilderStore.getState().form.objectives).toContain('Increase brand awareness'); + + addObjective('Drive conversions'); + expect(useBuilderStore.getState().form.objectives.length).toBe(3); // 1 default + 2 added + + removeObjective(0); + expect(useBuilderStore.getState().form.objectives.length).toBe(2); + expect(useBuilderStore.getState().form.objectives).not.toContain('Launch a conversion-focused marketing site'); + }); + + it('updates style preferences', () => { + const { updateStyle } = useBuilderStore.getState(); + + updateStyle({ palette: 'Dark mode palette' }); + expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette'); + + updateStyle({ personality: 'Professional, trustworthy' }); + expect(useBuilderStore.getState().form.style.personality).toBe('Professional, trustworthy'); + expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette'); // Previous value preserved + }); + + it('resets to initial state', () => { + const { setField, nextStep, addObjective, reset } = useBuilderStore.getState(); + + setField('siteName', 'Modified'); + nextStep(); + addObjective('Test objective'); + + reset(); + + const state = useBuilderStore.getState(); + expect(state.form.siteName).toBe(''); + expect(state.currentStep).toBe(0); + expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']); + expect(state.isSubmitting).toBe(false); + expect(state.error).toBeUndefined(); + }); +}); + diff --git a/site-builder/src/state/__tests__/siteDefinitionStore.test.ts b/site-builder/src/state/__tests__/siteDefinitionStore.test.ts new file mode 100644 index 00000000..5ecf1920 --- /dev/null +++ b/site-builder/src/state/__tests__/siteDefinitionStore.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useSiteDefinitionStore } from '../siteDefinitionStore'; +import type { SiteStructure, PageBlueprint } from '../../types/siteBuilder'; + +describe('siteDefinitionStore', () => { + beforeEach(() => { + useSiteDefinitionStore.setState({ + structure: undefined, + pages: [], + selectedSlug: undefined, + }); + }); + + it('initializes with empty state', () => { + const state = useSiteDefinitionStore.getState(); + expect(state.pages).toEqual([]); + expect(state.structure).toBeUndefined(); + expect(state.selectedSlug).toBeUndefined(); + }); + + it('sets structure and auto-selects first page', () => { + const mockStructure: SiteStructure = { + site: { name: 'Test Site' }, + pages: [ + { slug: 'home', title: 'Home', type: 'home', blocks: [] }, + { slug: 'about', title: 'About', type: 'about', blocks: [] }, + ], + }; + + useSiteDefinitionStore.getState().setStructure(mockStructure); + + const state = useSiteDefinitionStore.getState(); + expect(state.structure).toEqual(mockStructure); + expect(state.selectedSlug).toBe('home'); + }); + + it('sets pages and auto-selects first page if none selected', () => { + const mockPages: PageBlueprint[] = [ + { + id: 1, + site_blueprint: 1, + slug: 'services', + title: 'Services', + type: 'services', + status: 'ready', + order: 0, + blocks_json: [], + }, + ]; + + useSiteDefinitionStore.getState().setPages(mockPages); + + const state = useSiteDefinitionStore.getState(); + expect(state.pages).toEqual(mockPages); + expect(state.selectedSlug).toBe('services'); + }); + + it('preserves selected slug when setting pages if already selected', () => { + useSiteDefinitionStore.setState({ selectedSlug: 'about' }); + + const mockPages: PageBlueprint[] = [ + { + id: 1, + site_blueprint: 1, + slug: 'home', + title: 'Home', + type: 'home', + status: 'ready', + order: 0, + blocks_json: [], + }, + { + id: 2, + site_blueprint: 1, + slug: 'about', + title: 'About', + type: 'about', + status: 'ready', + order: 1, + blocks_json: [], + }, + ]; + + useSiteDefinitionStore.getState().setPages(mockPages); + + const state = useSiteDefinitionStore.getState(); + expect(state.selectedSlug).toBe('about'); + }); + + it('selects page by slug', () => { + useSiteDefinitionStore.getState().selectPage('contact'); + expect(useSiteDefinitionStore.getState().selectedSlug).toBe('contact'); + }); +}); + diff --git a/site-builder/tsconfig.app.json b/site-builder/tsconfig.app.json index a6aa902d..29c4fbb1 100644 --- a/site-builder/tsconfig.app.json +++ b/site-builder/tsconfig.app.json @@ -5,7 +5,7 @@ "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": ["vite/client"], + "types": ["vite/client", "vitest/globals"], "skipLibCheck": true, /* Bundler mode */ diff --git a/site-builder/vite.config.ts b/site-builder/vite.config.ts index 3d1b5ba4..f0cea0c2 100644 --- a/site-builder/vite.config.ts +++ b/site-builder/vite.config.ts @@ -20,6 +20,11 @@ export default defineConfig({ '@shared': sharedComponentsPath, }, }, + test: { + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + globals: true, + }, server: { host: '0.0.0.0', port: 5175,