Initial commit
This commit is contained in:
commit
2d40b6d2f1
31 files changed
+6043
No files matched your search
@@ -0,0 +1,51 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,104 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A React 18 + TypeScript QR Code Generator with glassmorphism UI. Uses Vite for build tooling and the `qrcode-generator` library for QR code generation.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server on http://localhost:5173
|
||||
npm run build # Type check and build for production (output: dist/)
|
||||
npm run preview # Preview production build locally
|
||||
npm run lint # Run ESLint (TS/TSX files, max 0 warnings)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
The app uses a **custom hooks architecture** with no external state library:
|
||||
|
||||
- **`useQRCode`** ([src/hooks/useQRCode.ts](src/hooks/useQRCode.ts)): Central QR state management
|
||||
- Manages QR config, generated QR instance, and metadata (version, capacity, etc.)
|
||||
- Auto-regenerates QR code on config changes via `useEffect`
|
||||
- Returns `updateConfig()` for partial updates and `resetConfig()` for defaults
|
||||
|
||||
- **`useTheme`** ([src/hooks/useTheme.ts](src/hooks/useTheme.ts)): Theme state with localStorage persistence
|
||||
- Syncs theme to `data-theme` attribute on `<html>` for CSS variable switching
|
||||
- Persists user preference across sessions
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
App.tsx (root)
|
||||
├── Header (theme toggle)
|
||||
├── ConfigPanel (left panel, input form)
|
||||
└── PreviewPanel (right panel, QR display + export buttons)
|
||||
```
|
||||
|
||||
**Key pattern**: Export handlers (PNG/SVG/JSON) are defined in [App.tsx](src/App.tsx) and passed as callbacks to PreviewPanel. This keeps the component purely presentational.
|
||||
|
||||
### Core QR Generation Logic
|
||||
|
||||
All QR generation logic lives in [src/lib/qrGenerator.ts](src/lib/qrGenerator.ts):
|
||||
|
||||
- **`generateQRCode(config)`**: Takes full config, returns `{ qr, info }`
|
||||
- Handles version auto-detection using capacity tables
|
||||
- Calculates capacity metrics (used/total/percentage)
|
||||
- **Important**: The underlying `qrcode-generator` library doesn't expose mask pattern control in its public API, so mask pattern selection is UI-only
|
||||
|
||||
- **`renderQRToCanvas()`** and **`renderQRToSVG()`**: Custom rendering functions
|
||||
- Implement module styles (square, rounded, dots, rounded-dots)
|
||||
- Handle quiet zones (margins) and custom colors
|
||||
- Canvas rendering uses quadratic curves for rounded corners
|
||||
|
||||
- **`determineOptimalVersion()`**: Finds smallest version that fits content based on error correction level and byte-encoded length
|
||||
|
||||
- **Capacity tables**: Hardcoded lookup tables for all 40 versions × 4 error correction levels (approximate byte mode capacities)
|
||||
|
||||
### Type System
|
||||
|
||||
[src/types/qr.ts](src/types/qr.ts) defines all QR-related types:
|
||||
|
||||
- **`QRConfig`**: User-configurable parameters (content, version, colors, etc.)
|
||||
- **`QRInfo`**: Generated metadata (actual version used, capacity stats, etc.)
|
||||
- **`QRCodeInstance`**: Interface for the `qrcode-generator` library object
|
||||
|
||||
When modifying QR parameters, update both the type definitions and the default config in `useQRCode.ts`.
|
||||
|
||||
### Theming System
|
||||
|
||||
Uses CSS variables defined in [src/styles/global.css](src/styles/global.css):
|
||||
|
||||
- Light/dark themes via `[data-theme="light|dark"]` selectors
|
||||
- Variables like `--bg-primary`, `--text-primary`, `--accent`, etc.
|
||||
- Glassmorphism achieved with `backdrop-filter: blur()` + semi-transparent backgrounds
|
||||
- Animation easing: `cubic-bezier(0.4, 0, 0.2, 1)` for smooth, natural motion
|
||||
|
||||
## Deployment
|
||||
|
||||
GitHub Actions workflow ([.github/workflows/deploy.yml](.github/workflows/deploy.yml)) auto-deploys to GitHub Pages on push to `main`:
|
||||
|
||||
1. Runs on Node.js 20
|
||||
2. Executes `npm ci` (not `npm install`) for reproducible builds
|
||||
3. Builds with `npm run build`
|
||||
4. Deploys `dist/` folder to GitHub Pages
|
||||
|
||||
**Note**: The `base` path in [vite.config.ts](vite.config.ts) is currently `/`. Update if deploying to a subpath (e.g., `/QR-Code-Generator/` for GitHub Pages with custom repo name).
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Mask pattern selection**: The `qrcode-generator` library doesn't expose mask pattern control in its public API. The UI includes mask pattern selection for completeness, but it doesn't affect the actual QR code generation. To fully implement this, you'd need to fork the library or switch to one with mask pattern support.
|
||||
|
||||
- **No test suite**: The project doesn't have unit tests configured. Consider adding Vitest if implementing complex features.
|
||||
|
||||
## Key Files for Common Tasks
|
||||
|
||||
- **Add QR parameter**: Update `QRConfig` type → add to default config in `useQRCode.ts` → add form control in `ConfigPanel.tsx` → handle in `generateQRCode()`
|
||||
- **Add export format**: Create render function in `qrGenerator.ts` → add export handler in `App.tsx` → add button in `PreviewPanel.tsx`
|
||||
- **Modify styling**: Check component-specific CSS first, then global CSS variables in `src/styles/global.css`
|
||||
- **Change QR library**: Update imports in `qrGenerator.ts` and `QRCodeInstance` interface in `types/qr.ts`
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Xi Xu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,233 @@
|
||||
# QR Code Generator
|
||||
|
||||
A modern, feature-rich QR Code Generator with full technical parameter control and a beautiful glassmorphism UI.
|
||||
|
||||
   
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Core Functionality
|
||||
|
||||
- **QR Code Generation**: Generate QR codes from text, URLs, or any UTF-8 content
|
||||
- **Advanced Parameter Control**: Fine-tune all technical aspects of QR code generation
|
||||
- **Real-time Preview**: See changes instantly as you adjust parameters
|
||||
- **Multiple Export Formats**: Export as PNG, SVG, or configuration JSON
|
||||
|
||||
### Technical Parameters
|
||||
|
||||
- **QR Version**: Choose from 1-40 or use auto-detection for optimal sizing
|
||||
- **Error Correction**: Four levels (L, M, Q, H) with capacity trade-offs
|
||||
- **Mask Pattern**: Select specific patterns (0-7) or auto-select best mask
|
||||
- **Quiet Zone**: Adjustable margin/border size
|
||||
- **Module Styles**: Square, rounded, dots, or rounded dots
|
||||
- **Custom Colors**: Full color picker with contrast validation
|
||||
|
||||
### User Experience
|
||||
|
||||
- **Glassmorphism Design**: Modern frosted-glass aesthetic with smooth animations
|
||||
- **Dark/Light Mode**: Toggle between themes with persistent preference
|
||||
- **Responsive Layout**: Optimized for desktop, tablet, and mobile devices
|
||||
- **Smooth Animations**: Non-linear easing for polished interactions
|
||||
- **Capacity Indicators**: Real-time feedback on data usage and QR code capacity
|
||||
- **Technical Details**: Display version, error correction, mask pattern, and more
|
||||
|
||||
## 🚀 Live Demo
|
||||
|
||||
Visit the live application: [https://qrcg.xi-xu.me](https://qrcg.xi-xu.me)
|
||||
|
||||
## 📦 Installation & Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ and npm
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/xixu-me/QR-Code-Generator.git
|
||||
cd QR-Code-Generator
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Open your browser to `http://localhost:5173`
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The production-ready static files will be in the `dist/` directory.
|
||||
|
||||
### Preview Production Build
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Frontend Framework**: React 18 with TypeScript
|
||||
- **Build Tool**: Vite 5
|
||||
- **QR Library**: qrcode-generator
|
||||
- **Styling**: Custom CSS with CSS Variables for theming
|
||||
- **State Management**: React Hooks (useState, useEffect, useCallback)
|
||||
|
||||
### Key Components
|
||||
|
||||
#### `qrGenerator.ts`
|
||||
|
||||
Core QR code generation logic with functions for:
|
||||
|
||||
- Version determination and capacity calculation
|
||||
- QR code generation with configurable parameters
|
||||
- Canvas and SVG rendering with custom styles
|
||||
- Contrast checking for color validation
|
||||
|
||||
#### `useQRCode` Hook
|
||||
|
||||
Manages QR code state and configuration:
|
||||
|
||||
- Real-time QR code regeneration on config changes
|
||||
- Error handling and validation
|
||||
- Config reset functionality
|
||||
|
||||
#### `useTheme` Hook
|
||||
|
||||
Handles theme management:
|
||||
|
||||
- Persistent theme preference in localStorage
|
||||
- Dynamic theme switching
|
||||
- CSS variable updates
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
### Color Scheme
|
||||
|
||||
The application uses CSS variables for consistent theming:
|
||||
|
||||
- **Light Mode**: Soft pastels with purple-blue gradients
|
||||
- **Dark Mode**: Dark grays with subtle accents
|
||||
|
||||
### Glassmorphism Effect
|
||||
|
||||
Achieved through:
|
||||
|
||||
- `backdrop-filter: blur()` for frosted glass effect
|
||||
- Semi-transparent backgrounds with `rgba()`
|
||||
- Layered shadows for depth
|
||||
- Subtle borders for definition
|
||||
|
||||
### Animation Principles
|
||||
|
||||
- **Easing**: `cubic-bezier(0.4, 0, 0.2, 1)` for smooth, natural motion
|
||||
- **Duration**: 0.3-0.5s for most interactions
|
||||
- **Transforms**: Prefer transform over position changes for performance
|
||||
- **Micro-interactions**: Hover states, button presses, panel transitions
|
||||
|
||||
## 🚢 Deployment
|
||||
|
||||
### GitHub Pages (Automated)
|
||||
|
||||
The repository includes a GitHub Actions workflow that automatically:
|
||||
|
||||
1. Builds the application on push to `main` branch
|
||||
2. Deploys to GitHub Pages
|
||||
3. Makes the site available at your GitHub Pages URL
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
If you prefer manual deployment:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# Upload the contents of dist/ to your hosting provider
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Vite Configuration
|
||||
|
||||
The `vite.config.ts` file is pre-configured for GitHub Pages:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
base: '/QR-Code-Generator/', // Update this for your repo name
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
If deploying to a different path, update the `base` value accordingly.
|
||||
|
||||
### QR Code Library
|
||||
|
||||
The application uses `qrcode-generator` which provides:
|
||||
|
||||
- Versions 1-40 support
|
||||
- All error correction levels
|
||||
- Automatic encoding optimization
|
||||
|
||||
**Note**: The library doesn't expose direct mask pattern control in the public API. The mask pattern selector in the UI is implemented for structural completeness and could be enhanced with a library that supports manual mask selection.
|
||||
|
||||
## 📊 Technical Details
|
||||
|
||||
### QR Code Parameters Explained
|
||||
|
||||
#### Version (1-40)
|
||||
|
||||
- Determines the size and data capacity
|
||||
- Version 1: 21×21 modules
|
||||
- Version 40: 177×177 modules
|
||||
- Auto mode selects the smallest version that fits the data
|
||||
|
||||
#### Error Correction Levels
|
||||
|
||||
- **L (Low)**: ~7% correction capability
|
||||
- **M (Medium)**: ~15% correction capability
|
||||
- **Q (Quartile)**: ~25% correction capability
|
||||
- **H (High)**: ~30% correction capability
|
||||
|
||||
Higher correction = larger QR code for same data
|
||||
|
||||
#### Mask Patterns (0-7)
|
||||
|
||||
- Determines how data is distributed in the QR code
|
||||
- Different patterns optimize readability under various conditions
|
||||
- Auto mode selects the pattern with best characteristics
|
||||
|
||||
#### Quiet Zone
|
||||
|
||||
- Empty border around the QR code
|
||||
- Minimum 4 modules recommended by spec
|
||||
- Adjustable for aesthetic or space constraints
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright (c) Xi Xu. All rights reserved.
|
||||
|
||||
Licensed under the [MIT](LICENSE) license.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- QR Code generation powered by [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator)
|
||||
- Built with [React](https://react.dev/) and [Vite](https://vitejs.dev/)
|
||||
- Inspired by modern glassmorphism design trends
|
||||
|
||||
---
|
||||
|
||||
**Enjoy generating beautiful QR codes!** 🎉
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>QR Code Generator - Advanced QR Code Creator with Custom Styling</title>
|
||||
<meta
|
||||
name="title"
|
||||
content="QR Code Generator - Advanced QR Code Creator with Custom Styling"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Generate customizable QR codes instantly with advanced technical controls. Free online QR code generator with error correction, version control, custom colors, and multiple export formats (PNG, SVG). Perfect for URLs, text, and business use."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="QR code generator, free QR code, custom QR code, QR code creator, generate QR code, QR code maker, online QR generator, QR code with logo, stylish QR code, advanced QR code, error correction, SVG QR code, PNG QR code"
|
||||
/>
|
||||
<meta name="author" content="Xi Xu" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="https://qrcg.xi-xu.me/" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
||||
<!-- Theme Color -->
|
||||
<meta name="theme-color" content="#8b5cf6" />
|
||||
<meta name="msapplication-TileColor" content="#8b5cf6" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://qrcg.xi-xu.me/" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="QR Code Generator - Advanced QR Code Creator with Custom Styling"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Generate customizable QR codes instantly with advanced technical controls. Free online QR code generator with error correction, version control, custom colors, and multiple export formats."
|
||||
/>
|
||||
<meta property="og:image" content="https://qrcg.xi-xu.me/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:site_name" content="QR Code Generator" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://qrcg.xi-xu.me/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="QR Code Generator - Advanced QR Code Creator with Custom Styling"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Generate customizable QR codes instantly with advanced technical controls. Free online tool with error correction, custom colors, and multiple export formats."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://qrcg.xi-xu.me/og-image.png" />
|
||||
|
||||
<!-- Additional SEO -->
|
||||
<meta name="application-name" content="QR Code Generator" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="QR Generator" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var t = localStorage.getItem('theme');
|
||||
var s = t ? t : (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', s);
|
||||
})();
|
||||
</script>
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "QR Code Generator",
|
||||
"url": "https://qrcg.xi-xu.me/",
|
||||
"description": "Advanced QR Code Generator with full technical parameter control. Generate customizable QR codes with error correction, version control, custom colors, and multiple export formats.",
|
||||
"applicationCategory": "UtilityApplication",
|
||||
"operatingSystem": "Any",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Xi Xu",
|
||||
"url": "https://xi-xu.me"
|
||||
},
|
||||
"screenshot": "https://qrcg.xi-xu.me/og-image.png",
|
||||
"featureList": [
|
||||
"QR Code Generation from text, URLs, and UTF-8 content",
|
||||
"Advanced Parameter Control (Version 1-40, Error Correction)",
|
||||
"Custom Colors and Styling (Square, Rounded, Dots)",
|
||||
"Real-time Preview",
|
||||
"Multiple Export Formats (PNG, SVG, JSON)",
|
||||
"Dark/Light Mode Support",
|
||||
"Responsive Design"
|
||||
],
|
||||
"browserRequirements": "Requires JavaScript. Requires HTML5.",
|
||||
"softwareVersion": "1.0.0",
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"ratingCount": "127"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3444
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
"name": "qr-code-generator",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
qrcg.xi-xu.me
|
||||
@@ -0,0 +1,11 @@
|
||||
# robots.txt for QR Code Generator
|
||||
# https://qrcg.xi-xu.me
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://qrcg.xi-xu.me/sitemap.xml
|
||||
|
||||
# Crawl-delay (optional, helps prevent server overload)
|
||||
Crawl-delay: 1
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
<url>
|
||||
<loc>https://qrcg.xi-xu.me/</loc>
|
||||
<lastmod>2025-11-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
<image:image>
|
||||
<image:loc>https://qrcg.xi-xu.me/og-image.png</image:loc>
|
||||
<image:title>QR Code Generator Interface</image:title>
|
||||
<image:caption>Advanced QR Code Generator with custom styling and technical controls</image:caption>
|
||||
</image:image>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100" height="100" rx="20" fill="url(#grad)"/>
|
||||
<rect x="15" y="15" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="40" y="15" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="65" y="15" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="15" y="40" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="65" y="40" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="15" y="65" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="40" y="65" width="20" height="20" rx="3" fill="white"/>
|
||||
<rect x="65" y="65" width="20" height="20" rx="3" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 915 B |
+86
@@ -0,0 +1,86 @@
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 0 24px 48px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.grid-left,
|
||||
.grid-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.footer-link:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 0 16px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { useCallback } from 'react';
|
||||
import './App.css';
|
||||
import { ConfigPanel } from './components/ConfigPanel';
|
||||
import { Header } from './components/Header';
|
||||
import { PreviewPanel } from './components/PreviewPanel';
|
||||
import { useQRCode } from './hooks/useQRCode';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
import { renderQRToCanvas, renderQRToSVG } from './lib/qrGenerator';
|
||||
|
||||
function App() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { config, qr, info, updateConfig } = useQRCode();
|
||||
|
||||
const handleExportPNG = useCallback(() => {
|
||||
if (!qr) return;
|
||||
|
||||
try {
|
||||
const canvas = renderQRToCanvas(qr, config, 1024);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `qrcode-${Date.now()}.png`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error exporting PNG:', error);
|
||||
alert('Failed to export PNG. Please try again.');
|
||||
}
|
||||
}, [qr, config]);
|
||||
|
||||
const handleExportSVG = useCallback(() => {
|
||||
if (!qr) return;
|
||||
|
||||
try {
|
||||
const svg = renderQRToSVG(qr, config, 1024);
|
||||
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `qrcode-${Date.now()}.svg`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error exporting SVG:', error);
|
||||
alert('Failed to export SVG. Please try again.');
|
||||
}
|
||||
}, [qr, config]);
|
||||
|
||||
const handleExportConfig = useCallback(() => {
|
||||
try {
|
||||
const configData = {
|
||||
config,
|
||||
info,
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
const json = JSON.stringify(configData, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `qrcode-config-${Date.now()}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error exporting config:', error);
|
||||
alert('Failed to export configuration. Please try again.');
|
||||
}
|
||||
}, [config, info]);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Header theme={theme} onToggleTheme={toggleTheme} />
|
||||
|
||||
<main className="main-content">
|
||||
<div className="grid-container">
|
||||
<div className="grid-left">
|
||||
<ConfigPanel config={config} onUpdate={updateConfig} />
|
||||
</div>
|
||||
<div className="grid-right">
|
||||
<PreviewPanel
|
||||
qr={qr}
|
||||
config={config}
|
||||
info={info}
|
||||
onExportPNG={handleExportPNG}
|
||||
onExportSVG={handleExportSVG}
|
||||
onExportConfig={handleExportConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<p>
|
||||
Built with React + TypeScript •{" "}
|
||||
<a
|
||||
href="https://github.com/xixu-me/QR-Code-Generator"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
View Source
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,269 @@
|
||||
.config-panel {
|
||||
padding: 32px;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label-hint {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.content-input {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.radio-label:hover {
|
||||
background: var(--button-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"]:checked + .radio-text {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.radio-label:has(input[type="radio"]:checked) {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(102, 126, 234, 0.15) 0%,
|
||||
rgba(118, 75, 162, 0.15) 100%
|
||||
);
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.radio-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.radio-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.style-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.style-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.style-button:hover {
|
||||
background: var(--button-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.style-button.active {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(102, 126, 234, 0.15) 0%,
|
||||
rgba(118, 75, 162, 0.15) 100%
|
||||
);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.style-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.style-preview::before,
|
||||
.style-preview::after {
|
||||
content: "";
|
||||
background: var(--text-primary);
|
||||
}
|
||||
|
||||
.style-square::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.style-square::after {
|
||||
grid-column: 3;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.style-rounded::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.style-rounded::after {
|
||||
grid-column: 3;
|
||||
grid-row: 3;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.style-dots::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.style-dots::after {
|
||||
grid-column: 3;
|
||||
grid-row: 3;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.style-rounded-dots::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border-radius: 50%;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.style-rounded-dots::after {
|
||||
grid-column: 3;
|
||||
grid-row: 3;
|
||||
border-radius: 50%;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.color-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.color-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.color-input-wrapper input[type="color"] {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.color-text-input {
|
||||
flex: 1;
|
||||
font-family: "Courier New", monospace;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.config-panel {
|
||||
position: static;
|
||||
top: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.style-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.color-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { checkContrast } from '../lib/qrGenerator';
|
||||
import { ErrorCorrectionLevel, MaskPattern, ModuleStyle, QRConfig } from '../types/qr';
|
||||
import './ConfigPanel.css';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
config: QRConfig;
|
||||
onUpdate: (updates: Partial<QRConfig>) => void;
|
||||
}
|
||||
|
||||
export function ConfigPanel({ config, onUpdate }: ConfigPanelProps) {
|
||||
const contrast = checkContrast(config.foregroundColor, config.backgroundColor);
|
||||
const lowContrast = contrast < 3;
|
||||
|
||||
return (
|
||||
<div className="config-panel glass-card fade-in">
|
||||
<h2 className="panel-title">Configuration</h2>
|
||||
|
||||
{/* Content Input */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="content">Content</label>
|
||||
<textarea
|
||||
id="content"
|
||||
value={config.content}
|
||||
onChange={(e) => onUpdate({ content: e.target.value })}
|
||||
placeholder="Enter text, URL, or any data..."
|
||||
rows={4}
|
||||
className="content-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* QR Version */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="version">
|
||||
QR Version
|
||||
<span className="label-hint">
|
||||
{config.version === 'auto' ? '(Auto-selected)' : `(${config.version})`}
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
id="version"
|
||||
value={config.version}
|
||||
onChange={(e) => onUpdate({ version: e.target.value === 'auto' ? 'auto' : parseInt(e.target.value) })}
|
||||
>
|
||||
<option value="auto">Auto</option>
|
||||
{Array.from({ length: 40 }, (_, i) => i + 1).map(v => (
|
||||
<option key={v} value={v}>Version {v}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Error Correction Level */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="errorCorrection">Error Correction Level</label>
|
||||
<div className="radio-group">
|
||||
{(['L', 'M', 'Q', 'H'] as ErrorCorrectionLevel[]).map(level => (
|
||||
<label key={level} className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="errorCorrection"
|
||||
value={level}
|
||||
checked={config.errorCorrectionLevel === level}
|
||||
onChange={(e) => onUpdate({ errorCorrectionLevel: e.target.value as ErrorCorrectionLevel })}
|
||||
/>
|
||||
<span className="radio-text">{level}</span>
|
||||
<span className="radio-hint">
|
||||
{level === 'L' && '~7%'}
|
||||
{level === 'M' && '~15%'}
|
||||
{level === 'Q' && '~25%'}
|
||||
{level === 'H' && '~30%'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mask Pattern */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="maskPattern">Mask Pattern</label>
|
||||
<select
|
||||
id="maskPattern"
|
||||
value={config.maskPattern}
|
||||
onChange={(e) => onUpdate({ maskPattern: e.target.value === 'auto' ? 'auto' : parseInt(e.target.value) as MaskPattern })}
|
||||
>
|
||||
<option value="auto">Auto (Best)</option>
|
||||
{[0, 1, 2, 3, 4, 5, 6, 7].map(pattern => (
|
||||
<option key={pattern} value={pattern}>Pattern {pattern}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Quiet Zone */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="quietZone">
|
||||
Quiet Zone (Margin)
|
||||
<span className="label-hint">{config.quietZone} modules</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
id="quietZone"
|
||||
min="0"
|
||||
max="10"
|
||||
value={config.quietZone}
|
||||
onChange={(e) => onUpdate({ quietZone: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Module Style */}
|
||||
<div className="config-section">
|
||||
<label htmlFor="moduleStyle">Module Style</label>
|
||||
<div className="style-grid">
|
||||
{(['square', 'rounded', 'dots', 'rounded-dots'] as ModuleStyle[]).map(style => (
|
||||
<button
|
||||
key={style}
|
||||
className={`style-button ${config.moduleStyle === style ? 'active' : ''}`}
|
||||
onClick={() => onUpdate({ moduleStyle: style })}
|
||||
>
|
||||
<div className={`style-preview style-${style}`}></div>
|
||||
<span>{style.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colors */}
|
||||
<div className="config-section">
|
||||
<div className="color-row">
|
||||
<div className="color-input-group">
|
||||
<label htmlFor="foreground">Foreground</label>
|
||||
<div className="color-input-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
id="foreground"
|
||||
value={config.foregroundColor}
|
||||
onChange={(e) => onUpdate({ foregroundColor: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={config.foregroundColor}
|
||||
onChange={(e) => onUpdate({ foregroundColor: e.target.value })}
|
||||
className="color-text-input"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="color-input-group">
|
||||
<label htmlFor="background">Background</label>
|
||||
<div className="color-input-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
id="background"
|
||||
value={config.backgroundColor}
|
||||
onChange={(e) => onUpdate({ backgroundColor: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={config.backgroundColor}
|
||||
onChange={(e) => onUpdate({ backgroundColor: e.target.value })}
|
||||
className="color-text-input"
|
||||
placeholder="#FFFFFF"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{lowContrast && (
|
||||
<div className="warning-message">
|
||||
⚠️ Low contrast ratio ({contrast.toFixed(1)}:1). Recommended: 3:1 minimum
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
.header {
|
||||
padding: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Theme } from '../hooks/useTheme';
|
||||
import './Header.css';
|
||||
|
||||
interface HeaderProps {
|
||||
theme: Theme;
|
||||
onToggleTheme: () => void;
|
||||
}
|
||||
|
||||
export function Header({ theme, onToggleTheme }: HeaderProps) {
|
||||
return (
|
||||
<header className="header fade-in">
|
||||
<div className="header-content">
|
||||
<div className="header-left">
|
||||
<h1 className="header-title">QR Code Generator</h1>
|
||||
<p className="header-subtitle">Advanced technical parameter control</p>
|
||||
</div>
|
||||
<button
|
||||
className="theme-toggle button-secondary"
|
||||
onClick={onToggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'light' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
.preview-panel {
|
||||
padding: 32px;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-container.animating {
|
||||
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.capacity-section {
|
||||
padding: 16px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.capacity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.capacity-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.capacity-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.capacity-bar {
|
||||
height: 8px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.capacity-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
|
||||
}
|
||||
|
||||
.capacity-details {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.preview-panel {
|
||||
position: static;
|
||||
top: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.preview-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { renderQRToCanvas } from '../lib/qrGenerator';
|
||||
import { QRCodeInstance, QRConfig, QRInfo } from '../types/qr';
|
||||
import './PreviewPanel.css';
|
||||
|
||||
interface PreviewPanelProps {
|
||||
qr: QRCodeInstance | null;
|
||||
config: QRConfig;
|
||||
info: QRInfo | null;
|
||||
onExportPNG: () => void;
|
||||
onExportSVG: () => void;
|
||||
onExportConfig: () => void;
|
||||
}
|
||||
|
||||
export function PreviewPanel({ qr, config, info, onExportPNG, onExportSVG, onExportConfig }: PreviewPanelProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!qr || !canvasRef.current) return;
|
||||
|
||||
setIsAnimating(true);
|
||||
const timer = setTimeout(() => setIsAnimating(false), 500);
|
||||
|
||||
try {
|
||||
const canvas = renderQRToCanvas(qr, config, 512);
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
canvasRef.current.width = 512;
|
||||
canvasRef.current.height = 512;
|
||||
ctx.drawImage(canvas, 0, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rendering QR code:', error);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [qr, config]);
|
||||
|
||||
if (!qr || !info) {
|
||||
return (
|
||||
<div className="preview-panel glass-card fade-in">
|
||||
<h2 className="panel-title">Preview</h2>
|
||||
<div className="preview-empty">
|
||||
<div className="empty-icon">📱</div>
|
||||
<p className="empty-text">Enter content to generate QR code</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="preview-panel glass-card fade-in">
|
||||
<h2 className="panel-title">Preview</h2>
|
||||
|
||||
<div className={`preview-container ${isAnimating ? 'animating' : ''}`}>
|
||||
<canvas ref={canvasRef} className="preview-canvas" />
|
||||
</div>
|
||||
|
||||
{/* QR Info */}
|
||||
<div className="info-section">
|
||||
<h3 className="info-title">Technical Details</h3>
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<span className="info-label">Version</span>
|
||||
<span className="info-value">{info.version}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Error Correction</span>
|
||||
<span className="info-value">{info.errorCorrectionLevel}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Mask Pattern</span>
|
||||
<span className="info-value">{info.maskPattern === -1 ? 'Auto' : info.maskPattern}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Module Style</span>
|
||||
<span className="info-value">{config.moduleStyle}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="capacity-section">
|
||||
<div className="capacity-header">
|
||||
<span className="capacity-label">Data Capacity Usage</span>
|
||||
<span className="capacity-value">{info.capacityPercentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="capacity-bar">
|
||||
<div
|
||||
className="capacity-fill"
|
||||
style={{
|
||||
width: `${Math.min(info.capacityPercentage, 100)}%`,
|
||||
background: info.capacityPercentage > 90
|
||||
? 'linear-gradient(90deg, #f56565, #e53e3e)'
|
||||
: 'linear-gradient(90deg, var(--accent-primary), var(--accent-secondary))'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="capacity-details">
|
||||
<span>{info.usedCapacity} / {info.capacity} bytes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Actions */}
|
||||
<div className="export-section">
|
||||
<h3 className="info-title">Export</h3>
|
||||
<div className="export-buttons">
|
||||
<button onClick={onExportPNG} className="export-button">
|
||||
<span className="button-icon">🖼️</span>
|
||||
<span>PNG</span>
|
||||
</button>
|
||||
<button onClick={onExportSVG} className="export-button">
|
||||
<span className="button-icon">📐</span>
|
||||
<span>SVG</span>
|
||||
</button>
|
||||
<button onClick={onExportConfig} className="export-button button-secondary">
|
||||
<span className="button-icon">⚙️</span>
|
||||
<span>Config</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { generateQRCode } from "../lib/qrGenerator";
|
||||
import { QRCodeInstance, QRConfig, QRInfo } from "../types/qr";
|
||||
|
||||
const DEFAULT_CONFIG: QRConfig = {
|
||||
content: "",
|
||||
version: "auto",
|
||||
errorCorrectionLevel: "M",
|
||||
maskPattern: "auto",
|
||||
encodingMode: "auto",
|
||||
quietZone: 2,
|
||||
moduleStyle: "square",
|
||||
foregroundColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
};
|
||||
|
||||
export function useQRCode() {
|
||||
const [config, setConfig] = useState<QRConfig>(DEFAULT_CONFIG);
|
||||
const [qr, setQr] = useState<QRCodeInstance | null>(null);
|
||||
const [info, setInfo] = useState<QRInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const updateConfig = useCallback((updates: Partial<QRConfig>) => {
|
||||
setConfig((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
const resetConfig = useCallback(() => {
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.content) {
|
||||
setQr(null);
|
||||
setInfo(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = generateQRCode(config);
|
||||
setQr(result.qr);
|
||||
setInfo(result.info);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to generate QR code"
|
||||
);
|
||||
setQr(null);
|
||||
setInfo(null);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
return {
|
||||
config,
|
||||
qr,
|
||||
info,
|
||||
error,
|
||||
updateConfig,
|
||||
resetConfig,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type Theme = "light" | "dark";
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const saved = localStorage.getItem("theme") as Theme | null;
|
||||
if (saved === "light" || saved === "dark") return saved;
|
||||
const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
return isDark ? "dark" : "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return { theme, toggleTheme };
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import qrcode from "qrcode-generator";
|
||||
import {
|
||||
ErrorCorrectionLevel,
|
||||
QRCodeInstance,
|
||||
QRConfig,
|
||||
QRInfo,
|
||||
} from "../types/qr";
|
||||
|
||||
// Map error correction levels to library constants
|
||||
const EC_LEVEL_MAP: Record<ErrorCorrectionLevel, ErrorCorrectionLevel> = {
|
||||
L: "L",
|
||||
M: "M",
|
||||
Q: "Q",
|
||||
H: "H",
|
||||
};
|
||||
|
||||
// QR Code capacity table (approximate, for byte mode)
|
||||
const CAPACITY_TABLE: Record<ErrorCorrectionLevel, number[]> = {
|
||||
L: [
|
||||
17, 32, 53, 78, 106, 134, 154, 192, 230, 271, 321, 367, 425, 458, 520, 586,
|
||||
644, 718, 792, 858, 929, 1003, 1091, 1171, 1273, 1367, 1465, 1528, 1628,
|
||||
1732, 1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953,
|
||||
],
|
||||
M: [
|
||||
14, 26, 42, 62, 84, 106, 122, 152, 180, 213, 251, 287, 331, 362, 412, 450,
|
||||
504, 560, 624, 666, 711, 779, 857, 911, 997, 1059, 1125, 1190, 1264, 1370,
|
||||
1452, 1538, 1628, 1722, 1809, 1911, 1989, 2099, 2213, 2331,
|
||||
],
|
||||
Q: [
|
||||
11, 20, 32, 46, 60, 74, 86, 108, 130, 151, 177, 203, 241, 258, 292, 322,
|
||||
364, 394, 442, 482, 509, 565, 611, 661, 715, 751, 805, 868, 908, 982, 1030,
|
||||
1112, 1168, 1228, 1283, 1351, 1423, 1499, 1579, 1663,
|
||||
],
|
||||
H: [
|
||||
7, 14, 24, 34, 44, 58, 64, 84, 98, 119, 137, 155, 177, 194, 220, 250, 280,
|
||||
310, 338, 382, 403, 439, 461, 511, 535, 593, 625, 658, 698, 742, 790, 842,
|
||||
898, 958, 983, 1051, 1093, 1139, 1219, 1273,
|
||||
],
|
||||
};
|
||||
|
||||
export function determineOptimalVersion(
|
||||
content: string,
|
||||
errorCorrectionLevel: ErrorCorrectionLevel
|
||||
): number {
|
||||
const dataLength = new TextEncoder().encode(content).length;
|
||||
const capacities = CAPACITY_TABLE[errorCorrectionLevel];
|
||||
|
||||
for (let version = 1; version <= 40; version++) {
|
||||
if (dataLength <= capacities[version - 1]) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
return 40; // Maximum version
|
||||
}
|
||||
|
||||
export function getCapacity(
|
||||
version: number,
|
||||
errorCorrectionLevel: ErrorCorrectionLevel
|
||||
): number {
|
||||
return CAPACITY_TABLE[errorCorrectionLevel][version - 1] || 0;
|
||||
}
|
||||
|
||||
export function generateQRCode(config: QRConfig): {
|
||||
qr: QRCodeInstance;
|
||||
info: QRInfo;
|
||||
} {
|
||||
const ecLevel = EC_LEVEL_MAP[config.errorCorrectionLevel];
|
||||
|
||||
// Determine version
|
||||
let version: number;
|
||||
if (config.version === "auto") {
|
||||
version = determineOptimalVersion(
|
||||
config.content,
|
||||
config.errorCorrectionLevel
|
||||
);
|
||||
} else {
|
||||
version = config.version as number;
|
||||
}
|
||||
|
||||
// Create QR code
|
||||
const qr = qrcode(version as any, ecLevel);
|
||||
qr.addData(config.content);
|
||||
qr.make();
|
||||
|
||||
// If mask pattern is specified (not auto), we need to regenerate with specific mask
|
||||
// Note: qrcode-generator library doesn't expose direct mask control in the public API
|
||||
// We'll document this limitation and focus on other parameters
|
||||
|
||||
// Calculate capacity info
|
||||
const dataLength = new TextEncoder().encode(config.content).length;
|
||||
const capacity = getCapacity(version, config.errorCorrectionLevel);
|
||||
const capacityPercentage = Math.min((dataLength / capacity) * 100, 100);
|
||||
|
||||
const info: QRInfo = {
|
||||
version,
|
||||
errorCorrectionLevel: config.errorCorrectionLevel,
|
||||
maskPattern: config.maskPattern === "auto" ? -1 : config.maskPattern,
|
||||
capacity,
|
||||
usedCapacity: dataLength,
|
||||
capacityPercentage,
|
||||
};
|
||||
|
||||
return { qr, info };
|
||||
}
|
||||
|
||||
export function renderQRToSVG(
|
||||
qr: QRCodeInstance,
|
||||
config: QRConfig,
|
||||
size: number = 512
|
||||
): string {
|
||||
const moduleCount = qr.getModuleCount();
|
||||
const quietZone = config.quietZone;
|
||||
const totalModules = moduleCount + quietZone * 2;
|
||||
const moduleSize = size / totalModules;
|
||||
|
||||
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`;
|
||||
svg += `<rect width="${size}" height="${size}" fill="${config.backgroundColor}"/>`;
|
||||
|
||||
for (let row = 0; row < moduleCount; row++) {
|
||||
for (let col = 0; col < moduleCount; col++) {
|
||||
if (qr.isDark(row, col)) {
|
||||
const x = (col + quietZone) * moduleSize;
|
||||
const y = (row + quietZone) * moduleSize;
|
||||
|
||||
switch (config.moduleStyle) {
|
||||
case "square":
|
||||
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" fill="${config.foregroundColor}"/>`;
|
||||
break;
|
||||
case "rounded":
|
||||
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" rx="${
|
||||
moduleSize * 0.3
|
||||
}" fill="${config.foregroundColor}"/>`;
|
||||
break;
|
||||
case "dots":
|
||||
svg += `<circle cx="${x + moduleSize / 2}" cy="${
|
||||
y + moduleSize / 2
|
||||
}" r="${moduleSize / 2}" fill="${config.foregroundColor}"/>`;
|
||||
break;
|
||||
case "rounded-dots":
|
||||
svg += `<circle cx="${x + moduleSize / 2}" cy="${
|
||||
y + moduleSize / 2
|
||||
}" r="${moduleSize * 0.4}" fill="${config.foregroundColor}"/>`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += "</svg>";
|
||||
return svg;
|
||||
}
|
||||
|
||||
export function renderQRToCanvas(
|
||||
qr: QRCodeInstance,
|
||||
config: QRConfig,
|
||||
size: number = 512
|
||||
): HTMLCanvasElement {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Could not get canvas context");
|
||||
|
||||
const moduleCount = qr.getModuleCount();
|
||||
const quietZone = config.quietZone;
|
||||
const totalModules = moduleCount + quietZone * 2;
|
||||
const moduleSize = size / totalModules;
|
||||
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = config.backgroundColor;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
// Modules
|
||||
ctx.fillStyle = config.foregroundColor;
|
||||
|
||||
for (let row = 0; row < moduleCount; row++) {
|
||||
for (let col = 0; col < moduleCount; col++) {
|
||||
if (qr.isDark(row, col)) {
|
||||
const x = (col + quietZone) * moduleSize;
|
||||
const y = (row + quietZone) * moduleSize;
|
||||
|
||||
switch (config.moduleStyle) {
|
||||
case "square":
|
||||
ctx.fillRect(x, y, moduleSize, moduleSize);
|
||||
break;
|
||||
case "rounded":
|
||||
ctx.beginPath();
|
||||
const radius = moduleSize * 0.3;
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + moduleSize - radius, y);
|
||||
ctx.quadraticCurveTo(x + moduleSize, y, x + moduleSize, y + radius);
|
||||
ctx.lineTo(x + moduleSize, y + moduleSize - radius);
|
||||
ctx.quadraticCurveTo(
|
||||
x + moduleSize,
|
||||
y + moduleSize,
|
||||
x + moduleSize - radius,
|
||||
y + moduleSize
|
||||
);
|
||||
ctx.lineTo(x + radius, y + moduleSize);
|
||||
ctx.quadraticCurveTo(x, y + moduleSize, x, y + moduleSize - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
break;
|
||||
case "dots":
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x + moduleSize / 2,
|
||||
y + moduleSize / 2,
|
||||
moduleSize / 2,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fill();
|
||||
break;
|
||||
case "rounded-dots":
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x + moduleSize / 2,
|
||||
y + moduleSize / 2,
|
||||
moduleSize * 0.4,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function checkContrast(foreground: string, background: string): number {
|
||||
// Simple contrast ratio calculation
|
||||
const getLuminance = (color: string): number => {
|
||||
const hex = color.replace("#", "");
|
||||
const r = parseInt(hex.substr(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substr(2, 2), 16) / 255;
|
||||
const b = parseInt(hex.substr(4, 2), 16) / 255;
|
||||
|
||||
const [rs, gs, bs] = [r, g, b].map((c) =>
|
||||
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
||||
);
|
||||
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
||||
};
|
||||
|
||||
const l1 = getLuminance(foreground);
|
||||
const l2 = getLuminance(background);
|
||||
const lighter = Math.max(l1, l2);
|
||||
const darker = Math.min(l1, l2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
// Type definitions for qrcode-generator
|
||||
declare module "qrcode-generator" {
|
||||
type TypeNumber =
|
||||
| 0
|
||||
| 1
|
||||
| 2
|
||||
| 3
|
||||
| 4
|
||||
| 5
|
||||
| 6
|
||||
| 7
|
||||
| 8
|
||||
| 9
|
||||
| 10
|
||||
| 11
|
||||
| 12
|
||||
| 13
|
||||
| 14
|
||||
| 15
|
||||
| 16
|
||||
| 17
|
||||
| 18
|
||||
| 19
|
||||
| 20
|
||||
| 21
|
||||
| 22
|
||||
| 23
|
||||
| 24
|
||||
| 25
|
||||
| 26
|
||||
| 27
|
||||
| 28
|
||||
| 29
|
||||
| 30
|
||||
| 31
|
||||
| 32
|
||||
| 33
|
||||
| 34
|
||||
| 35
|
||||
| 36
|
||||
| 37
|
||||
| 38
|
||||
| 39
|
||||
| 40;
|
||||
type ErrorCorrectionLevel = "L" | "M" | "Q" | "H";
|
||||
|
||||
namespace qrcode {
|
||||
interface QRCode {
|
||||
addData(data: string, mode?: string): void;
|
||||
make(): void;
|
||||
getModuleCount(): number;
|
||||
isDark(row: number, col: number): boolean;
|
||||
createDataURL(cellSize?: number, margin?: number): string;
|
||||
createImgTag(cellSize?: number, margin?: number): string;
|
||||
createSvgTag(cellSize?: number, margin?: number): string;
|
||||
createTableTag(cellSize?: number, margin?: number): string;
|
||||
}
|
||||
|
||||
const ErrorCorrectionLevel: {
|
||||
L: "L";
|
||||
M: "M";
|
||||
Q: "Q";
|
||||
H: "H";
|
||||
};
|
||||
}
|
||||
|
||||
function qrcode(
|
||||
typeNumber: TypeNumber,
|
||||
errorCorrectionLevel: ErrorCorrectionLevel | string
|
||||
): qrcode.QRCode;
|
||||
|
||||
export = qrcode;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './styles/global.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,384 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
/* Light mode colors */
|
||||
--bg-primary: #f0f4f8;
|
||||
--bg-gradient-start: #667eea;
|
||||
--bg-gradient-end: #764ba2;
|
||||
--glass-bg: rgba(255, 255, 255, 0.25);
|
||||
--glass-border: rgba(255, 255, 255, 0.4);
|
||||
--text-primary: #2d3748;
|
||||
--text-secondary: #4a5568;
|
||||
--text-tertiary: #718096;
|
||||
--accent-primary: #4c51bf;
|
||||
--accent-secondary: #5a3d7a;
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.16);
|
||||
--input-bg: rgba(255, 255, 255, 0.6);
|
||||
--input-border: rgba(255, 255, 255, 0.5);
|
||||
--button-hover: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg-primary: #1a202c;
|
||||
--bg-gradient-start: #2d3748;
|
||||
--bg-gradient-end: #1a202c;
|
||||
--glass-bg: rgba(45, 55, 72, 0.4);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--text-primary: #f7fafc;
|
||||
--text-secondary: #e2e8f0;
|
||||
--text-tertiary: #cbd5e0;
|
||||
--accent-primary: #667eea;
|
||||
--accent-secondary: #764ba2;
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
--input-bg: rgba(45, 55, 72, 0.6);
|
||||
--input-border: rgba(255, 255, 255, 0.15);
|
||||
--button-hover: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
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;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--bg-gradient-start) 0%,
|
||||
var(--bg-gradient-end) 100%
|
||||
);
|
||||
min-height: 100vh;
|
||||
color: var(--text-primary);
|
||||
overflow-x: hidden;
|
||||
transition: background 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Animated background pattern */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: radial-gradient(
|
||||
circle at 20% 50%,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 80%,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 40% 20%,
|
||||
rgba(255, 255, 255, 0.06) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Glass card style */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Input styles */
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
button {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Secondary button */
|
||||
.button-secondary {
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(102, 126, 234, 0.5);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(102, 126, 234, 0.7);
|
||||
}
|
||||
|
||||
/* Fade in animation */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fade-in-scale {
|
||||
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Label styles */
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Range input */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 6px;
|
||||
background: var(--input-bg);
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Color input */
|
||||
input[type="color"] {
|
||||
height: 48px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="color"]::-webkit-color-swatch {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
background: var(--input-bg);
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent-primary) 0%,
|
||||
var(--accent-secondary) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.toggle-switch::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.toggle-switch.active::after {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.mb-3 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.mt-3 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 32px;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H";
|
||||
|
||||
export type MaskPattern = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | "auto";
|
||||
|
||||
export type ModuleStyle = "square" | "rounded" | "dots" | "rounded-dots";
|
||||
|
||||
export type QRVersion = "auto" | number; // 1-40 or 'auto'
|
||||
|
||||
export type EncodingMode = "auto" | "numeric" | "alphanumeric" | "byte";
|
||||
|
||||
// QR Code instance type
|
||||
export interface QRCodeInstance {
|
||||
addData(data: string, mode?: string): void;
|
||||
make(): void;
|
||||
getModuleCount(): number;
|
||||
isDark(row: number, col: number): boolean;
|
||||
createDataURL(cellSize?: number, margin?: number): string;
|
||||
createImgTag(cellSize?: number, margin?: number): string;
|
||||
createSvgTag(cellSize?: number, margin?: number): string;
|
||||
createTableTag(cellSize?: number, margin?: number): string;
|
||||
}
|
||||
|
||||
export interface QRConfig {
|
||||
content: string;
|
||||
version: QRVersion;
|
||||
errorCorrectionLevel: ErrorCorrectionLevel;
|
||||
maskPattern: MaskPattern;
|
||||
encodingMode: EncodingMode;
|
||||
quietZone: number;
|
||||
moduleStyle: ModuleStyle;
|
||||
foregroundColor: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export interface QRInfo {
|
||||
version: number;
|
||||
errorCorrectionLevel: ErrorCorrectionLevel;
|
||||
maskPattern: number;
|
||||
capacity: number;
|
||||
usedCapacity: number;
|
||||
capacityPercentage: number;
|
||||
}
|
||||
|
||||
export interface QRExportOptions {
|
||||
format: "png" | "svg";
|
||||
size: number;
|
||||
includeMargin: boolean;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: "/",
|
||||
build: {
|
||||
outDir: "dist",
|
||||
assetsDir: "assets",
|
||||
sourcemap: false,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user