diff --git a/.gitignore b/.gitignore index 994a50d..98cef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ coverage/ + + + diff --git a/README.md b/README.md index cb75daa..3fc1979 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ import { TemplateRegistry } from 'create-react-forge/templates'; const registry = new TemplateRegistry(); const templates = registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'tailwind' }, + styling: { solution: 'styled-components' }, stateManagement: 'zustand', testing: { enabled: true, unit: { runner: 'vitest' }, e2e: { enabled: true, runner: 'playwright' } }, dataFetching: { enabled: true }, diff --git a/src/__tests__/integration/build-verification.test.ts b/src/__tests__/integration/build-verification.test.ts index 95b370a..a26027a 100644 --- a/src/__tests__/integration/build-verification.test.ts +++ b/src/__tests__/integration/build-verification.test.ts @@ -58,7 +58,7 @@ describe('Build Verification Tests', () => { it('should generate and build a minimal Next.js project', async () => { const config = createConfig('nextjs-build-minimal', { runtime: 'nextjs', - styling: { solution: 'css' }, + styling: { solution: 'none' }, }); projectPaths.push(config.path); @@ -122,7 +122,7 @@ describe('Build Verification Tests', () => { it('should generate and build Next.js with state management', async () => { const config = createConfig('nextjs-build-zustand', { runtime: 'nextjs', - styling: { solution: 'css' }, + styling: { solution: 'none' }, stateManagement: 'zustand', }); projectPaths.push(config.path); @@ -156,7 +156,7 @@ describe('Build Verification Tests', () => { it('should generate and build a minimal Vite project', async () => { const config = createConfig('vite-build-minimal', { runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, }); projectPaths.push(config.path); diff --git a/src/__tests__/integration/generator.test.ts b/src/__tests__/integration/generator.test.ts index 2f1b067..6a8f099 100644 --- a/src/__tests__/integration/generator.test.ts +++ b/src/__tests__/integration/generator.test.ts @@ -192,11 +192,11 @@ describe('ProjectGenerator Integration', () => { expect(devDeps).toHaveProperty('@playwright/test'); }); - it.skip('should generate Next.js + CSS Modules + Redux', async () => { + it.skip('should generate Next.js + None styling + Redux', async () => { const config = createBaseConfig({ name: 'nextjs-redux', runtime: 'nextjs', - styling: { solution: 'css-modules' }, + styling: { solution: 'none' }, stateManagement: 'redux', testing: { enabled: true, @@ -231,7 +231,7 @@ describe('ProjectGenerator Integration', () => { const config = createBaseConfig({ name: 'vite-jest-pw', runtime: 'vite', - styling: { solution: 'css-modules' }, + styling: { solution: 'styled-components' }, testing: { enabled: true, unit: { enabled: true, runner: 'jest' }, diff --git a/src/__tests__/integration/package-json.test.ts b/src/__tests__/integration/package-json.test.ts index 640bf3b..44e29a5 100644 --- a/src/__tests__/integration/package-json.test.ts +++ b/src/__tests__/integration/package-json.test.ts @@ -165,9 +165,10 @@ describe('Package.json Generation', () => { expect(devDeps).toHaveProperty('autoprefixer'); }); - it.skip('should not include tailwind when css-modules selected', async () => { - const config = createConfig('css-modules-deps', { - styling: { solution: 'css-modules' }, + it.skip('should not include tailwind when none styling selected', async () => { + const config = createConfig('none-styling-deps', { + runtime: 'nextjs', + styling: { solution: 'none' }, }); projectPaths.push(config.path); diff --git a/src/__tests__/integration/scenarios.test.ts b/src/__tests__/integration/scenarios.test.ts index 4a992c6..935ea2a 100644 --- a/src/__tests__/integration/scenarios.test.ts +++ b/src/__tests__/integration/scenarios.test.ts @@ -63,10 +63,10 @@ describe('Real-World Scenarios', () => { }); describe('Scenario: Minimal SPA (Learning/Prototyping)', () => { - it('should generate minimal Vite + CSS project', async () => { + it('should generate minimal Vite + styled-components project', async () => { const config = createConfig('minimal-spa', { runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', dataFetching: { enabled: false, library: 'tanstack-query' }, testing: { @@ -230,10 +230,10 @@ describe('Real-World Scenarios', () => { }); describe.skip('Scenario: Next.js with Redux', () => { - it('should generate Next.js + CSS Modules + Redux', async () => { + it('should generate Next.js + None styling + Redux', async () => { const config = createConfig('nextjs-redux', { runtime: 'nextjs', - styling: { solution: 'css-modules' }, + styling: { solution: 'none' }, stateManagement: 'redux', dataFetching: { enabled: false, library: 'tanstack-query' }, testing: { diff --git a/src/__tests__/integration/styling-verification.test.ts b/src/__tests__/integration/styling-verification.test.ts index 0f69864..8eda968 100644 --- a/src/__tests__/integration/styling-verification.test.ts +++ b/src/__tests__/integration/styling-verification.test.ts @@ -1,8 +1,8 @@ +import { execa } from 'execa'; import { existsSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterEach, describe, expect, it } from 'vitest'; -import { execa } from 'execa'; import { ProjectConfig } from '../../config/schema.js'; import { ProjectGenerator } from '../../generator/index.js'; @@ -28,7 +28,7 @@ function createConfig(name: string, overrides: Partial): ProjectC path: basePath, runtime: 'vite', language: 'typescript', - styling: { solution: 'tailwind' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', dataFetching: { enabled: false, library: 'tanstack-query' }, testing: { @@ -53,11 +53,11 @@ describe('Styling Solutions Verification', () => { projectPaths.length = 0; }); - describe('Plain CSS', () => { - it('should generate and build with plain CSS', async () => { - const config = createConfig('css-plain', { + describe('Styled Components (Vite)', () => { + it('should generate project with Styled Components file structure', async () => { + const config = createConfig('styled-vite-structure', { runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, }); projectPaths.push(config.path); @@ -66,31 +66,17 @@ describe('Styling Solutions Verification', () => { expect(result.success).toBe(true); - // Install dependencies - console.log('Installing dependencies for plain CSS project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); - expect(installResult.exitCode).toBe(0); - - // Build the project - console.log('Building plain CSS project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 120000, - }); - expect(buildResult.exitCode).toBe(0); - - expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); - }); + // Verify Styled Components files exist + expect(existsSync(join(config.path, 'src/styles/globals.ts'))).toBe(true); + expect(existsSync(join(config.path, 'src/components/ui/Button.styled.ts'))).toBe(true); + expect(existsSync(join(config.path, 'src/app/provider.tsx'))).toBe(true); + expect(existsSync(join(config.path, 'src/main.tsx'))).toBe(true); + }, 60000); - describe.skip('CSS Modules', () => { - it('should generate and build with CSS Modules', async () => { - const config = createConfig('css-modules', { + it('should generate and build with Styled Components', async () => { + const config = createConfig('styled-vite', { runtime: 'vite', - styling: { solution: 'css-modules' }, + styling: { solution: 'styled-components' }, }); projectPaths.push(config.path); @@ -98,10 +84,11 @@ describe('Styling Solutions Verification', () => { const result = await generator.generate(); expect(result.success).toBe(true); - expect(existsSync(join(config.path, 'src/components/ui/Button.module.css'))).toBe(true); + expect(existsSync(join(config.path, 'src/styles/globals.ts'))).toBe(true); + expect(existsSync(join(config.path, 'src/components/ui/Button.styled.ts'))).toBe(true); // Install dependencies - console.log('Installing dependencies for CSS Modules project...'); + console.log('Installing dependencies for Styled Components project...'); const installResult = await execa('npm', ['install'], { cwd: config.path, timeout: 120000, @@ -109,7 +96,7 @@ describe('Styling Solutions Verification', () => { expect(installResult.exitCode).toBe(0); // Build the project - console.log('Building CSS Modules project...'); + console.log('Building Styled Components project...'); const buildResult = await execa('npm', ['run', 'build'], { cwd: config.path, timeout: 120000, @@ -120,11 +107,11 @@ describe('Styling Solutions Verification', () => { }, 300000); }); - describe.skip('Styled Components', () => { - it('should generate and build with Styled Components (Vite)', async () => { - const config = createConfig('styled-vite', { - runtime: 'vite', - styling: { solution: 'styled-components' }, + describe('Tailwind CSS (Next.js)', () => { + it('should generate and build with Tailwind', async () => { + const config = createConfig('tailwind-nextjs', { + runtime: 'nextjs', + styling: { solution: 'tailwind' }, }); projectPaths.push(config.path); @@ -132,11 +119,11 @@ describe('Styling Solutions Verification', () => { const result = await generator.generate(); expect(result.success).toBe(true); - expect(existsSync(join(config.path, 'src/styles/globals.ts'))).toBe(true); - expect(existsSync(join(config.path, 'src/components/ui/Button.styled.ts'))).toBe(true); + expect(existsSync(join(config.path, 'tailwind.config.js'))).toBe(true); + expect(existsSync(join(config.path, 'postcss.config.js'))).toBe(true); // Install dependencies - console.log('Installing dependencies for Styled Components project...'); + console.log('Installing dependencies for Tailwind project...'); const installResult = await execa('npm', ['install'], { cwd: config.path, timeout: 120000, @@ -144,20 +131,22 @@ describe('Styling Solutions Verification', () => { expect(installResult.exitCode).toBe(0); // Build the project - console.log('Building Styled Components project...'); + console.log('Building Tailwind project...'); const buildResult = await execa('npm', ['run', 'build'], { cwd: config.path, - timeout: 120000, + timeout: 180000, }); expect(buildResult.exitCode).toBe(0); - expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); + expect(existsSync(join(config.path, '.next'))).toBe(true); + }, 360000); + }); - it('should generate and build with Styled Components (Next.js)', async () => { - const config = createConfig('styled-nextjs', { + describe('None Styling (Next.js)', () => { + it('should generate project with no styling framework', async () => { + const config = createConfig('none-nextjs-structure', { runtime: 'nextjs', - styling: { solution: 'styled-components' }, + styling: { solution: 'none' }, }); projectPaths.push(config.path); @@ -165,33 +154,20 @@ describe('Styling Solutions Verification', () => { const result = await generator.generate(); expect(result.success).toBe(true); - expect(existsSync(join(config.path, 'src/styles/globals.ts'))).toBe(true); - // Install dependencies - console.log('Installing dependencies for Next.js + Styled Components...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); - expect(installResult.exitCode).toBe(0); + // Verify no Tailwind config files + expect(existsSync(join(config.path, 'tailwind.config.js'))).toBe(false); + expect(existsSync(join(config.path, 'postcss.config.js'))).toBe(false); - // Build the project - console.log('Building Next.js + Styled Components...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, - }); - expect(buildResult.exitCode).toBe(0); - - expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); - }); + // Verify base files exist + expect(existsSync(join(config.path, 'src/app/page.tsx'))).toBe(true); + expect(existsSync(join(config.path, 'src/styles/globals.css'))).toBe(true); + }, 60000); - describe('Tailwind CSS', () => { - it('should generate and build with Tailwind', async () => { - const config = createConfig('tailwind', { - runtime: 'vite', - styling: { solution: 'tailwind' }, + it('should generate and build with no styling framework', async () => { + const config = createConfig('none-nextjs', { + runtime: 'nextjs', + styling: { solution: 'none' }, }); projectPaths.push(config.path); @@ -199,11 +175,9 @@ describe('Styling Solutions Verification', () => { const result = await generator.generate(); expect(result.success).toBe(true); - expect(existsSync(join(config.path, 'tailwind.config.js'))).toBe(true); - expect(existsSync(join(config.path, 'postcss.config.js'))).toBe(true); // Install dependencies - console.log('Installing dependencies for Tailwind project...'); + console.log('Installing dependencies for None styling project...'); const installResult = await execa('npm', ['install'], { cwd: config.path, timeout: 120000, @@ -211,15 +185,14 @@ describe('Styling Solutions Verification', () => { expect(installResult.exitCode).toBe(0); // Build the project - console.log('Building Tailwind project...'); + console.log('Building None styling project...'); const buildResult = await execa('npm', ['run', 'build'], { cwd: config.path, - timeout: 120000, + timeout: 180000, }); expect(buildResult.exitCode).toBe(0); - expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); + expect(existsSync(join(config.path, '.next'))).toBe(true); + }, 360000); }); }); - diff --git a/src/__tests__/integration/template-loading.test.ts b/src/__tests__/integration/template-loading.test.ts index b509719..58eb31f 100644 --- a/src/__tests__/integration/template-loading.test.ts +++ b/src/__tests__/integration/template-loading.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { TemplateRegistry } from '../../templates/registry.js'; /** @@ -124,10 +124,10 @@ describe('TemplateRegistry', () => { }); describe('loadTemplatesForConfig', () => { - it('should load templates for minimal Vite config', () => { + it('should load templates for minimal Vite config with styled-components', () => { const templates = registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', testing: { enabled: false, @@ -137,14 +137,15 @@ describe('TemplateRegistry', () => { dataFetching: { enabled: false }, }); - // Should have base and runtime - expect(templates.length).toBeGreaterThanOrEqual(2); + // Should have base, runtime, and styled-components + expect(templates.length).toBeGreaterThanOrEqual(3); expect(templates.some(t => t.name === 'base')).toBe(true); + expect(templates.some(t => t.path === 'styling/styled-components')).toBe(true); }); - it('should load templates for Vite + Tailwind config', () => { + it('should load templates for Next.js + Tailwind config', () => { const templates = registry.loadTemplatesForConfig({ - runtime: 'vite', + runtime: 'nextjs', styling: { solution: 'tailwind' }, stateManagement: 'none', testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, @@ -154,10 +155,24 @@ describe('TemplateRegistry', () => { expect(templates.some(t => t.path === 'styling/tailwind')).toBe(true); }); + it('should load templates for Next.js + None styling config', () => { + const templates = registry.loadTemplatesForConfig({ + runtime: 'nextjs', + styling: { solution: 'none' }, + stateManagement: 'none', + testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, + dataFetching: { enabled: false }, + }); + + // Should NOT load any styling template for 'none' + expect(templates.some(t => t.path === 'styling/tailwind')).toBe(false); + expect(templates.some(t => t.path === 'styling/styled-components')).toBe(false); + }); + it('should load templates for Next.js + Redux config', () => { const templates = registry.loadTemplatesForConfig({ runtime: 'nextjs', - styling: { solution: 'css' }, + styling: { solution: 'none' }, stateManagement: 'redux', testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, dataFetching: { enabled: false }, @@ -196,7 +211,7 @@ describe('TemplateRegistry', () => { it('should load testing templates when testing is enabled', () => { const templates = registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', testing: { enabled: true, @@ -213,7 +228,7 @@ describe('TemplateRegistry', () => { it('should load tanstack-query template when dataFetching enabled', () => { const templates = registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, dataFetching: { enabled: true }, @@ -226,7 +241,7 @@ describe('TemplateRegistry', () => { describe('Merged Dependencies', () => { it('should merge dependencies from multiple templates', () => { registry.loadTemplatesForConfig({ - runtime: 'vite', + runtime: 'nextjs', styling: { solution: 'tailwind' }, stateManagement: 'zustand', testing: { @@ -250,7 +265,7 @@ describe('TemplateRegistry', () => { it('should include scripts from templates', () => { registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', testing: { enabled: true, @@ -269,7 +284,7 @@ describe('TemplateRegistry', () => { describe('Merged Files', () => { it('should merge files from multiple templates', () => { registry.loadTemplatesForConfig({ - runtime: 'vite', + runtime: 'nextjs', styling: { solution: 'tailwind' }, stateManagement: 'zustand', testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, @@ -280,13 +295,6 @@ describe('TemplateRegistry', () => { expect(files.size).toBeGreaterThan(0); - // Should have files from different templates merged - // Check for vite config file - const hasViteConfig = Array.from(files.keys()).some( - path => path.includes('vite.config') - ); - expect(hasViteConfig).toBe(true); - // Check for store files from zustand const hasStoreFiles = Array.from(files.keys()).some( path => path.includes('stores') @@ -298,7 +306,7 @@ describe('TemplateRegistry', () => { // Load base first, then runtime registry.loadTemplatesForConfig({ runtime: 'vite', - styling: { solution: 'css' }, + styling: { solution: 'styled-components' }, stateManagement: 'none', testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, dataFetching: { enabled: false }, @@ -371,4 +379,3 @@ describe('TemplateRegistry', () => { }); }); }); - diff --git a/src/__tests__/styling-alignment.test.ts b/src/__tests__/styling-alignment.test.ts new file mode 100644 index 0000000..0d1139e --- /dev/null +++ b/src/__tests__/styling-alignment.test.ts @@ -0,0 +1,218 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { TemplateRegistry } from '../templates/registry.js'; + +/** + * Styling Alignment Tests + * Verifies that styling overlays properly integrate with the app structure + * by checking file contents for correct imports and usage patterns + */ + +describe('Styling Alignment', () => { + let registry: TemplateRegistry; + + beforeEach(() => { + registry = new TemplateRegistry(); + }); + + describe('Styled Components Overlay', () => { + it('should have provider.tsx for Vite that includes GlobalStyles', () => { + const template = registry.loadAndRegister('styling/styled-components', 'vite'); + const providerTsx = template.files.get('src/app/provider.tsx'); + + expect(providerTsx).toBeDefined(); + expect(providerTsx).toContain("import { GlobalStyles } from '@/styles/globals'"); + expect(providerTsx).toContain(''); + }); + + it('should have providers.tsx for Next.js with StyledComponentsRegistry', () => { + const template = registry.loadAndRegister('styling/styled-components', 'nextjs'); + const providersTsx = template.files.get('src/app/providers.tsx'); + + expect(providersTsx).toBeDefined(); + expect(providersTsx).toContain("import StyledComponentsRegistry from '@/lib/StyledComponentsRegistry'"); + expect(providersTsx).toContain("import { GlobalStyles } from '@/styles/globals'"); + expect(providersTsx).toContain(''); + expect(providersTsx).toContain(''); + }); + + it('should have layout.tsx for Next.js without CSS import', () => { + const template = registry.loadAndRegister('styling/styled-components', 'nextjs'); + const layoutTsx = template.files.get('src/app/layout.tsx'); + + expect(layoutTsx).toBeDefined(); + expect(layoutTsx).not.toContain("import '@/styles/globals.css'"); + expect(layoutTsx).toContain(''); + }); + + it('should have main.tsx for Vite without CSS import', () => { + const template = registry.loadAndRegister('styling/styled-components', 'vite'); + const mainTsx = template.files.get('src/main.tsx'); + + expect(mainTsx).toBeDefined(); + expect(mainTsx).not.toContain("import '@/styles/globals.css'"); + expect(mainTsx).toContain(''); + }); + + it('should have StyledComponentsRegistry.tsx for Next.js SSR', () => { + const template = registry.loadAndRegister('styling/styled-components', 'nextjs'); + const registryTsx = template.files.get('src/lib/StyledComponentsRegistry.tsx'); + + expect(registryTsx).toBeDefined(); + expect(registryTsx).toContain("'use client'"); + expect(registryTsx).toContain('ServerStyleSheet'); + expect(registryTsx).toContain('StyleSheetManager'); + expect(registryTsx).toContain('useServerInsertedHTML'); + }); + + it('should have globals.ts with GlobalStyles export', () => { + const template = registry.loadAndRegister('styling/styled-components'); + const globalsTs = template.files.get('src/styles/globals.ts'); + + expect(globalsTs).toBeDefined(); + expect(globalsTs).toContain('createGlobalStyle'); + expect(globalsTs).toContain('export const GlobalStyles'); + }); + + it('should have Button.styled.ts with styled component', () => { + const template = registry.loadAndRegister('styling/styled-components'); + const buttonStyled = template.files.get('src/components/ui/Button.styled.ts'); + + expect(buttonStyled).toBeDefined(); + expect(buttonStyled).toContain("import styled from 'styled-components'"); + expect(buttonStyled).toContain('export const StyledButton'); + }); + + it('should properly merge with Vite runtime', () => { + registry.loadTemplatesForConfig({ + runtime: 'vite', + styling: { solution: 'styled-components' }, + stateManagement: 'none', + testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, + dataFetching: { enabled: false }, + }); + + const files = registry.getMergedFiles(); + + // provider.tsx should include GlobalStyles + const providerTsx = files.get('src/app/provider.tsx'); + expect(providerTsx).toContain(''); + + // main.tsx should not import CSS + const mainTsx = files.get('src/main.tsx'); + expect(mainTsx).not.toContain("globals.css"); + + // Should have styled-components specific files + expect(files.has('src/styles/globals.ts')).toBe(true); + expect(files.has('src/components/ui/Button.styled.ts')).toBe(true); + }); + + it('should properly merge with Next.js runtime', () => { + registry.loadTemplatesForConfig({ + runtime: 'nextjs', + styling: { solution: 'styled-components' }, + stateManagement: 'none', + testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, + dataFetching: { enabled: false }, + }); + + const files = registry.getMergedFiles(); + + // providers.tsx should include StyledComponentsRegistry + const providersTsx = files.get('src/app/providers.tsx'); + expect(providersTsx).toContain(''); + expect(providersTsx).toContain(''); + + // layout.tsx should not import CSS + const layoutTsx = files.get('src/app/layout.tsx'); + expect(layoutTsx).not.toContain("globals.css"); + + // Should have Next.js specific registry + expect(files.has('src/lib/StyledComponentsRegistry.tsx')).toBe(true); + }); + }); + + describe('Tailwind Overlay', () => { + it('should have globals.css with Tailwind directives', () => { + const template = registry.loadAndRegister('styling/tailwind'); + const globalsCss = template.files.get('src/styles/globals.css'); + + expect(globalsCss).toBeDefined(); + expect(globalsCss).toContain('@import'); + }); + + it('should have tailwind.config.js', () => { + const template = registry.loadAndRegister('styling/tailwind'); + const tailwindConfig = template.files.get('tailwind.config.js'); + + expect(tailwindConfig).toBeDefined(); + expect(tailwindConfig).toContain('content'); + }); + + it('should have postcss.config.js', () => { + const template = registry.loadAndRegister('styling/tailwind'); + const postcssConfig = template.files.get('postcss.config.js'); + + expect(postcssConfig).toBeDefined(); + }); + + it('should properly merge with Next.js runtime', () => { + registry.loadTemplatesForConfig({ + runtime: 'nextjs', + styling: { solution: 'tailwind' }, + stateManagement: 'none', + testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, + dataFetching: { enabled: false }, + }); + + const files = registry.getMergedFiles(); + + // Should have Tailwind config files + expect(files.has('tailwind.config.js')).toBe(true); + expect(files.has('postcss.config.js')).toBe(true); + + // Should have Tailwind-specific Next.js pages + const pageTsx = files.get('src/app/page.tsx'); + expect(pageTsx).toContain('className='); + }); + }); + + describe('None Styling Option (Next.js)', () => { + it('should work without any styling overlay', () => { + registry.loadTemplatesForConfig({ + runtime: 'nextjs', + styling: { solution: 'none' }, + stateManagement: 'none', + testing: { enabled: false, e2e: { enabled: false, runner: 'none' } }, + dataFetching: { enabled: false }, + }); + + const files = registry.getMergedFiles(); + + // Should have base runtime files + expect(files.has('src/app/page.tsx')).toBe(true); + expect(files.has('src/app/layout.tsx')).toBe(true); + expect(files.has('src/styles/globals.css')).toBe(true); + + // Should NOT have Tailwind config files + expect(files.has('tailwind.config.js')).toBe(false); + expect(files.has('postcss.config.js')).toBe(false); + + // Page should use inline styles (not Tailwind classes) + const pageTsx = files.get('src/app/page.tsx'); + expect(pageTsx).toContain('style='); + }); + }); + + describe('Cross-Styling Compatibility', () => { + it('Styled Components should not have CSS file imports in Vite files', () => { + const viteTemplate = registry.loadAndRegister('styling/styled-components', 'vite'); + const providerTsx = viteTemplate.files.get('src/app/provider.tsx'); + const mainTsx = viteTemplate.files.get('src/main.tsx'); + + expect(providerTsx).toBeDefined(); + expect(mainTsx).toBeDefined(); + expect(providerTsx).not.toContain('.css'); + expect(mainTsx).not.toContain('.css'); + }); + }); +}); diff --git a/src/cli/parser.ts b/src/cli/parser.ts index b6bd22b..c73dbca 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -36,10 +36,9 @@ export function createCommand(): Command { ) .addOption( new Option('--styling ', 'Styling solution').choices([ - 'css', + 'none', 'tailwind', 'styled-components', - 'css-modules', ]) ) .addOption( diff --git a/src/config/defaults.ts b/src/config/defaults.ts index eb8d5c2..aa1fade 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -8,10 +8,9 @@ export const RUNTIME_DESCRIPTIONS: Record = { }; export const STYLING_DESCRIPTIONS: Record = { - css: 'Plain CSS with CSS modules', + none: 'None - Plain CSS classes', tailwind: 'Tailwind CSS - Utility-first framework', 'styled-components': 'Styled Components - CSS-in-JS', - 'css-modules': 'CSS Modules - Scoped styling', }; export const STATE_DESCRIPTIONS: Record = { diff --git a/src/config/schema.ts b/src/config/schema.ts index a1d1199..b078c72 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -11,7 +11,7 @@ export type Runtime = z.infer; export const LanguageSchema = z.enum(['javascript', 'typescript']); export type Language = z.infer; -export const StylingSchema = z.enum(['css', 'tailwind', 'styled-components', 'css-modules']); +export const StylingSchema = z.enum(['none', 'tailwind', 'styled-components']); export type Styling = z.infer; export const StateManagementSchema = z.enum(['none', 'redux', 'zustand', 'jotai']); @@ -61,7 +61,7 @@ export const GitConfigSchema = z.object({ export type GitConfig = z.infer; export const StylingConfigSchema = z.object({ - solution: StylingSchema.default('tailwind'), + solution: StylingSchema.default('styled-components'), }); export type StylingConfigType = z.infer; @@ -94,7 +94,7 @@ export const DEFAULT_CONFIG: ProjectConfig = { runtime: 'vite', language: 'typescript', styling: { - solution: 'tailwind', + solution: 'styled-components', }, stateManagement: 'none', dataFetching: { diff --git a/src/index.ts b/src/index.ts index bb1cea6..b0c447f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,5 @@ main().catch((error) => { + + diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 1020e59..3b21808 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -2,3 +2,6 @@ export * from './types.js'; export * from './loader.js'; export * from './manager.js'; + + + diff --git a/src/templates/overlays/base/manifest.json b/src/templates/overlays/base/manifest.json index e77a94a..6b6b720 100644 --- a/src/templates/overlays/base/manifest.json +++ b/src/templates/overlays/base/manifest.json @@ -4,13 +4,14 @@ "description": "Base shared utilities, types, and hooks following bulletproof-react patterns", "compatibleWith": ["runtime-vite", "runtime-nextjs"], "dependencies": { - "clsx": "^2.1.0" + "styled-components": "^6.1.14" + }, + "devDependencies": { + "@types/styled-components": "^5.1.34" }, - "devDependencies": {}, "scripts": {}, "filePatterns": { "include": ["**/*"], "exclude": ["manifest.json"] } } - diff --git a/src/templates/overlays/base/src/components/ui/Button.tsx b/src/templates/overlays/base/src/components/ui/Button.tsx index 12e754d..f39f7b4 100644 --- a/src/templates/overlays/base/src/components/ui/Button.tsx +++ b/src/templates/overlays/base/src/components/ui/Button.tsx @@ -1,5 +1,5 @@ import { forwardRef } from 'react'; -import { cn } from '@/lib/utils'; +import styled, { css } from 'styled-components'; export type ButtonProps = React.ButtonHTMLAttributes & { variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; @@ -7,29 +7,110 @@ export type ButtonProps = React.ButtonHTMLAttributes & { isLoading?: boolean; }; -const variants = { - primary: - 'bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:outline-indigo-600', - secondary: - 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:outline-gray-600', - outline: - 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus-visible:outline-gray-600', - ghost: - 'bg-transparent text-gray-700 hover:bg-gray-100 focus-visible:outline-gray-600', - danger: - 'bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600', +const variantStyles = { + primary: css` + background-color: #4f46e5; + color: white; + &:hover:not(:disabled) { + background-color: #6366f1; + } + `, + secondary: css` + background-color: #f3f4f6; + color: #111827; + &:hover:not(:disabled) { + background-color: #e5e7eb; + } + `, + outline: css` + background-color: transparent; + color: #374151; + border: 1px solid #d1d5db; + &:hover:not(:disabled) { + background-color: #f9fafb; + } + `, + ghost: css` + background-color: transparent; + color: #374151; + &:hover:not(:disabled) { + background-color: #f3f4f6; + } + `, + danger: css` + background-color: #dc2626; + color: white; + &:hover:not(:disabled) { + background-color: #ef4444; + } + `, }; -const sizes = { - sm: 'px-2.5 py-1.5 text-xs', - md: 'px-3.5 py-2.5 text-sm', - lg: 'px-4 py-3 text-base', +const sizeStyles = { + sm: css` + padding: 0.375rem 0.625rem; + font-size: 0.75rem; + `, + md: css` + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + `, + lg: css` + padding: 0.75rem 1rem; + font-size: 1rem; + `, }; +const StyledButton = styled.button<{ + $variant: ButtonProps['variant']; + $size: ButtonProps['size']; +}>` + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + font-weight: 600; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s, color 0.2s; + border: none; + cursor: pointer; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } + + ${(props) => variantStyles[props.$variant || 'primary']} + ${(props) => sizeStyles[props.$size || 'md']} +`; + +const LoadingSpinner = styled.span` + margin-right: 0.5rem; + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +`; + export const Button = forwardRef( ( { - className, variant = 'primary', size = 'md', isLoading = false, @@ -40,27 +121,18 @@ export const Button = forwardRef( ref ) => { return ( - + ); } ); Button.displayName = 'Button'; - diff --git a/src/templates/overlays/base/src/components/ui/Input.tsx b/src/templates/overlays/base/src/components/ui/Input.tsx index 75af930..2d8dbc3 100644 --- a/src/templates/overlays/base/src/components/ui/Input.tsx +++ b/src/templates/overlays/base/src/components/ui/Input.tsx @@ -1,51 +1,77 @@ import { forwardRef } from 'react'; -import { cn } from '@/lib/utils'; +import styled from 'styled-components'; export type InputProps = React.InputHTMLAttributes & { label?: string; error?: string; }; +const InputWrapper = styled.div` + width: 100%; +`; + +const Label = styled.label` + display: block; + margin-bottom: 0.25rem; + font-size: 0.875rem; + font-weight: 500; + color: #374151; +`; + +const StyledInput = styled.input<{ $hasError?: boolean }>` + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + color: #111827; + background-color: white; + border: 1px solid ${(props) => (props.$hasError ? '#fca5a5' : '#d1d5db')}; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: border-color 0.2s, box-shadow 0.2s; + + &::placeholder { + color: #9ca3af; + } + + &:focus { + outline: none; + border-color: ${(props) => (props.$hasError ? '#ef4444' : '#4f46e5')}; + box-shadow: 0 0 0 2px ${(props) => (props.$hasError ? '#fecaca' : '#c7d2fe')}; + } + + &:disabled { + cursor: not-allowed; + background-color: #f9fafb; + color: #6b7280; + } +`; + +const ErrorMessage = styled.p` + margin-top: 0.25rem; + font-size: 0.875rem; + color: #dc2626; +`; + export const Input = forwardRef( - ({ className, label, error, id, ...props }, ref) => { + ({ label, error, id, ...props }, ref) => { const inputId = id || props.name; return ( -
- {label && ( - - )} - + {label && } + - {error && ( -

- {error} -

- )} -
+ {error && {error}} + ); } ); Input.displayName = 'Input'; - diff --git a/src/templates/overlays/base/src/lib/utils.ts b/src/templates/overlays/base/src/lib/utils.ts index 5496507..2f549df 100644 --- a/src/templates/overlays/base/src/lib/utils.ts +++ b/src/templates/overlays/base/src/lib/utils.ts @@ -1,12 +1,3 @@ -import { clsx, type ClassValue } from 'clsx'; - -/** - * Utility function for constructing className strings conditionally - */ -export function cn(...inputs: ClassValue[]) { - return clsx(inputs); -} - /** * Format a date to a human-readable string */ @@ -31,4 +22,3 @@ export function sleep(ms: number): Promise { export function generateId(): string { return Math.random().toString(36).substring(2, 9); } - diff --git a/src/templates/overlays/runtime/nextjs/src/app/error.tsx b/src/templates/overlays/runtime/nextjs/src/app/error.tsx index c676d5a..2106e5e 100644 --- a/src/templates/overlays/runtime/nextjs/src/app/error.tsx +++ b/src/templates/overlays/runtime/nextjs/src/app/error.tsx @@ -2,6 +2,40 @@ import { useEffect } from 'react'; +const styles = { + container: { + display: 'flex', + minHeight: '100vh', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + }, + content: { + textAlign: 'center' as const, + }, + title: { + fontSize: '1.5rem', + fontWeight: 700, + color: '#dc2626', + }, + message: { + marginTop: '1rem', + color: '#4b5563', + }, + retryButton: { + marginTop: '1.5rem', + padding: '0.625rem 0.875rem', + fontSize: '0.875rem', + fontWeight: 600, + color: 'white', + backgroundColor: '#4f46e5', + border: 'none', + borderRadius: '0.375rem', + boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + cursor: 'pointer', + }, +}; + export default function Error({ error, reset, @@ -10,25 +44,20 @@ export default function Error({ reset: () => void; }) { useEffect(() => { - // Log the error to an error reporting service console.error(error); }, [error]); return ( -
-
-

Something went wrong

-

+

+
+

Something went wrong

+

{error.message || 'An unexpected error occurred'}

-
); } - diff --git a/src/templates/overlays/runtime/nextjs/src/app/loading.tsx b/src/templates/overlays/runtime/nextjs/src/app/loading.tsx index 643816f..a6e60c2 100644 --- a/src/templates/overlays/runtime/nextjs/src/app/loading.tsx +++ b/src/templates/overlays/runtime/nextjs/src/app/loading.tsx @@ -1,12 +1,30 @@ +const styles = { + container: { + display: 'flex', + minHeight: '100vh', + alignItems: 'center', + justifyContent: 'center', + }, + spinner: { + width: '2rem', + height: '2rem', + border: '2px solid #d1d5db', + borderTopColor: '#4f46e5', + borderRadius: '50%', + animation: 'spin 1s linear infinite', + }, +}; + export default function Loading() { return ( -
-
+
+ +
); } - diff --git a/src/templates/overlays/runtime/nextjs/src/app/not-found.tsx b/src/templates/overlays/runtime/nextjs/src/app/not-found.tsx index 3f877e7..a64fea7 100644 --- a/src/templates/overlays/runtime/nextjs/src/app/not-found.tsx +++ b/src/templates/overlays/runtime/nextjs/src/app/not-found.tsx @@ -1,21 +1,63 @@ import Link from 'next/link'; +const styles = { + container: { + display: 'flex', + minHeight: '100vh', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + }, + content: { + textAlign: 'center' as const, + }, + errorCode: { + fontSize: '1rem', + fontWeight: 600, + color: '#4f46e5', + }, + title: { + marginTop: '1rem', + fontSize: '1.875rem', + fontWeight: 700, + letterSpacing: '-0.025em', + }, + description: { + marginTop: '1.5rem', + fontSize: '1rem', + lineHeight: '1.75rem', + color: '#4b5563', + }, + actions: { + marginTop: '2.5rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '1.5rem', + }, + homeLink: { + padding: '0.625rem 0.875rem', + fontSize: '0.875rem', + fontWeight: 600, + color: 'white', + backgroundColor: '#4f46e5', + borderRadius: '0.375rem', + textDecoration: 'none', + boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + }, +}; + export default function NotFound() { return ( -
-
-

404

-

- Page not found -

-

+

+
+

404

+

Page not found

+

Sorry, we couldn't find the page you're looking for.

-
- +
+ Go back home
@@ -23,4 +65,3 @@ export default function NotFound() {
); } - diff --git a/src/templates/overlays/runtime/nextjs/src/app/page.tsx b/src/templates/overlays/runtime/nextjs/src/app/page.tsx index 846753b..a6514c6 100644 --- a/src/templates/overlays/runtime/nextjs/src/app/page.tsx +++ b/src/templates/overlays/runtime/nextjs/src/app/page.tsx @@ -1,25 +1,68 @@ import Link from 'next/link'; +const styles = { + container: { + display: 'flex', + minHeight: '100vh', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + }, + content: { + textAlign: 'center' as const, + }, + title: { + fontSize: '2.25rem', + fontWeight: 700, + letterSpacing: '-0.025em', + }, + description: { + marginTop: '1.5rem', + fontSize: '1.125rem', + lineHeight: '2rem', + color: '#4b5563', + }, + actions: { + marginTop: '2.5rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '1.5rem', + }, + primaryLink: { + padding: '0.625rem 0.875rem', + fontSize: '0.875rem', + fontWeight: 600, + color: 'white', + backgroundColor: '#4f46e5', + borderRadius: '0.375rem', + textDecoration: 'none', + boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + }, + secondaryLink: { + fontSize: '0.875rem', + fontWeight: 600, + lineHeight: '1.5rem', + color: 'inherit', + textDecoration: 'none', + }, +}; + export default function HomePage() { return ( -
-
-

- Welcome to Your App -

-

+

+
+

Welcome to Your App

+

A production-ready Next.js application scaffolded with create-react-forge.

-
- + ); } - diff --git a/src/templates/overlays/runtime/nextjs/src/styles/globals.css b/src/templates/overlays/runtime/nextjs/src/styles/globals.css index d9eb156..96d0ab3 100644 --- a/src/templates/overlays/runtime/nextjs/src/styles/globals.css +++ b/src/templates/overlays/runtime/nextjs/src/styles/globals.css @@ -1,4 +1,4 @@ -/* Base styles - will be enhanced by styling overlay (tailwind/css-modules) */ +/* Base styles - will be enhanced by styling overlay (tailwind) if selected */ *, *::before, diff --git a/src/templates/overlays/runtime/vite/src/components/errors/ErrorFallback.tsx b/src/templates/overlays/runtime/vite/src/components/errors/ErrorFallback.tsx index 571d760..ff7bb5f 100644 --- a/src/templates/overlays/runtime/vite/src/components/errors/ErrorFallback.tsx +++ b/src/templates/overlays/runtime/vite/src/components/errors/ErrorFallback.tsx @@ -1,21 +1,55 @@ import { FallbackProps } from 'react-error-boundary'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 1.5rem; + font-weight: 700; + color: #dc2626; +`; + +const Message = styled.p` + margin-top: 1rem; + color: #4b5563; +`; + +const RetryButton = styled.button` + margin-top: 1.5rem; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + background-color: #4f46e5; + border: none; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } +`; export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { return ( -
-
-

Something went wrong

-

- {error.message || 'An unexpected error occurred'} -

- -
-
+ + + Something went wrong + {error.message || 'An unexpected error occurred'} + Try again + + ); } - diff --git a/src/templates/overlays/runtime/vite/src/components/ui/LoadingSpinner.tsx b/src/templates/overlays/runtime/vite/src/components/ui/LoadingSpinner.tsx index 6f3db8a..5a5f74e 100644 --- a/src/templates/overlays/runtime/vite/src/components/ui/LoadingSpinner.tsx +++ b/src/templates/overlays/runtime/vite/src/components/ui/LoadingSpinner.tsx @@ -1,23 +1,43 @@ +import styled, { keyframes } from 'styled-components'; + type LoadingSpinnerProps = { size?: 'sm' | 'md' | 'lg'; - className?: string; }; +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + const sizes = { - sm: 'h-4 w-4', - md: 'h-8 w-8', - lg: 'h-16 w-16', + sm: '1rem', + md: '2rem', + lg: '4rem', }; -export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) { +const SpinnerContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +const Spinner = styled.div<{ $size: 'sm' | 'md' | 'lg' }>` + width: ${(props) => sizes[props.$size]}; + height: ${(props) => sizes[props.$size]}; + border: 2px solid #d1d5db; + border-top-color: #4f46e5; + border-radius: 50%; + animation: ${spin} 1s linear infinite; +`; + +export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) { return ( -
-
-
+ + + ); } - diff --git a/src/templates/overlays/runtime/vite/src/features/misc/routes/Landing.tsx b/src/templates/overlays/runtime/vite/src/features/misc/routes/Landing.tsx index f192b91..e4340fa 100644 --- a/src/templates/overlays/runtime/vite/src/features/misc/routes/Landing.tsx +++ b/src/templates/overlays/runtime/vite/src/features/misc/routes/Landing.tsx @@ -1,33 +1,90 @@ import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + + @media (min-width: 640px) { + font-size: 3.75rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: #4b5563; +`; + +const Actions = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryLink = styled(Link)` + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + background-color: #4f46e5; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + text-decoration: none; + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } +`; + +const SecondaryLink = styled.a` + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: inherit; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +`; export function Landing() { return ( -
+ + + + ); } - diff --git a/src/templates/overlays/runtime/vite/src/features/misc/routes/NotFound.tsx b/src/templates/overlays/runtime/vite/src/features/misc/routes/NotFound.tsx index b7c5d4c..7839f91 100644 --- a/src/templates/overlays/runtime/vite/src/features/misc/routes/NotFound.tsx +++ b/src/templates/overlays/runtime/vite/src/features/misc/routes/NotFound.tsx @@ -1,26 +1,84 @@ import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const ErrorCode = styled.p` + font-size: 1rem; + font-weight: 600; + color: #4f46e5; +`; + +const Title = styled.h1` + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + + @media (min-width: 640px) { + font-size: 3rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: #4b5563; +`; + +const Actions = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const HomeLink = styled(Link)` + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + background-color: #4f46e5; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + text-decoration: none; + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; export function NotFound() { return ( -
-
-

404

-

- Page not found -

-

+ + + 404 + Page not found + Sorry, we couldn't find the page you're looking for. -

-
- - Go back home - -
-
-
+ + + Go back home + + + ); } - diff --git a/src/templates/overlays/runtime/vite/src/main.tsx b/src/templates/overlays/runtime/vite/src/main.tsx index 807ad1b..fc10c03 100644 --- a/src/templates/overlays/runtime/vite/src/main.tsx +++ b/src/templates/overlays/runtime/vite/src/main.tsx @@ -1,5 +1,4 @@ import { App } from '@/app/App'; -import '@/styles/globals.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -8,4 +7,3 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ); - diff --git a/src/templates/overlays/runtime/vite/src/styles/globals.css b/src/templates/overlays/runtime/vite/src/styles/globals.css deleted file mode 100644 index 7ef60d4..0000000 --- a/src/templates/overlays/runtime/vite/src/styles/globals.css +++ /dev/null @@ -1,55 +0,0 @@ -/* Base styles - will be enhanced by styling overlay (tailwind/css-modules) */ - -*, -*::before, -*::after { - box-sizing: border-box; -} - -* { - margin: 0; -} - -html, -body { - height: 100%; -} - -body { - line-height: 1.5; - -webkit-font-smoothing: antialiased; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Helvetica Neue', Arial, sans-serif; -} - -img, -picture, -video, -canvas, -svg { - display: block; - max-width: 100%; -} - -input, -button, -textarea, -select { - font: inherit; -} - -p, -h1, -h2, -h3, -h4, -h5, -h6 { - overflow-wrap: break-word; -} - -#root { - isolation: isolate; - height: 100%; -} - diff --git a/src/templates/overlays/styling/css-modules/_nextjs/src/app/loading.tsx b/src/templates/overlays/styling/css-modules/_nextjs/src/app/loading.tsx new file mode 100644 index 0000000..7c4de41 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_nextjs/src/app/loading.tsx @@ -0,0 +1,13 @@ +import styles from './loading.module.css'; + +export default function Loading() { + return ( +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.module.css b/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.module.css new file mode 100644 index 0000000..bcf43e5 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.module.css @@ -0,0 +1,73 @@ +.container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.content { + text-align: center; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .title { + font-size: 3.75rem; + } +} + +.description { + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: var(--color-text-muted); +} + +.buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.primaryBtn { + display: inline-block; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; +} + +.primaryBtn:hover { + background-color: var(--color-primary-hover); +} + +.linkBtn { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: var(--color-text); + text-decoration: none; + transition: color 0.2s; +} + +.linkBtn:hover { + color: var(--color-primary); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.tsx b/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.tsx new file mode 100644 index 0000000..b396a16 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_nextjs/src/app/page.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import styles from './page.module.css'; + +export default function HomePage() { + return ( +
+
+

Welcome to Your App

+

+ A production-ready Next.js application scaffolded with create-react-forge. +

+
+ + Get started + + + Learn more + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.module.css b/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.module.css new file mode 100644 index 0000000..5512dde --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.module.css @@ -0,0 +1,45 @@ +.container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.content { + text-align: center; +} + +.title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-danger); +} + +.message { + margin-top: 1rem; + color: var(--color-text-muted); +} + +.button { + margin-top: 1.5rem; + display: inline-block; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + border: none; + cursor: pointer; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; +} + +.button:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.tsx b/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.tsx new file mode 100644 index 0000000..f5f284f --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/components/errors/ErrorFallback.tsx @@ -0,0 +1,22 @@ +import { FallbackProps } from 'react-error-boundary'; +import styles from './ErrorFallback.module.css'; + +export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + return ( +
+
+

Something went wrong

+

+ {error.message || 'An unexpected error occurred'} +

+ +
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.module.css b/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.module.css new file mode 100644 index 0000000..2210160 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.module.css @@ -0,0 +1,40 @@ +.container { + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + border-radius: 50%; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + animation: spin 1s linear infinite; +} + +.sm { + width: 1rem; + height: 1rem; +} + +.md { + width: 2rem; + height: 2rem; +} + +.lg { + width: 4rem; + height: 4rem; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.tsx b/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..c6c4f9d --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,23 @@ +import styles from './LoadingSpinner.module.css'; + +type LoadingSpinnerProps = { + size?: 'sm' | 'md' | 'lg'; +}; + +export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) { + const sizeClass = styles[size] || styles.md; + + return ( +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.module.css b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.module.css new file mode 100644 index 0000000..bcf43e5 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.module.css @@ -0,0 +1,73 @@ +.container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.content { + text-align: center; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .title { + font-size: 3.75rem; + } +} + +.description { + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: var(--color-text-muted); +} + +.buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.primaryBtn { + display: inline-block; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; +} + +.primaryBtn:hover { + background-color: var(--color-primary-hover); +} + +.linkBtn { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: var(--color-text); + text-decoration: none; + transition: color 0.2s; +} + +.linkBtn:hover { + color: var(--color-primary); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.tsx b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.tsx new file mode 100644 index 0000000..c81d768 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/Landing.tsx @@ -0,0 +1,32 @@ +import { Link } from 'react-router-dom'; +import styles from './Landing.module.css'; + +export function Landing() { + return ( +
+
+

Welcome to Your App

+

+ A production-ready React application scaffolded with create-react-forge. +

+
+ + Get started + + + Learn more + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.module.css b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.module.css new file mode 100644 index 0000000..ac2508b --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.module.css @@ -0,0 +1,67 @@ +.container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.content { + text-align: center; +} + +.code { + font-size: 1rem; + font-weight: 600; + color: var(--color-primary); +} + +.title { + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .title { + font-size: 3rem; + } +} + +.description { + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: var(--color-text-muted); +} + +.buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.primaryBtn { + display: inline-block; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; +} + +.primaryBtn:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.tsx b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.tsx new file mode 100644 index 0000000..586c422 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/_vite/src/features/misc/routes/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from 'react-router-dom'; +import styles from './NotFound.module.css'; + +export function NotFound() { + return ( +
+
+

404

+

Page not found

+

+ Sorry, we couldn't find the page you're looking for. +

+
+ + Go back home + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css-modules/manifest.json b/src/templates/overlays/styling/css-modules/manifest.json deleted file mode 100644 index 22cedee..0000000 --- a/src/templates/overlays/styling/css-modules/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "styling-css-modules", - "version": "1.0.0", - "description": "CSS Modules styling with scoped CSS", - "compatibleWith": ["runtime-vite", "runtime-nextjs"], - "dependencies": {}, - "devDependencies": {}, - "scripts": {}, - "filePatterns": { - "include": ["**/*"], - "exclude": ["manifest.json"] - } -} - diff --git a/src/templates/overlays/styling/css-modules/src/components/ui/Button.module.css b/src/templates/overlays/styling/css-modules/src/components/ui/Button.module.css deleted file mode 100644 index 5b4c8ef..0000000 --- a/src/templates/overlays/styling/css-modules/src/components/ui/Button.module.css +++ /dev/null @@ -1,87 +0,0 @@ -.button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-md); - font-weight: 600; - transition: all 0.15s ease; - cursor: pointer; - border: none; -} - -.button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Variants */ -.primary { - background-color: var(--color-primary-600); - color: white; -} - -.primary:hover:not(:disabled) { - background-color: var(--color-primary-500); -} - -.secondary { - background-color: var(--color-gray-100); - color: var(--color-gray-900); -} - -.secondary:hover:not(:disabled) { - background-color: var(--color-gray-200); -} - -.outline { - background-color: transparent; - border: 1px solid var(--color-gray-300); - color: var(--color-gray-700); -} - -.outline:hover:not(:disabled) { - background-color: var(--color-gray-50); -} - -.danger { - background-color: var(--color-red-600); - color: white; -} - -.danger:hover:not(:disabled) { - background-color: var(--color-red-500); -} - -/* Sizes */ -.sm { - padding: var(--spacing-1) var(--spacing-2); - font-size: 0.75rem; -} - -.md { - padding: var(--spacing-2) var(--spacing-4); - font-size: 0.875rem; -} - -.lg { - padding: var(--spacing-3) var(--spacing-4); - font-size: 1rem; -} - -/* Loading spinner */ -.spinner { - width: 1rem; - height: 1rem; - margin-right: var(--spacing-2); - border: 2px solid currentColor; - border-top-color: transparent; - border-radius: var(--radius-full); - animation: spin 0.6s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - diff --git a/src/templates/overlays/styling/css-modules/src/css.d.ts b/src/templates/overlays/styling/css-modules/src/css.d.ts new file mode 100644 index 0000000..22881a7 --- /dev/null +++ b/src/templates/overlays/styling/css-modules/src/css.d.ts @@ -0,0 +1,13 @@ +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module '*.css' { + const content: string; + export default content; +} + + + + diff --git a/src/templates/overlays/styling/css-modules/src/styles/globals.css b/src/templates/overlays/styling/css-modules/src/styles/globals.css deleted file mode 100644 index eedf94b..0000000 --- a/src/templates/overlays/styling/css-modules/src/styles/globals.css +++ /dev/null @@ -1,91 +0,0 @@ -/* CSS Custom Properties (Design Tokens) */ -:root { - /* Colors */ - --color-primary-50: #eff6ff; - --color-primary-100: #dbeafe; - --color-primary-500: #3b82f6; - --color-primary-600: #2563eb; - --color-primary-700: #1d4ed8; - - --color-gray-50: #f9fafb; - --color-gray-100: #f3f4f6; - --color-gray-200: #e5e7eb; - --color-gray-300: #d1d5db; - --color-gray-400: #9ca3af; - --color-gray-500: #6b7280; - --color-gray-600: #4b5563; - --color-gray-700: #374151; - --color-gray-800: #1f2937; - --color-gray-900: #111827; - - --color-red-500: #ef4444; - --color-red-600: #dc2626; - - /* Typography */ - --font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', - Roboto, 'Helvetica Neue', Arial, sans-serif; - --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - monospace; - - /* Spacing */ - --spacing-1: 0.25rem; - --spacing-2: 0.5rem; - --spacing-3: 0.75rem; - --spacing-4: 1rem; - --spacing-6: 1.5rem; - --spacing-8: 2rem; - - /* Border Radius */ - --radius-sm: 0.25rem; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; - --radius-full: 9999px; - - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); -} - -/* Reset */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font-family: var(--font-family-sans); - line-height: 1.5; - color: var(--color-gray-900); - background-color: white; -} - -img, -picture, -video, -canvas, -svg { - display: block; - max-width: 100%; -} - -input, -button, -textarea, -select { - font: inherit; -} - -/* Focus styles */ -:focus-visible { - outline: 2px solid var(--color-primary-500); - outline-offset: 2px; -} - diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/error.css b/src/templates/overlays/styling/css/_nextjs/src/app/error.css new file mode 100644 index 0000000..fd4c5fa --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/error.css @@ -0,0 +1,49 @@ +.error-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.error-content { + text-align: center; +} + +.error-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-danger); +} + +.error-message { + margin-top: 1rem; + color: var(--color-text-muted); +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; + border: none; + cursor: pointer; +} + +.btn-primary { + margin-top: 1.5rem; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/error.tsx b/src/templates/overlays/styling/css/_nextjs/src/app/error.tsx new file mode 100644 index 0000000..1daab6d --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/error.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useEffect } from 'react'; +import './error.css'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+

Something went wrong

+

+ {error.message || 'An unexpected error occurred'} +

+ +
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/loading.css b/src/templates/overlays/styling/css/_nextjs/src/app/loading.css new file mode 100644 index 0000000..a517d54 --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/loading.css @@ -0,0 +1,28 @@ +.spinner-container { + display: flex; + min-height: 100vh; + align-items: center; + justify-content: center; +} + +.spinner { + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/loading.tsx b/src/templates/overlays/styling/css/_nextjs/src/app/loading.tsx new file mode 100644 index 0000000..0325e66 --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/loading.tsx @@ -0,0 +1,13 @@ +import './loading.css'; + +export default function Loading() { + return ( +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/not-found.css b/src/templates/overlays/styling/css/_nextjs/src/app/not-found.css new file mode 100644 index 0000000..c70ceaf --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/not-found.css @@ -0,0 +1,70 @@ +.not-found-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.not-found-content { + text-align: center; +} + +.not-found-code { + font-size: 1rem; + font-weight: 600; + color: var(--color-primary); +} + +.not-found-title { + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .not-found-title { + font-size: 3rem; + } +} + +.not-found-description { + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: var(--color-text-muted); +} + +.not-found-buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; +} + +.btn-primary { + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/not-found.tsx b/src/templates/overlays/styling/css/_nextjs/src/app/not-found.tsx new file mode 100644 index 0000000..885b05f --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/not-found.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; +import './not-found.css'; + +export default function NotFound() { + return ( +
+
+

404

+

Page not found

+

+ Sorry, we couldn't find the page you're looking for. +

+
+ + Go back home + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/page.css b/src/templates/overlays/styling/css/_nextjs/src/app/page.css new file mode 100644 index 0000000..54a3a1d --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/page.css @@ -0,0 +1,73 @@ +.landing-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.landing-content { + text-align: center; +} + +.landing-title { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .landing-title { + font-size: 3.75rem; + } +} + +.landing-description { + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: var(--color-text-muted); +} + +.landing-buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; +} + +.btn-primary { + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-link { + font-size: 0.875rem; + line-height: 1.5rem; + color: var(--color-text); +} + +.btn-link:hover { + color: var(--color-primary); +} + + + + diff --git a/src/templates/overlays/styling/css/_nextjs/src/app/page.tsx b/src/templates/overlays/styling/css/_nextjs/src/app/page.tsx new file mode 100644 index 0000000..6d6e756 --- /dev/null +++ b/src/templates/overlays/styling/css/_nextjs/src/app/page.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import './page.css'; + +export default function HomePage() { + return ( +
+
+

Welcome to Your App

+

+ A production-ready Next.js application scaffolded with create-react-forge. +

+
+ + Get started + + + Learn more + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.css b/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.css new file mode 100644 index 0000000..fd4c5fa --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.css @@ -0,0 +1,49 @@ +.error-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.error-content { + text-align: center; +} + +.error-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-danger); +} + +.error-message { + margin-top: 1rem; + color: var(--color-text-muted); +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; + border: none; + cursor: pointer; +} + +.btn-primary { + margin-top: 1.5rem; + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.tsx b/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.tsx new file mode 100644 index 0000000..c1de42f --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/components/errors/ErrorFallback.tsx @@ -0,0 +1,22 @@ +import { FallbackProps } from 'react-error-boundary'; +import './ErrorFallback.css'; + +export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + return ( +
+
+

Something went wrong

+

+ {error.message || 'An unexpected error occurred'} +

+ +
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.css b/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.css new file mode 100644 index 0000000..0f8f763 --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.css @@ -0,0 +1,40 @@ +.spinner-container { + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + border-radius: 50%; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + animation: spin 1s linear infinite; +} + +.spinner-sm { + width: 1rem; + height: 1rem; +} + +.spinner-md { + width: 2rem; + height: 2rem; +} + +.spinner-lg { + width: 4rem; + height: 4rem; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.tsx b/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..3ae98f2 --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,21 @@ +import './LoadingSpinner.css'; + +type LoadingSpinnerProps = { + size?: 'sm' | 'md' | 'lg'; +}; + +export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) { + return ( +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.css b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.css new file mode 100644 index 0000000..54a3a1d --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.css @@ -0,0 +1,73 @@ +.landing-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.landing-content { + text-align: center; +} + +.landing-title { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .landing-title { + font-size: 3.75rem; + } +} + +.landing-description { + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: var(--color-text-muted); +} + +.landing-buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; +} + +.btn-primary { + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-link { + font-size: 0.875rem; + line-height: 1.5rem; + color: var(--color-text); +} + +.btn-link:hover { + color: var(--color-primary); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.tsx b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.tsx new file mode 100644 index 0000000..f92b7f4 --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/Landing.tsx @@ -0,0 +1,32 @@ +import { Link } from 'react-router-dom'; +import './Landing.css'; + +export function Landing() { + return ( +
+
+

Welcome to Your App

+

+ A production-ready React application scaffolded with create-react-forge. +

+
+ + Get started + + + Learn more + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.css b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.css new file mode 100644 index 0000000..c70ceaf --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.css @@ -0,0 +1,70 @@ +.not-found-container { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.not-found-content { + text-align: center; +} + +.not-found-code { + font-size: 1rem; + font-weight: 600; + color: var(--color-primary); +} + +.not-found-title { + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text); +} + +@media (min-width: 640px) { + .not-found-title { + font-size: 3rem; + } +} + +.not-found-description { + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: var(--color-text-muted); +} + +.not-found-buttons { + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +.btn { + display: inline-block; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; +} + +.btn-primary { + border-radius: var(--radius-md); + background-color: var(--color-primary); + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + + + + diff --git a/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.tsx b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.tsx new file mode 100644 index 0000000..0ffc4f5 --- /dev/null +++ b/src/templates/overlays/styling/css/_vite/src/features/misc/routes/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from 'react-router-dom'; +import './NotFound.css'; + +export function NotFound() { + return ( +
+
+

404

+

Page not found

+

+ Sorry, we couldn't find the page you're looking for. +

+
+ + Go back home + +
+
+
+ ); +} + + + + diff --git a/src/templates/overlays/styling/css/manifest.json b/src/templates/overlays/styling/css/manifest.json new file mode 100644 index 0000000..06c5b1a --- /dev/null +++ b/src/templates/overlays/styling/css/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "styling-css", + "version": "1.0.0", + "description": "Plain CSS styling solution", + "compatibleWith": ["runtime-vite"], + "dependencies": {}, + "devDependencies": {}, + "scripts": {}, + "filePatterns": { + "include": ["**/*"], + "exclude": ["manifest.json", "_vite"] + }, + "runtimeOverrides": { + "vite": "_vite" + } +} + diff --git a/src/templates/overlays/styling/css/src/styles/globals.css b/src/templates/overlays/styling/css/src/styles/globals.css new file mode 100644 index 0000000..25ebc08 --- /dev/null +++ b/src/templates/overlays/styling/css/src/styles/globals.css @@ -0,0 +1,107 @@ +/* CSS Reset and Base Styles */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + /* Colors */ + --color-primary: #4f46e5; + --color-primary-hover: #6366f1; + --color-secondary: #64748b; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-text: #111827; + --color-text-muted: #4b5563; + --color-background: #ffffff; + --color-border: #e5e7eb; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 2.5rem; + + /* Border Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; + + /* Font */ + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; +} + +html { + font-family: var(--font-sans); + line-height: 1.5; + -webkit-text-size-adjust: 100%; +} + +body { + color: var(--color-text); + background-color: var(--color-background); +} + +a { + color: inherit; + text-decoration: none; +} + +button { + font-family: inherit; + font-size: inherit; + cursor: pointer; +} + +/* Utility Classes */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.text-center { + text-align: center; +} + +.min-h-screen { + min-height: 100vh; +} + +.gap-6 { + gap: var(--spacing-lg); +} + +.mt-4 { + margin-top: var(--spacing-md); +} + +.mt-6 { + margin-top: var(--spacing-lg); +} + +.mt-10 { + margin-top: var(--spacing-2xl); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/error.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/error.tsx new file mode 100644 index 0000000..d0dcdd5 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/error.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useEffect } from 'react'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 1.5rem; + font-weight: 700; + color: #dc2626; +`; + +const Message = styled.p` + margin-top: 1rem; + color: #4b5563; +`; + +const RetryButton = styled.button` + margin-top: 1.5rem; + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + border: none; + cursor: pointer; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } +`; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + + return ( + + + Something went wrong + {error.message || 'An unexpected error occurred'} + Try again + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/layout.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/layout.tsx new file mode 100644 index 0000000..83fd0fd --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next'; +import { Providers } from './providers'; + +export const metadata: Metadata = { + title: '{{PROJECT_NAME}}', + description: 'A production-ready Next.js application', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/loading.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/loading.tsx new file mode 100644 index 0000000..87888b8 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/loading.tsx @@ -0,0 +1,40 @@ +'use client'; + +import styled, { keyframes } from 'styled-components'; + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + +const Container = styled.div` + display: flex; + min-height: 100vh; + align-items: center; + justify-content: center; +`; + +const Spinner = styled.div` + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 2px solid #d1d5db; + border-top-color: #4f46e5; + animation: ${spin} 1s linear infinite; +`; + +export default function Loading() { + return ( + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/not-found.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/not-found.tsx new file mode 100644 index 0000000..b2616f7 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/not-found.tsx @@ -0,0 +1,91 @@ +'use client'; + +import Link from 'next/link'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const ErrorCode = styled.p` + font-size: 1rem; + font-weight: 600; + color: #4f46e5; +`; + +const Title = styled.h1` + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +export default function NotFound() { + return ( + + + 404 + Page not found + + Sorry, we couldn't find the page you're looking for. + + + Go back home + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/page.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/page.tsx new file mode 100644 index 0000000..11d174c --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/page.tsx @@ -0,0 +1,102 @@ +'use client'; + +import Link from 'next/link'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3.75rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +const SecondaryLink = styled.a` + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: #111827; + text-decoration: none; + + &:hover { + color: #4f46e5; + } +`; + +export default function HomePage() { + return ( + + + Welcome to Your App + + A production-ready Next.js application scaffolded with create-react-forge. + + + Get started + + Learn more + + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_nextjs/src/app/providers.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/app/providers.tsx new file mode 100644 index 0000000..44b276d --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/app/providers.tsx @@ -0,0 +1,22 @@ +'use client'; + +import StyledComponentsRegistry from '@/lib/StyledComponentsRegistry'; +import { GlobalStyles } from '@/styles/globals'; +import { ReactNode } from 'react'; + +type ProvidersProps = { + children: ReactNode; +}; + +export function Providers({ children }: ProvidersProps) { + return ( + + + {children} + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/src/lib/StyledComponentsRegistry.tsx b/src/templates/overlays/styling/styled-components/_nextjs/src/lib/StyledComponentsRegistry.tsx similarity index 72% rename from src/templates/overlays/styling/styled-components/src/lib/StyledComponentsRegistry.tsx rename to src/templates/overlays/styling/styled-components/_nextjs/src/lib/StyledComponentsRegistry.tsx index 9375230..b15e88c 100644 --- a/src/templates/overlays/styling/styled-components/src/lib/StyledComponentsRegistry.tsx +++ b/src/templates/overlays/styling/styled-components/_nextjs/src/lib/StyledComponentsRegistry.tsx @@ -1,7 +1,7 @@ 'use client'; -import React, { useState } from 'react'; import { useServerInsertedHTML } from 'next/navigation'; +import React, { useState } from 'react'; import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; export default function StyledComponentsRegistry({ @@ -10,6 +10,7 @@ export default function StyledComponentsRegistry({ children: React.ReactNode; }) { // Only create stylesheet once with lazy initial state + // useState with initializer ensures this only runs once const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); useServerInsertedHTML(() => { @@ -18,7 +19,11 @@ export default function StyledComponentsRegistry({ return <>{styles}; }); - if (typeof window !== 'undefined') return <>{children}; + // On the client side, styled-components automatically injects styles + // We only need StyleSheetManager on the server for SSR collection + if (typeof window !== 'undefined') { + return <>{children}; + } return ( @@ -27,3 +32,6 @@ export default function StyledComponentsRegistry({ ); } + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/app/provider.tsx b/src/templates/overlays/styling/styled-components/_vite/src/app/provider.tsx new file mode 100644 index 0000000..f4058d6 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/app/provider.tsx @@ -0,0 +1,27 @@ +import { ErrorFallback } from '@/components/errors/ErrorFallback'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { GlobalStyles } from '@/styles/globals'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { BrowserRouter } from 'react-router-dom'; + +type AppProviderProps = { + children: React.ReactNode; +}; + +export function AppProvider({ children }: AppProviderProps) { + return ( + }> + + + + {children} + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/components/errors/ErrorFallback.tsx b/src/templates/overlays/styling/styled-components/_vite/src/components/errors/ErrorFallback.tsx new file mode 100644 index 0000000..2c0eeb9 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/components/errors/ErrorFallback.tsx @@ -0,0 +1,59 @@ +import { FallbackProps } from 'react-error-boundary'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 1.5rem; + font-weight: 700; + color: #dc2626; +`; + +const Message = styled.p` + margin-top: 1rem; + color: #4b5563; +`; + +const RetryButton = styled.button` + margin-top: 1.5rem; + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + border: none; + cursor: pointer; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } +`; + +export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + return ( + + + Something went wrong + {error.message || 'An unexpected error occurred'} + Try again + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/components/ui/LoadingSpinner.tsx b/src/templates/overlays/styling/styled-components/_vite/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..fbe30cc --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,47 @@ +import styled, { keyframes } from 'styled-components'; + +type LoadingSpinnerProps = { + size?: 'sm' | 'md' | 'lg'; +}; + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + +const sizes = { + sm: '1rem', + md: '2rem', + lg: '4rem', +}; + +const Container = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +const Spinner = styled.div<{ $size: 'sm' | 'md' | 'lg' }>` + width: ${(props) => sizes[props.$size]}; + height: ${(props) => sizes[props.$size]}; + border-radius: 50%; + border: 2px solid #d1d5db; + border-top-color: #4f46e5; + animation: ${spin} 1s linear infinite; +`; + +export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) { + return ( + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/Landing.tsx b/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/Landing.tsx new file mode 100644 index 0000000..6ef2741 --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/Landing.tsx @@ -0,0 +1,100 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3.75rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +const SecondaryLink = styled.a` + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: #111827; + text-decoration: none; + + &:hover { + color: #4f46e5; + } +`; + +export function Landing() { + return ( + + + Welcome to Your App + + A production-ready React application scaffolded with create-react-forge. + + + Get started + + Learn more + + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/NotFound.tsx b/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/NotFound.tsx new file mode 100644 index 0000000..fff510e --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/features/misc/routes/NotFound.tsx @@ -0,0 +1,89 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const ErrorCode = styled.p` + font-size: 1rem; + font-weight: 600; + color: #4f46e5; +`; + +const Title = styled.h1` + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +export function NotFound() { + return ( + + + 404 + Page not found + + Sorry, we couldn't find the page you're looking for. + + + Go back home + + + + ); +} + + + + diff --git a/src/templates/overlays/styling/styled-components/_vite/src/main.tsx b/src/templates/overlays/styling/styled-components/_vite/src/main.tsx new file mode 100644 index 0000000..58d193d --- /dev/null +++ b/src/templates/overlays/styling/styled-components/_vite/src/main.tsx @@ -0,0 +1,13 @@ +import { App } from '@/app/App'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); + + + + diff --git a/src/templates/overlays/styling/styled-components/manifest.json b/src/templates/overlays/styling/styled-components/manifest.json index 7def56c..0694af8 100644 --- a/src/templates/overlays/styling/styled-components/manifest.json +++ b/src/templates/overlays/styling/styled-components/manifest.json @@ -13,7 +13,11 @@ "scripts": {}, "filePatterns": { "include": ["**/*"], - "exclude": ["manifest.json"] + "exclude": ["manifest.json", "_vite", "_nextjs"] + }, + "runtimeOverrides": { + "vite": "_vite", + "nextjs": "_nextjs" } } diff --git a/src/templates/overlays/styling/tailwind/_nextjs/src/app/error.tsx b/src/templates/overlays/styling/tailwind/_nextjs/src/app/error.tsx new file mode 100644 index 0000000..635128c --- /dev/null +++ b/src/templates/overlays/styling/tailwind/_nextjs/src/app/error.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+

Something went wrong

+

+ {error.message || 'An unexpected error occurred'} +

+ +
+
+ ); +} + diff --git a/src/templates/overlays/styling/tailwind/_nextjs/src/app/loading.tsx b/src/templates/overlays/styling/tailwind/_nextjs/src/app/loading.tsx new file mode 100644 index 0000000..643816f --- /dev/null +++ b/src/templates/overlays/styling/tailwind/_nextjs/src/app/loading.tsx @@ -0,0 +1,12 @@ +export default function Loading() { + return ( +
+
+
+ ); +} + diff --git a/src/templates/overlays/styling/tailwind/_nextjs/src/app/not-found.tsx b/src/templates/overlays/styling/tailwind/_nextjs/src/app/not-found.tsx new file mode 100644 index 0000000..3f877e7 --- /dev/null +++ b/src/templates/overlays/styling/tailwind/_nextjs/src/app/not-found.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; + +export default function NotFound() { + return ( +
+
+

404

+

+ Page not found +

+

+ Sorry, we couldn't find the page you're looking for. +

+
+ + Go back home + +
+
+
+ ); +} + diff --git a/src/templates/overlays/styling/tailwind/_nextjs/src/app/page.tsx b/src/templates/overlays/styling/tailwind/_nextjs/src/app/page.tsx new file mode 100644 index 0000000..846753b --- /dev/null +++ b/src/templates/overlays/styling/tailwind/_nextjs/src/app/page.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link'; + +export default function HomePage() { + return ( +
+
+

+ Welcome to Your App +

+

+ A production-ready Next.js application scaffolded with create-react-forge. +

+
+ + Get started + + + Learn more + +
+
+
+ ); +} + diff --git a/src/templates/overlays/styling/tailwind/manifest.json b/src/templates/overlays/styling/tailwind/manifest.json index e0f484e..5fce7e7 100644 --- a/src/templates/overlays/styling/tailwind/manifest.json +++ b/src/templates/overlays/styling/tailwind/manifest.json @@ -2,7 +2,7 @@ "name": "styling-tailwind", "version": "1.0.0", "description": "Tailwind CSS styling setup with modern configuration", - "compatibleWith": ["runtime-vite", "runtime-nextjs"], + "compatibleWith": ["runtime-nextjs"], "dependencies": {}, "devDependencies": { "tailwindcss": "^4.0.0", @@ -13,7 +13,9 @@ "scripts": {}, "filePatterns": { "include": ["**/*"], - "exclude": ["manifest.json"] + "exclude": ["manifest.json", "_nextjs"] + }, + "runtimeOverrides": { + "nextjs": "_nextjs" } } - diff --git a/src/templates/registry.ts b/src/templates/registry.ts index a62fbe9..037d80c 100644 --- a/src/templates/registry.ts +++ b/src/templates/registry.ts @@ -17,6 +17,10 @@ export interface TemplateManifest { include?: string[]; exclude?: string[]; }; + runtimeOverrides?: { + vite?: string; + nextjs?: string; + }; } /** @@ -141,7 +145,7 @@ export class TemplateRegistry { /** * Load a template overlay from a directory */ - loadTemplate(templatePath: string): TemplateOverlay { + loadTemplate(templatePath: string, runtime?: 'vite' | 'nextjs'): TemplateOverlay { const fullPath = join(this.templatesDir, templatePath); const manifestPath = join(fullPath, 'manifest.json'); @@ -153,6 +157,20 @@ export class TemplateRegistry { const exclude = ['manifest.json', ...(manifest.filePatterns?.exclude || [])]; const files = readDirectoryRecursively(fullPath, fullPath, exclude); + // Load runtime-specific files if runtime overrides exist + if (runtime && manifest.runtimeOverrides) { + const runtimeDir = manifest.runtimeOverrides[runtime]; + if (runtimeDir) { + const runtimePath = join(fullPath, runtimeDir); + if (existsSync(runtimePath)) { + const runtimeFiles = readDirectoryRecursively(runtimePath, runtimePath, []); + runtimeFiles.forEach((content, path) => { + files.set(path, content); + }); + } + } + } + const overlay: TemplateOverlay = { name: manifest.name, path: templatePath, @@ -166,8 +184,8 @@ export class TemplateRegistry { /** * Load and register a template */ - loadAndRegister(templatePath: string): TemplateOverlay { - const overlay = this.loadTemplate(templatePath); + loadAndRegister(templatePath: string, runtime?: 'vite' | 'nextjs'): TemplateOverlay { + const overlay = this.loadTemplate(templatePath, runtime); this.loadedTemplates.set(templatePath, overlay); return overlay; } @@ -220,33 +238,34 @@ export class TemplateRegistry { // Load runtime template templates.push(this.loadAndRegister(`runtime/${config.runtime}`)); - // Load styling template + // Load styling template (pass runtime for runtime-specific overlays) + // Vite: always styled-components + // Next.js: tailwind or none (none = no styling overlay, use runtime defaults) if (config.styling.solution === 'tailwind') { - templates.push(this.loadAndRegister('styling/tailwind')); - } else if (config.styling.solution === 'css-modules') { - templates.push(this.loadAndRegister('styling/css-modules')); + templates.push(this.loadAndRegister('styling/tailwind', config.runtime)); } else if (config.styling.solution === 'styled-components') { - templates.push(this.loadAndRegister('styling/styled-components')); + templates.push(this.loadAndRegister('styling/styled-components', config.runtime)); } + // 'none' - don't load any styling overlay, use runtime defaults // Load state management template if (config.stateManagement && config.stateManagement !== 'none') { - templates.push(this.loadAndRegister(`state/${config.stateManagement}`)); + templates.push(this.loadAndRegister(`state/${config.stateManagement}`, config.runtime)); } // Load testing templates if (config.testing.enabled) { if (config.testing.unit?.runner) { - templates.push(this.loadAndRegister(`testing/${config.testing.unit.runner}`)); + templates.push(this.loadAndRegister(`testing/${config.testing.unit.runner}`, config.runtime)); } if (config.testing.e2e?.enabled && config.testing.e2e.runner && config.testing.e2e.runner !== 'none') { - templates.push(this.loadAndRegister(`testing/${config.testing.e2e.runner}`)); + templates.push(this.loadAndRegister(`testing/${config.testing.e2e.runner}`, config.runtime)); } } // Load data fetching template if (config.dataFetching.enabled) { - templates.push(this.loadAndRegister('features/tanstack-query')); + templates.push(this.loadAndRegister('features/tanstack-query', config.runtime)); } return templates; diff --git a/src/templates/utils.ts b/src/templates/utils.ts index f7ea1cb..aef2f27 100644 --- a/src/templates/utils.ts +++ b/src/templates/utils.ts @@ -15,9 +15,9 @@ export function getTemplatePathForRuntime(runtime: 'vite' | 'nextjs'): string { * Get template path for styling solution */ export function getTemplatePathForStyling(styling: string): string { - if (styling === 'css') return ''; + if (styling === 'none') return ''; if (styling === 'tailwind') return 'styling/tailwind'; - if (styling === 'css-modules') return 'styling/css-modules'; + if (styling === 'styled-components') return 'styling/styled-components'; return `styling/${styling}`; } @@ -102,7 +102,7 @@ export function getApplicableTemplates(config: { }): string[] { const templates = ['base', getTemplatePathForRuntime(config.runtime as 'vite' | 'nextjs')]; - if (config.styling && config.styling !== 'css') { + if (config.styling && config.styling !== 'none') { const stylingPath = getTemplatePathForStyling(config.styling); if (stylingPath) templates.push(stylingPath); } diff --git a/test/.babelrc b/test/.babelrc new file mode 100644 index 0000000..495e2f9 --- /dev/null +++ b/test/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": ["next/babel"], + "plugins": [["styled-components", { "ssr": true }]] +} + diff --git a/test/ARCHITECTURE.md b/test/ARCHITECTURE.md new file mode 100644 index 0000000..3220870 --- /dev/null +++ b/test/ARCHITECTURE.md @@ -0,0 +1,55 @@ +# Project Architecture + +## Overview + +This project was bootstrapped with `create-react-forge` using: + +- **Runtime**: Vite (SPA) +- **Language**: TypeScript +- **Styling**: styled-components +- **State Management**: zustand +- **Data Fetching**: None +- **Testing**: Disabled + +## Directory Structure + +``` +src/ +├── components/ # Reusable UI components +│ └── ui/ # Base UI primitives +├── features/ # Feature-based modules +├── hooks/ # Custom React hooks +├── providers/ # React context providers +├── stores/ # State management stores +├── types/ # TypeScript type definitions +└── utils/ # Utility functions +``` + +## Naming Conventions + +### Files +- **Components**: PascalCase (e.g., UserProfile.tsx) +- **Hooks**: camelCase with "use" prefix (e.g., useAuth.ts) +- **Utilities**: camelCase (e.g., formatDate.ts) +- **Constants**: SCREAMING_SNAKE_CASE (e.g., constants.ts) + +### Code +- **Components**: PascalCase +- **Functions**: camelCase +- **Variables**: camelCase +- **Constants**: SCREAMING_SNAKE_CASE +- **Types/Interfaces**: PascalCase + +## Testing Strategy + +Testing is currently disabled. + +## Data Fetching + +Standard `fetch` or `axios` is used for data fetching. + +## State Management + + +We use **zustand** for global client state. + diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..7ea2452 --- /dev/null +++ b/test/README.md @@ -0,0 +1,72 @@ +# test + +![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat&logo=vite&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) ![styled-components](https://img.shields.io/badge/styled--components-DB7093?style=flat&logo=styled-components&logoColor=white) ![Zustand](https://img.shields.io/badge/Zustand-443E38?style=flat&logo=react&logoColor=white) + +A production-ready React application built with Vite, scaffolded by create-react-forge. + +## Prerequisites + +- Node.js 18.x or higher +- npm + +## Getting Started + +1. Install dependencies: + +```bash +npm install +``` + +2. Start the development server: + +```bash +npm run dev +``` + +3. Open [http://localhost:5173](http://localhost:5173) in your browser. + +## Available Scripts + +| Command | Description | +|---------|-------------| +| `npm run dev` | Start development server | +| `npm run build` | Build for production | +| `npm run preview` | Preview production build | +| `npm run format` | Format code with Prettier | +| `npm run lint` | Lint code | + +## Project Structure + +``` +test/ +├── src/ +│ ├── components/ # Reusable UI components +│ │ └── ui/ # Base UI primitives +│ ├── features/ # Feature-based modules +│ ├── hooks/ # Custom React hooks +│ ├── stores/ # State management stores +│ ├── styles/ # Global styles +│ └── types/ # TypeScript type definitions +├── public/ # Static assets +└── [config files] # Configuration files +``` + +## Tech Stack + +- **Runtime**: Vite +- **Language**: TypeScript +- **Styling**: styled-components +- **State Management**: zustand + +## Documentation + +- [Vite Documentation](https://vitejs.dev/) +- [React Router](https://reactrouter.com/) +- [React Documentation](https://react.dev/) +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [styled-components Documentation](https://styled-components.com/docs) +- [Zustand Documentation](https://zustand-demo.pmnd.rs/) + +## License + +MIT diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..cb2cb35 --- /dev/null +++ b/test/index.html @@ -0,0 +1,14 @@ + + + + + + + test + + +
+ + + + diff --git a/test/package-lock.json b/test/package-lock.json new file mode 100644 index 0000000..6864f32 --- /dev/null +++ b/test/package-lock.json @@ -0,0 +1,2206 @@ +{ + "name": "test", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "version": "0.1.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.1.2", + "react-router-dom": "^7.1.1", + "styled-components": "^6.1.14", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/react": "^19.0.6", + "@types/react-dom": "^19.0.3", + "@types/styled-components": "^5.1.34", + "@vitejs/plugin-react": "^4.3.4", + "babel-plugin-styled-components": "^2.1.4", + "typescript": "^5.3.0", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/styled-components": { + "version": "5.1.36", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.36.tgz", + "integrity": "sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "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", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-components": { + "version": "6.3.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.8.tgz", + "integrity": "sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.6", + "tslib": "2.8.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..5d74bf1 --- /dev/null +++ b/test/package.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.1.2", + "react-router-dom": "^7.1.1", + "styled-components": "^6.1.14", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/react": "^19.0.6", + "@types/react-dom": "^19.0.3", + "@types/styled-components": "^5.1.34", + "@vitejs/plugin-react": "^4.3.4", + "babel-plugin-styled-components": "^2.1.4", + "typescript": "^5.3.0", + "vite": "^6.0.7" + } +} diff --git a/test/public/vite.svg b/test/public/vite.svg new file mode 100644 index 0000000..b3e75a1 --- /dev/null +++ b/test/public/vite.svg @@ -0,0 +1,2 @@ + + diff --git a/test/src/app/App.tsx b/test/src/app/App.tsx new file mode 100644 index 0000000..aaf2bfc --- /dev/null +++ b/test/src/app/App.tsx @@ -0,0 +1,11 @@ +import { AppProvider } from '@/app/provider'; +import { AppRouter } from '@/app/router'; + +export function App() { + return ( + + + + ); +} + diff --git a/test/src/app/provider.tsx b/test/src/app/provider.tsx new file mode 100644 index 0000000..dc53055 --- /dev/null +++ b/test/src/app/provider.tsx @@ -0,0 +1,25 @@ +import { ErrorFallback } from '@/components/errors/ErrorFallback'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { GlobalStyles } from '@/styles/globals'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { BrowserRouter } from 'react-router-dom'; + +type AppProviderProps = { + children: React.ReactNode; +}; + +export function AppProvider({ children }: AppProviderProps) { + return ( + }> + + + + {children} + + + + ); +} + + diff --git a/test/src/app/router.tsx b/test/src/app/router.tsx new file mode 100644 index 0000000..b5fd33a --- /dev/null +++ b/test/src/app/router.tsx @@ -0,0 +1,19 @@ +import { Landing } from '@/features/misc/routes/Landing'; +import { NotFound } from '@/features/misc/routes/NotFound'; +import { useRoutes } from 'react-router-dom'; + +export function AppRouter() { + const routes = useRoutes([ + { + path: '/', + element: , + }, + { + path: '*', + element: , + }, + ]); + + return routes; +} + diff --git a/test/src/components/errors/ErrorFallback.tsx b/test/src/components/errors/ErrorFallback.tsx new file mode 100644 index 0000000..033f220 --- /dev/null +++ b/test/src/components/errors/ErrorFallback.tsx @@ -0,0 +1,57 @@ +import { FallbackProps } from 'react-error-boundary'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 1.5rem; + font-weight: 700; + color: #dc2626; +`; + +const Message = styled.p` + margin-top: 1rem; + color: #4b5563; +`; + +const RetryButton = styled.button` + margin-top: 1.5rem; + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + border: none; + cursor: pointer; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } +`; + +export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + return ( + + + Something went wrong + {error.message || 'An unexpected error occurred'} + Try again + + + ); +} + + diff --git a/test/src/components/ui/Button.styled.ts b/test/src/components/ui/Button.styled.ts new file mode 100644 index 0000000..9ea9cb3 --- /dev/null +++ b/test/src/components/ui/Button.styled.ts @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +export const StyledButton = styled.button<{ variant?: 'primary' | 'secondary' }>` + padding: 0.5rem 1rem; + font-size: 1rem; + font-weight: 500; + border-radius: 0.375rem; + border: none; + cursor: pointer; + transition: all 0.2s ease-in-out; + + ${({ variant = 'primary' }) => + variant === 'primary' + ? ` + background-color: #3b82f6; + color: white; + + &:hover { + background-color: #2563eb; + } + + &:disabled { + background-color: #9ca3af; + cursor: not-allowed; + } + ` + : ` + background-color: #e5e7eb; + color: #1f2937; + + &:hover { + background-color: #d1d5db; + } + + &:disabled { + background-color: #f3f4f6; + color: #9ca3af; + cursor: not-allowed; + } + `} +`; + diff --git a/test/src/components/ui/Button.tsx b/test/src/components/ui/Button.tsx new file mode 100644 index 0000000..f39f7b4 --- /dev/null +++ b/test/src/components/ui/Button.tsx @@ -0,0 +1,138 @@ +import { forwardRef } from 'react'; +import styled, { css } from 'styled-components'; + +export type ButtonProps = React.ButtonHTMLAttributes & { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; +}; + +const variantStyles = { + primary: css` + background-color: #4f46e5; + color: white; + &:hover:not(:disabled) { + background-color: #6366f1; + } + `, + secondary: css` + background-color: #f3f4f6; + color: #111827; + &:hover:not(:disabled) { + background-color: #e5e7eb; + } + `, + outline: css` + background-color: transparent; + color: #374151; + border: 1px solid #d1d5db; + &:hover:not(:disabled) { + background-color: #f9fafb; + } + `, + ghost: css` + background-color: transparent; + color: #374151; + &:hover:not(:disabled) { + background-color: #f3f4f6; + } + `, + danger: css` + background-color: #dc2626; + color: white; + &:hover:not(:disabled) { + background-color: #ef4444; + } + `, +}; + +const sizeStyles = { + sm: css` + padding: 0.375rem 0.625rem; + font-size: 0.75rem; + `, + md: css` + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + `, + lg: css` + padding: 0.75rem 1rem; + font-size: 1rem; + `, +}; + +const StyledButton = styled.button<{ + $variant: ButtonProps['variant']; + $size: ButtonProps['size']; +}>` + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + font-weight: 600; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s, color 0.2s; + border: none; + cursor: pointer; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } + + ${(props) => variantStyles[props.$variant || 'primary']} + ${(props) => sizeStyles[props.$size || 'md']} +`; + +const LoadingSpinner = styled.span` + margin-right: 0.5rem; + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +`; + +export const Button = forwardRef( + ( + { + variant = 'primary', + size = 'md', + isLoading = false, + disabled, + children, + ...props + }, + ref + ) => { + return ( + + {isLoading && } + {children} + + ); + } +); + +Button.displayName = 'Button'; diff --git a/test/src/components/ui/Input.tsx b/test/src/components/ui/Input.tsx new file mode 100644 index 0000000..2d8dbc3 --- /dev/null +++ b/test/src/components/ui/Input.tsx @@ -0,0 +1,77 @@ +import { forwardRef } from 'react'; +import styled from 'styled-components'; + +export type InputProps = React.InputHTMLAttributes & { + label?: string; + error?: string; +}; + +const InputWrapper = styled.div` + width: 100%; +`; + +const Label = styled.label` + display: block; + margin-bottom: 0.25rem; + font-size: 0.875rem; + font-weight: 500; + color: #374151; +`; + +const StyledInput = styled.input<{ $hasError?: boolean }>` + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + color: #111827; + background-color: white; + border: 1px solid ${(props) => (props.$hasError ? '#fca5a5' : '#d1d5db')}; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: border-color 0.2s, box-shadow 0.2s; + + &::placeholder { + color: #9ca3af; + } + + &:focus { + outline: none; + border-color: ${(props) => (props.$hasError ? '#ef4444' : '#4f46e5')}; + box-shadow: 0 0 0 2px ${(props) => (props.$hasError ? '#fecaca' : '#c7d2fe')}; + } + + &:disabled { + cursor: not-allowed; + background-color: #f9fafb; + color: #6b7280; + } +`; + +const ErrorMessage = styled.p` + margin-top: 0.25rem; + font-size: 0.875rem; + color: #dc2626; +`; + +export const Input = forwardRef( + ({ label, error, id, ...props }, ref) => { + const inputId = id || props.name; + + return ( + + {label && } + + {error && {error}} + + ); + } +); + +Input.displayName = 'Input'; diff --git a/test/src/components/ui/LoadingSpinner.tsx b/test/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..0cfc8c9 --- /dev/null +++ b/test/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,45 @@ +import styled, { keyframes } from 'styled-components'; + +type LoadingSpinnerProps = { + size?: 'sm' | 'md' | 'lg'; +}; + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + +const sizes = { + sm: '1rem', + md: '2rem', + lg: '4rem', +}; + +const Container = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +const Spinner = styled.div<{ $size: 'sm' | 'md' | 'lg' }>` + width: ${(props) => sizes[props.$size]}; + height: ${(props) => sizes[props.$size]}; + border-radius: 50%; + border: 2px solid #d1d5db; + border-top-color: #4f46e5; + animation: ${spin} 1s linear infinite; +`; + +export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) { + return ( + + + + ); +} + + diff --git a/test/src/components/ui/index.ts b/test/src/components/ui/index.ts new file mode 100644 index 0000000..143c361 --- /dev/null +++ b/test/src/components/ui/index.ts @@ -0,0 +1,3 @@ +export { Button, type ButtonProps } from './Button'; +export { Input, type InputProps } from './Input'; + diff --git a/test/src/features/misc/routes/Landing.tsx b/test/src/features/misc/routes/Landing.tsx new file mode 100644 index 0000000..3266d49 --- /dev/null +++ b/test/src/features/misc/routes/Landing.tsx @@ -0,0 +1,98 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h1` + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3.75rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1.125rem; + line-height: 2rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +const SecondaryLink = styled.a` + font-size: 0.875rem; + font-weight: 600; + line-height: 1.5rem; + color: #111827; + text-decoration: none; + + &:hover { + color: #4f46e5; + } +`; + +export function Landing() { + return ( + + + Welcome to Your App + + A production-ready React application scaffolded with create-react-forge. + + + Get started + + Learn more + + + + + ); +} + + diff --git a/test/src/features/misc/routes/NotFound.tsx b/test/src/features/misc/routes/NotFound.tsx new file mode 100644 index 0000000..17a5efc --- /dev/null +++ b/test/src/features/misc/routes/NotFound.tsx @@ -0,0 +1,87 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const ErrorCode = styled.p` + font-size: 1rem; + font-weight: 600; + color: #4f46e5; +`; + +const Title = styled.h1` + margin-top: 1rem; + font-size: 1.875rem; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + + @media (min-width: 640px) { + font-size: 3rem; + } +`; + +const Description = styled.p` + margin-top: 1.5rem; + font-size: 1rem; + line-height: 1.75rem; + color: #4b5563; +`; + +const ButtonGroup = styled.div` + margin-top: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; +`; + +const PrimaryButton = styled(Link)` + border-radius: 0.375rem; + background-color: #4f46e5; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-weight: 600; + color: white; + text-decoration: none; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: background-color 0.2s; + + &:hover { + background-color: #6366f1; + } + + &:focus-visible { + outline: 2px solid #4f46e5; + outline-offset: 2px; + } +`; + +export function NotFound() { + return ( + + + 404 + Page not found + + Sorry, we couldn't find the page you're looking for. + + + Go back home + + + + ); +} + + diff --git a/test/src/hooks/use-disclosure.ts b/test/src/hooks/use-disclosure.ts new file mode 100644 index 0000000..7bc8678 --- /dev/null +++ b/test/src/hooks/use-disclosure.ts @@ -0,0 +1,21 @@ +import { useCallback, useState } from 'react'; + +/** + * Hook for managing disclosure state (modals, dropdowns, etc.) + * Following bulletproof-react patterns + */ +export function useDisclosure(initialState = false) { + const [isOpen, setIsOpen] = useState(initialState); + + const open = useCallback(() => setIsOpen(true), []); + const close = useCallback(() => setIsOpen(false), []); + const toggle = useCallback(() => setIsOpen((prev) => !prev), []); + + return { + isOpen, + open, + close, + toggle, + }; +} + diff --git a/test/src/hooks/use-local-storage.ts b/test/src/hooks/use-local-storage.ts new file mode 100644 index 0000000..c83d12b --- /dev/null +++ b/test/src/hooks/use-local-storage.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useState } from 'react'; + +/** + * Hook for syncing state with localStorage + */ +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((prev: T) => T)) => void, () => void] { + // Get stored value or use initial value + const readValue = useCallback((): T => { + if (typeof window === 'undefined') { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? (JSON.parse(item) as T) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }, [initialValue, key]); + + const [storedValue, setStoredValue] = useState(readValue); + + // Set value to localStorage + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + if (typeof window === 'undefined') { + console.warn(`Cannot set localStorage key "${key}" during SSR`); + return; + } + + try { + const valueToStore = value instanceof Function ? value(storedValue) : value; + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + setStoredValue(valueToStore); + + // Dispatch storage event for other tabs/windows + window.dispatchEvent(new Event('local-storage')); + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }, + [key, storedValue] + ); + + // Remove value from localStorage + const removeValue = useCallback(() => { + if (typeof window === 'undefined') { + return; + } + + try { + window.localStorage.removeItem(key); + setStoredValue(initialValue); + } catch (error) { + console.warn(`Error removing localStorage key "${key}":`, error); + } + }, [initialValue, key]); + + // Listen for changes in other tabs/windows + useEffect(() => { + const handleStorageChange = () => { + setStoredValue(readValue()); + }; + + window.addEventListener('storage', handleStorageChange); + window.addEventListener('local-storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('local-storage', handleStorageChange); + }; + }, [readValue]); + + return [storedValue, setValue, removeValue]; +} + diff --git a/test/src/lib/api-client.ts b/test/src/lib/api-client.ts new file mode 100644 index 0000000..7c9dea9 --- /dev/null +++ b/test/src/lib/api-client.ts @@ -0,0 +1,115 @@ +/** + * API Client - Centralized HTTP client following bulletproof-react patterns + * Replace with your actual API configuration + */ + +export type RequestConfig = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + headers?: Record; + body?: unknown; + params?: Record; +}; + +export type ApiResponse = { + data: T; + status: number; +}; + +// Get API URL from environment - works for both Vite and Next.js +const getApiUrl = (): string => { + // Check for Vite environment variable (import.meta.env) + if (typeof import.meta !== 'undefined' && (import.meta as any).env) { + return (import.meta as any).env.VITE_API_URL || 'http://localhost:3001'; + } + // Check for Next.js environment variable (process.env) + if (typeof process !== 'undefined' && process.env) { + return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + } + // Fallback + return 'http://localhost:3001'; +}; + +const API_URL = getApiUrl(); + +/** + * Build URL with query parameters + */ +function buildUrl(endpoint: string, params?: Record): string { + const url = new URL(endpoint, API_URL); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, String(value)); + }); + } + + return url.toString(); +} + +/** + * Get authorization headers (customize based on your auth strategy) + */ +function getAuthHeaders(): Record { + const token = localStorage.getItem('token'); + + if (token) { + return { Authorization: `Bearer ${token}` }; + } + + return {}; +} + +/** + * Main API client function + */ +export async function apiClient( + endpoint: string, + config: RequestConfig = {} +): Promise> { + const { method = 'GET', headers = {}, body, params } = config; + + const url = buildUrl(endpoint, params); + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + ...headers, + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.message || `HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + return { + data, + status: response.status, + }; +} + +/** + * Convenience methods + */ +export const api = { + get: (endpoint: string, params?: Record) => + apiClient(endpoint, { method: 'GET', params }), + + post: (endpoint: string, body: unknown) => + apiClient(endpoint, { method: 'POST', body }), + + put: (endpoint: string, body: unknown) => + apiClient(endpoint, { method: 'PUT', body }), + + patch: (endpoint: string, body: unknown) => + apiClient(endpoint, { method: 'PATCH', body }), + + delete: (endpoint: string) => + apiClient(endpoint, { method: 'DELETE' }), +}; + diff --git a/test/src/lib/utils.ts b/test/src/lib/utils.ts new file mode 100644 index 0000000..2f549df --- /dev/null +++ b/test/src/lib/utils.ts @@ -0,0 +1,24 @@ +/** + * Format a date to a human-readable string + */ +export function formatDate(date: Date | string): string { + return new Intl.DateTimeFormat('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + }).format(new Date(date)); +} + +/** + * Delay execution for a specified number of milliseconds + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Generate a unique ID + */ +export function generateId(): string { + return Math.random().toString(36).substring(2, 9); +} diff --git a/test/src/main.tsx b/test/src/main.tsx new file mode 100644 index 0000000..8fb6db2 --- /dev/null +++ b/test/src/main.tsx @@ -0,0 +1,11 @@ +import { App } from '@/app/App'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); + + diff --git a/test/src/stores/auth.ts b/test/src/stores/auth.ts new file mode 100644 index 0000000..eaa71bc --- /dev/null +++ b/test/src/stores/auth.ts @@ -0,0 +1,48 @@ +import type { User } from '@/types/api'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +/** + * Auth store with persistence following bulletproof-react patterns + */ + +type AuthState = { + user: User | null; + token: string | null; + isAuthenticated: boolean; + setAuth: (user: User, token: string) => void; + logout: () => void; +}; + +export const useAuth = create()( + persist( + (set) => ({ + user: null, + token: null, + isAuthenticated: false, + + setAuth: (user, token) => + set({ + user, + token, + isAuthenticated: true, + }), + + logout: () => + set({ + user: null, + token: null, + isAuthenticated: false, + }), + }), + { + name: 'auth-storage', + partialize: (state) => ({ + user: state.user, + token: state.token, + isAuthenticated: state.isAuthenticated, + }), + } + ) +); + diff --git a/test/src/stores/index.ts b/test/src/stores/index.ts new file mode 100644 index 0000000..2a250da --- /dev/null +++ b/test/src/stores/index.ts @@ -0,0 +1,3 @@ +export { useAuth } from './auth'; +export { useNotifications, type Notification, type NotificationType } from './notifications'; + diff --git a/test/src/stores/notifications.ts b/test/src/stores/notifications.ts new file mode 100644 index 0000000..08381e2 --- /dev/null +++ b/test/src/stores/notifications.ts @@ -0,0 +1,54 @@ +import { generateId } from '@/lib/utils'; +import { create } from 'zustand'; + +/** + * Notification store following bulletproof-react patterns + */ + +export type NotificationType = 'info' | 'success' | 'warning' | 'error'; + +export type Notification = { + id: string; + type: NotificationType; + title: string; + message?: string; + duration?: number; +}; + +type NotificationsState = { + notifications: Notification[]; + addNotification: (notification: Omit) => void; + removeNotification: (id: string) => void; + clearNotifications: () => void; +}; + +export const useNotifications = create((set) => ({ + notifications: [], + + addNotification: (notification) => { + const id = generateId(); + const newNotification = { ...notification, id }; + + set((state) => ({ + notifications: [...state.notifications, newNotification], + })); + + // Auto-remove after duration (default 5 seconds) + const duration = notification.duration ?? 5000; + if (duration > 0) { + setTimeout(() => { + set((state) => ({ + notifications: state.notifications.filter((n) => n.id !== id), + })); + }, duration); + } + }, + + removeNotification: (id) => + set((state) => ({ + notifications: state.notifications.filter((n) => n.id !== id), + })), + + clearNotifications: () => set({ notifications: [] }), +})); + diff --git a/test/src/styles/globals.ts b/test/src/styles/globals.ts new file mode 100644 index 0000000..2285054 --- /dev/null +++ b/test/src/styles/globals.ts @@ -0,0 +1,28 @@ +import { createGlobalStyle } from 'styled-components'; + +export const GlobalStyles = createGlobalStyle` + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + html, + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + a { + color: inherit; + text-decoration: none; + } + + code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; + } +`; + diff --git a/test/src/types/api.ts b/test/src/types/api.ts new file mode 100644 index 0000000..bcda646 --- /dev/null +++ b/test/src/types/api.ts @@ -0,0 +1,47 @@ +/** + * Common API types following bulletproof-react patterns + */ + +export type BaseEntity = { + id: string; + createdAt: string; + updatedAt: string; +}; + +export type PaginatedResponse = { + data: T[]; + meta: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +}; + +export type ApiError = { + message: string; + statusCode: number; + errors?: Record; +}; + +export type User = BaseEntity & { + email: string; + name: string; + avatar?: string; + role: 'admin' | 'user'; +}; + +export type AuthResponse = { + user: User; + token: string; +}; + +export type LoginCredentials = { + email: string; + password: string; +}; + +export type RegisterCredentials = LoginCredentials & { + name: string; +}; + diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..cfdd5f9 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/test/tsconfig.node.json b/test/tsconfig.node.json new file mode 100644 index 0000000..58113cd --- /dev/null +++ b/test/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "composite": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} + diff --git a/test/vite.config.ts b/test/vite.config.ts new file mode 100644 index 0000000..b5a2b44 --- /dev/null +++ b/test/vite.config.ts @@ -0,0 +1,22 @@ +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 3000, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); +