Initial commit
This commit is contained in:
commit
4b6e2be336
27 files changed
+7174
No files matched your search
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 repository
|
||||
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: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- 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,8 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -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,248 @@
|
||||
# 🔑 UUID Playground
|
||||
|
||||
A modern, production-ready web application for generating and exploring UUIDs with a beautiful glassmorphism interface. Generate UUIDs and view them in multiple formats with one click.
|
||||
|
||||
[](https://github.com/xixu-me/UUID-Playground/actions/workflows/deploy.yml)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Core Functionality
|
||||
|
||||
- **Multiple UUID Versions**: Generate UUIDv1 (time-based), UUIDv4 (random), and UUIDv7 (time-ordered)
|
||||
- **8 Format Representations**: View each UUID in canonical, hex, braced, URN, decimal, binary, Base64, and URL-safe Base64 formats
|
||||
- **One-Click Copy**: Copy any format to clipboard with visual feedback
|
||||
- **UUID Validation**: Paste UUID strings (canonical, hex, braced, or URN) to normalize and view all representations
|
||||
- **Lock Feature**: Prevent accidental regeneration with UUID lock toggle
|
||||
- **Auto-Generate**: Automatically generate new UUIDs at configurable intervals (1-60 seconds)
|
||||
- **Keyboard Shortcuts**: Press `Space` or `Enter` to generate new UUIDs
|
||||
|
||||
### UI/UX Excellence
|
||||
|
||||
- **Glassmorphism Design**: Modern translucent panels with blur effects
|
||||
- **Light/Dark Themes**: Automatic theme detection with manual toggle (preference saved)
|
||||
- **Smooth Animations**: GPU-accelerated animations with non-linear easing
|
||||
- **Responsive Layout**: Optimized for desktop, tablet, and mobile devices
|
||||
- **Toast Notifications**: Elegant feedback for all user actions
|
||||
- **Accessibility**: Full keyboard navigation and ARIA labels
|
||||
|
||||
## 🚀 Live Demo
|
||||
|
||||
Visit the live application at: [https://uuid.xi-xu.me](https://uuid.xi-xu.me)
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Framework**: React 18 with TypeScript
|
||||
- **Build Tool**: Vite 5
|
||||
- **Styling**: Pure CSS with CSS Variables
|
||||
- **Testing**: Vitest
|
||||
- **Deployment**: GitHub Actions → GitHub Pages
|
||||
- **UUID Generation**: Web Crypto API
|
||||
|
||||
## 📦 Installation & Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ (LTS recommended)
|
||||
- npm or yarn
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/xixu-me/UUID-Playground.git
|
||||
cd UUID-Playground
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start development server**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The app will be available at `http://localhost:5173`
|
||||
|
||||
4. **Run tests**
|
||||
|
||||
```bash
|
||||
# Run tests once
|
||||
npm test
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
5. **Lint code**
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
6. **Build for production**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Built files will be in the `dist/` directory
|
||||
|
||||
7. **Preview production build**
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🚢 Deployment
|
||||
|
||||
### GitHub Pages Deployment
|
||||
|
||||
This repository is configured to automatically deploy to GitHub Pages using GitHub Actions.
|
||||
|
||||
#### Setup Instructions
|
||||
|
||||
1. **Enable GitHub Pages**
|
||||
- Go to your repository settings
|
||||
- Navigate to **Pages** section
|
||||
- Under "Build and deployment":
|
||||
- Source: **GitHub Actions**
|
||||
|
||||
2. **Push to main branch**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
3. **Workflow automatically runs**
|
||||
|
||||
- Installs dependencies
|
||||
- Runs tests
|
||||
- Builds the project
|
||||
- Deploys to GitHub Pages
|
||||
|
||||
4. **Access your site**
|
||||
- Your site will be available at: `https://<username>.github.io/<repository-name>/`
|
||||
|
||||
#### Workflow Details
|
||||
|
||||
The deployment workflow (`.github/workflows/deploy.yml`):
|
||||
|
||||
- Triggers on push to `main` branch or manual dispatch
|
||||
- Uses Node.js 20 LTS
|
||||
- Caches npm dependencies for faster builds
|
||||
- Runs tests before deployment
|
||||
- Uploads build artifacts to GitHub Pages
|
||||
- Deploys using official GitHub Actions
|
||||
|
||||
#### Customizing Repository Name
|
||||
|
||||
If you rename the repository, update the `base` path in `vite.config.ts`:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
base: '/your-new-repo-name/',
|
||||
// ... other config
|
||||
})
|
||||
```
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Changing Colors
|
||||
|
||||
Edit CSS variables in `src/styles/App.css`:
|
||||
|
||||
```css
|
||||
[data-theme='light'] {
|
||||
--accent-primary: #667eea; /* Primary accent color */
|
||||
--accent-hover: #5a67d8; /* Hover state color */
|
||||
/* ... other variables */
|
||||
}
|
||||
```
|
||||
|
||||
### Adding New UUID Formats
|
||||
|
||||
1. Add format conversion in `src/lib/uuid.ts`:
|
||||
|
||||
```typescript
|
||||
export interface UUIDFormats {
|
||||
// ... existing formats
|
||||
yourNewFormat: string
|
||||
}
|
||||
```
|
||||
|
||||
2. Update `getAllFormats()` function to include new format
|
||||
|
||||
3. Add description in `App.tsx` formatDescriptions object
|
||||
|
||||
### Modifying Auto-Generate Limits
|
||||
|
||||
In `src/App.tsx`, change the min/max values:
|
||||
|
||||
```typescript
|
||||
<input
|
||||
type="number"
|
||||
min="1" // Minimum seconds
|
||||
max="60" // Maximum seconds
|
||||
// ...
|
||||
|
||||
/>
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
The repository includes comprehensive tests for UUID utilities:
|
||||
|
||||
- **UUID Generation**: Validates correct version formatting
|
||||
- **UUID Normalization**: Tests various input formats
|
||||
- **Format Conversion**: Verifies all format representations
|
||||
- **Version Detection**: Ensures correct UUID version identification
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
npm test # Run once
|
||||
npm run test:watch # Watch mode
|
||||
```
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- Uses native Web Crypto API for cryptographically secure random generation
|
||||
- No external dependencies for UUID generation
|
||||
- Client-side only - no data sent to servers
|
||||
- No tracking or analytics
|
||||
|
||||
## ♿ Accessibility
|
||||
|
||||
- Full keyboard navigation support
|
||||
- ARIA labels on interactive elements
|
||||
- Sufficient color contrast ratios
|
||||
- Respects `prefers-reduced-motion`
|
||||
- Focus indicators on all interactive elements
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright (c) Xi Xu. All rights reserved.
|
||||
|
||||
Licensed under the [MIT](LICENSE) license.
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- [UUID Specification (RFC 4122)](https://datatracker.ietf.org/doc/html/rfc4122)
|
||||
- [UUID Version 7 Draft](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format)
|
||||
- [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
|
||||
- [Vite Documentation](https://vitejs.dev/)
|
||||
- [React Documentation](https://react.dev/)
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Design inspired by modern glassmorphism trends
|
||||
- UUID generation using Web Crypto API standards
|
||||
- Built with Vite's lightning-fast tooling
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ and TypeScript
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Modern UUID Playground - Generate and explore UUID formats with a beautiful interface" />
|
||||
<title>UUID Playground</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4940
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"jsdom": "^24.1.3",
|
||||
"prettier": "^3.3.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.1",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"name": "uuid-playground",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uuid.xi-xu.me
|
||||
@@ -0,0 +1,10 @@
|
||||
<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)"/>
|
||||
<text x="50" y="70" font-family="Arial, sans-serif" font-size="60" font-weight="bold" text-anchor="middle" fill="white">🔑</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 513 B |
+244
@@ -0,0 +1,244 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormatCard } from './components/FormatCard'
|
||||
import { InfoPanel } from './components/InfoPanel'
|
||||
import { ToastContainer } from './components/Toast'
|
||||
import { ValidationPanel } from './components/ValidationPanel'
|
||||
import { useKeyboard } from './hooks/useKeyboard'
|
||||
import { useTheme } from './hooks/useTheme'
|
||||
import { useToast } from './hooks/useToast'
|
||||
import { generateUUID, getAllFormats, UUIDFormats, UUIDVersion } from './lib/uuid'
|
||||
import './styles/App.css'
|
||||
|
||||
function App() {
|
||||
const [currentUUID, setCurrentUUID] = useState<string>('')
|
||||
const [formats, setFormats] = useState<UUIDFormats | null>(null)
|
||||
const [isLocked, setIsLocked] = useState(false)
|
||||
const [version, setVersion] = useState<UUIDVersion>('v4')
|
||||
const [autoGenerate, setAutoGenerate] = useState(false)
|
||||
const [autoInterval, setAutoInterval] = useState(5)
|
||||
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { toasts, showToast, removeToast } = useToast()
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (isLocked) {
|
||||
showToast('UUID is locked', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
const uuid = generateUUID(version)
|
||||
setCurrentUUID(uuid)
|
||||
setFormats(getAllFormats(uuid))
|
||||
}
|
||||
|
||||
const handleCopy = async (value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
showToast('Copied to clipboard!', 'success')
|
||||
} catch (error) {
|
||||
showToast('Failed to copy', 'error')
|
||||
console.error('Copy failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidated = (validatedFormats: UUIDFormats) => {
|
||||
if (isLocked) {
|
||||
showToast('UUID is locked', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentUUID(validatedFormats.canonical)
|
||||
setFormats(validatedFormats)
|
||||
showToast('UUID loaded successfully', 'success')
|
||||
}
|
||||
|
||||
// Generate initial UUID on mount
|
||||
useEffect(() => {
|
||||
handleGenerate()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Auto-generate functionality
|
||||
useEffect(() => {
|
||||
if (!autoGenerate || isLocked) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
handleGenerate()
|
||||
}, autoInterval * 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoGenerate, autoInterval, isLocked])
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboard(handleGenerate, [' ', 'Space', 'Enter'])
|
||||
|
||||
const formatLabels: Record<string, string> = {
|
||||
canonical: 'Canonical',
|
||||
hex: 'Hex',
|
||||
braced: 'Braced',
|
||||
urn: 'URN',
|
||||
decimal: 'Decimal',
|
||||
binary: 'Binary',
|
||||
base64: 'Base64',
|
||||
base64url: 'Base64URL',
|
||||
}
|
||||
|
||||
const formatDescriptions: Record<string, string> = {
|
||||
canonical: 'Standard hyphenated format',
|
||||
hex: 'Hexadecimal without hyphens',
|
||||
braced: 'Microsoft GUID style',
|
||||
urn: 'Uniform Resource Name format',
|
||||
decimal: '128-bit integer representation',
|
||||
binary: '128 bits grouped by 32',
|
||||
base64: 'Standard Base64 encoding',
|
||||
base64url: 'URL-safe Base64 encoding',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="background-gradient"></div>
|
||||
|
||||
<header className="header">
|
||||
<div className="header-content">
|
||||
<h1 className="title">
|
||||
<span className="title-icon">🔑</span>
|
||||
UUID Playground
|
||||
</h1>
|
||||
<button
|
||||
className="theme-toggle"
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
>
|
||||
{theme === 'light' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="main">
|
||||
<div className="container">
|
||||
{/* Main UUID Card */}
|
||||
<div className="uuid-main-card">
|
||||
<div className="uuid-display">
|
||||
<div className="uuid-value">{currentUUID}</div>
|
||||
</div>
|
||||
|
||||
<div className="controls">
|
||||
<div className="control-group">
|
||||
<label htmlFor="version-select" className="control-label">
|
||||
Version:
|
||||
</label>
|
||||
<select
|
||||
id="version-select"
|
||||
className="version-select"
|
||||
value={version}
|
||||
onChange={e => setVersion(e.target.value as UUIDVersion)}
|
||||
disabled={isLocked}
|
||||
>
|
||||
<option value="v4">UUID v4 (Random)</option>
|
||||
<option value="v1">UUID v1 (Time-based)</option>
|
||||
<option value="v7">UUID v7 (Time-ordered)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="generate-button"
|
||||
onClick={handleGenerate}
|
||||
disabled={isLocked}
|
||||
aria-label="Generate new UUID"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M17.5 10C17.5 14.1421 14.1421 17.5 10 17.5C5.85786 17.5 2.5 14.1421 2.5 10C2.5 5.85786 5.85786 2.5 10 2.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.5 5L10 10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
Generate
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`lock-button ${isLocked ? 'locked' : ''}`}
|
||||
onClick={() => setIsLocked(!isLocked)}
|
||||
aria-label={isLocked ? 'Unlock UUID' : 'Lock UUID'}
|
||||
title={isLocked ? 'Unlock to allow changes' : 'Lock to prevent changes'}
|
||||
>
|
||||
{isLocked ? '🔒' : '🔓'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="auto-generate">
|
||||
<label className="auto-generate-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoGenerate}
|
||||
onChange={e => setAutoGenerate(e.target.checked)}
|
||||
disabled={isLocked}
|
||||
/>
|
||||
<span>Auto-generate every</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="auto-interval-input"
|
||||
min="1"
|
||||
max="60"
|
||||
value={autoInterval}
|
||||
onChange={e => setAutoInterval(Math.max(1, Math.min(60, parseInt(e.target.value) || 5)))}
|
||||
disabled={!autoGenerate || isLocked}
|
||||
aria-label="Auto-generate interval in seconds"
|
||||
/>
|
||||
<span>seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Format Cards Grid */}
|
||||
{formats && (
|
||||
<div className="formats-grid">
|
||||
{Object.entries(formats).map(([key, value]) => (
|
||||
<FormatCard
|
||||
key={key}
|
||||
label={formatLabels[key]}
|
||||
value={value}
|
||||
description={formatDescriptions[key]}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Panel */}
|
||||
<ValidationPanel onValidated={handleValidated} />
|
||||
|
||||
{/* Info Panel */}
|
||||
<InfoPanel />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<p>
|
||||
Built with React + TypeScript •{' '}
|
||||
<a
|
||||
href="https://github.com/xixu-me/UUID-Playground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
View Source
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<ToastContainer toasts={toasts} onClose={removeToast} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface FormatCardProps {
|
||||
label: string
|
||||
value: string
|
||||
description?: string
|
||||
onCopy: (value: string) => void
|
||||
}
|
||||
|
||||
export function FormatCard({ label, value, description, onCopy }: FormatCardProps) {
|
||||
const [copying, setCopying] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
setCopying(true)
|
||||
onCopy(value)
|
||||
|
||||
setTimeout(() => {
|
||||
setCopying(false)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="format-card">
|
||||
<div className="format-card-header">
|
||||
<div>
|
||||
<h3 className="format-label">{label}</h3>
|
||||
{description && <p className="format-description">{description}</p>}
|
||||
</div>
|
||||
<button
|
||||
className={`copy-button ${copying ? 'copying' : ''}`}
|
||||
onClick={handleCopy}
|
||||
aria-label={`Copy ${label}`}
|
||||
>
|
||||
{copying ? (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M13.5 4L6 11.5L2.5 8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect
|
||||
x="5.5"
|
||||
y="5.5"
|
||||
width="8"
|
||||
height="8"
|
||||
rx="1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 10.5H2.5C1.94772 10.5 1.5 10.0523 1.5 9.5V2.5C1.5 1.94772 1.94772 1.5 2.5 1.5H9.5C10.0523 1.5 10.5 1.94772 10.5 2.5V3.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="format-value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export function InfoPanel() {
|
||||
return (
|
||||
<div className="info-panel">
|
||||
<h3>About UUIDs</h3>
|
||||
<p>
|
||||
A <strong>Universally Unique Identifier (UUID)</strong> is a 128-bit number used to
|
||||
uniquely identify information in computer systems. The probability of generating duplicate
|
||||
UUIDs is extremely low, making them ideal for distributed systems.
|
||||
</p>
|
||||
|
||||
<h4>UUID Versions</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>v1:</strong> Time-based, includes timestamp and MAC address
|
||||
</li>
|
||||
<li>
|
||||
<strong>v4:</strong> Random, most commonly used
|
||||
</li>
|
||||
<li>
|
||||
<strong>v7:</strong> Time-ordered, sortable by creation time
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Format Representations</h4>
|
||||
<p>
|
||||
All formats shown represent the <strong>same 128-bit value</strong>, just encoded
|
||||
differently. You can convert between any format while preserving the underlying UUID.
|
||||
</p>
|
||||
|
||||
<div className="keyboard-shortcuts">
|
||||
<h4>Keyboard Shortcuts</h4>
|
||||
<kbd>Space</kbd> or <kbd>Enter</kbd> - Generate new UUID
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Toast as ToastType } from '../hooks/useToast'
|
||||
|
||||
interface ToastProps {
|
||||
toast: ToastType
|
||||
onClose: (id: string) => void
|
||||
}
|
||||
|
||||
export function Toast({ toast, onClose }: ToastProps) {
|
||||
return (
|
||||
<div className={`toast toast-${toast.type}`} onClick={() => onClose(toast.id)}>
|
||||
<span>{toast.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: ToastType[]
|
||||
onClose: (id: string) => void
|
||||
}
|
||||
|
||||
export function ToastContainer({ toasts, onClose }: ToastContainerProps) {
|
||||
return (
|
||||
<div className="toast-container">
|
||||
{toasts.map(toast => (
|
||||
<Toast key={toast.id} toast={toast} onClose={onClose} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState } from 'react'
|
||||
import { detectUUIDVersion, getAllFormats, normalizeUUID, UUIDFormats } from '../lib/uuid'
|
||||
|
||||
interface ValidationPanelProps {
|
||||
onValidated: (formats: UUIDFormats) => void
|
||||
}
|
||||
|
||||
export function ValidationPanel({ onValidated }: ValidationPanelProps) {
|
||||
const [input, setInput] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [version, setVersion] = useState<string | null>(null)
|
||||
|
||||
const handleValidate = () => {
|
||||
if (!input.trim()) {
|
||||
setError('Please enter a UUID')
|
||||
setVersion(null)
|
||||
return
|
||||
}
|
||||
|
||||
const normalized = normalizeUUID(input)
|
||||
if (!normalized) {
|
||||
setError('Invalid UUID format')
|
||||
setVersion(null)
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
const detectedVersion = detectUUIDVersion(normalized)
|
||||
setVersion(detectedVersion)
|
||||
onValidated(getAllFormats(normalized))
|
||||
setInput('')
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleValidate()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="validation-panel">
|
||||
<h3>Validate UUID</h3>
|
||||
<p className="validation-description">
|
||||
Paste a UUID string (canonical, hex, braced, or URN) to normalize and view all representations
|
||||
</p>
|
||||
|
||||
<div className="validation-input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="validation-input"
|
||||
placeholder="Paste UUID here..."
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
aria-label="UUID validation input"
|
||||
/>
|
||||
<button className="validate-button" onClick={handleValidate}>
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="validation-error">{error}</div>}
|
||||
{version && <div className="validation-success">Valid UUID: {version}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function useKeyboard(callback: () => void, keys: string[]) {
|
||||
useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
// Don't trigger if user is typing in an input
|
||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||
return
|
||||
}
|
||||
|
||||
if (keys.includes(event.key) || keys.includes(event.code)) {
|
||||
event.preventDefault()
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [callback, keys])
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export type Theme = 'light' | 'dark'
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem('uuid-playground-theme')
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
return stored
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('uuid-playground-theme', theme)
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
|
||||
}
|
||||
|
||||
return { theme, toggleTheme }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export interface Toast {
|
||||
id: string
|
||||
message: string
|
||||
type: 'success' | 'error' | 'info'
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
|
||||
const showToast = useCallback((message: string, type: Toast['type'] = 'success') => {
|
||||
const id = Math.random().toString(36).substring(2, 9)
|
||||
const toast: Toast = { id, message, type }
|
||||
|
||||
setToasts(prev => [...prev, toast])
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}, 3000)
|
||||
}, [])
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}, [])
|
||||
|
||||
return { toasts, showToast, removeToast }
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
detectUUIDVersion,
|
||||
generateUUID,
|
||||
generateUUIDv4,
|
||||
getAllFormats,
|
||||
normalizeUUID,
|
||||
uuidToBytes,
|
||||
} from './uuid'
|
||||
|
||||
describe('UUID Generation', () => {
|
||||
it('should generate valid UUIDv4', () => {
|
||||
const uuid = generateUUIDv4()
|
||||
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
|
||||
it('should generate unique UUIDs', () => {
|
||||
const uuid1 = generateUUIDv4()
|
||||
const uuid2 = generateUUIDv4()
|
||||
expect(uuid1).not.toBe(uuid2)
|
||||
})
|
||||
|
||||
it('should generate UUIDv1', () => {
|
||||
const uuid = generateUUID('v1')
|
||||
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
|
||||
it('should generate UUIDv7', () => {
|
||||
const uuid = generateUUID('v7')
|
||||
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UUID Normalization', () => {
|
||||
const validUUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
it('should normalize canonical format', () => {
|
||||
expect(normalizeUUID(validUUID)).toBe(validUUID)
|
||||
})
|
||||
|
||||
it('should normalize uppercase UUID', () => {
|
||||
expect(normalizeUUID(validUUID.toUpperCase())).toBe(validUUID)
|
||||
})
|
||||
|
||||
it('should normalize hex format without hyphens', () => {
|
||||
const hex = validUUID.replace(/-/g, '')
|
||||
expect(normalizeUUID(hex)).toBe(validUUID)
|
||||
})
|
||||
|
||||
it('should normalize braced format', () => {
|
||||
expect(normalizeUUID(`{${validUUID}}`)).toBe(validUUID)
|
||||
})
|
||||
|
||||
it('should normalize URN format', () => {
|
||||
expect(normalizeUUID(`urn:uuid:${validUUID}`)).toBe(validUUID)
|
||||
})
|
||||
|
||||
it('should return null for invalid UUID', () => {
|
||||
expect(normalizeUUID('not-a-uuid')).toBeNull()
|
||||
expect(normalizeUUID('550e8400-e29b-41d4-a716')).toBeNull()
|
||||
expect(normalizeUUID('')).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle whitespace', () => {
|
||||
expect(normalizeUUID(` ${validUUID} `)).toBe(validUUID)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UUID Format Conversion', () => {
|
||||
const testUUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
it('should convert to all formats', () => {
|
||||
const formats = getAllFormats(testUUID)
|
||||
|
||||
expect(formats.canonical).toBe(testUUID)
|
||||
expect(formats.hex).toBe('550e8400e29b41d4a716446655440000')
|
||||
expect(formats.braced).toBe(`{${testUUID}}`)
|
||||
expect(formats.urn).toBe(`urn:uuid:${testUUID}`)
|
||||
expect(formats.decimal).toBe('113059749145936325402354257176981405696')
|
||||
expect(formats.binary).toMatch(/^[01 ]+$/)
|
||||
expect(formats.base64).toBeTruthy()
|
||||
expect(formats.base64url).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate valid base64', () => {
|
||||
const formats = getAllFormats(testUUID)
|
||||
// Base64 should be decodable
|
||||
expect(formats.base64.length).toBeGreaterThan(0)
|
||||
expect(formats.base64url.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should generate correct binary format', () => {
|
||||
const formats = getAllFormats(testUUID)
|
||||
// Should have 4 groups of 32 bits
|
||||
const groups = formats.binary.split(' ')
|
||||
expect(groups).toHaveLength(4)
|
||||
groups.forEach(group => {
|
||||
expect(group).toHaveLength(32)
|
||||
expect(group).toMatch(/^[01]+$/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UUID Version Detection', () => {
|
||||
it('should detect UUIDv4', () => {
|
||||
const uuid = generateUUIDv4()
|
||||
expect(detectUUIDVersion(uuid)).toBe('v4 (Random)')
|
||||
})
|
||||
|
||||
it('should detect UUIDv1', () => {
|
||||
const uuid = generateUUID('v1')
|
||||
expect(detectUUIDVersion(uuid)).toBe('v1 (Time-based)')
|
||||
})
|
||||
|
||||
it('should detect UUIDv7', () => {
|
||||
const uuid = generateUUID('v7')
|
||||
expect(detectUUIDVersion(uuid)).toBe('v7 (Time-ordered)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('UUID to Bytes Conversion', () => {
|
||||
it('should convert UUID to bytes', () => {
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000'
|
||||
const bytes = uuidToBytes(uuid)
|
||||
|
||||
expect(bytes).toBeInstanceOf(Uint8Array)
|
||||
expect(bytes.length).toBe(16)
|
||||
expect(bytes[0]).toBe(0x55)
|
||||
expect(bytes[1]).toBe(0x0e)
|
||||
expect(bytes[2]).toBe(0x84)
|
||||
})
|
||||
|
||||
it('should throw on invalid UUID', () => {
|
||||
expect(() => uuidToBytes('invalid')).toThrow()
|
||||
})
|
||||
})
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* UUID generation and conversion utilities
|
||||
*/
|
||||
|
||||
export type UUIDVersion = 'v4' | 'v1' | 'v7'
|
||||
|
||||
export interface UUIDFormats {
|
||||
canonical: string
|
||||
hex: string
|
||||
braced: string
|
||||
urn: string
|
||||
decimal: string
|
||||
binary: string
|
||||
base64: string
|
||||
base64url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a UUIDv4 using Web Crypto API
|
||||
*/
|
||||
export function generateUUIDv4(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
// Fallback implementation
|
||||
const bytes = new Uint8Array(16)
|
||||
crypto.getRandomValues(bytes)
|
||||
|
||||
// Set version (4) and variant bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
|
||||
return bytesToUUID(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a UUIDv1 (time-based)
|
||||
*/
|
||||
export function generateUUIDv1(): string {
|
||||
const now = Date.now()
|
||||
const timeHigh = BigInt(now) * 10000n + 0x01b21dd213814000n
|
||||
|
||||
const bytes = new Uint8Array(16)
|
||||
crypto.getRandomValues(bytes)
|
||||
|
||||
// Set time fields
|
||||
const timeLow = Number(timeHigh & 0xffffffffn)
|
||||
const timeMid = Number((timeHigh >> 32n) & 0xffffn)
|
||||
const timeHighAndVersion = Number((timeHigh >> 48n) & 0x0fffn) | 0x1000
|
||||
|
||||
bytes[0] = (timeLow >> 24) & 0xff
|
||||
bytes[1] = (timeLow >> 16) & 0xff
|
||||
bytes[2] = (timeLow >> 8) & 0xff
|
||||
bytes[3] = timeLow & 0xff
|
||||
bytes[4] = (timeMid >> 8) & 0xff
|
||||
bytes[5] = timeMid & 0xff
|
||||
bytes[6] = (timeHighAndVersion >> 8) & 0xff
|
||||
bytes[7] = timeHighAndVersion & 0xff
|
||||
|
||||
// Set variant bits
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
|
||||
return bytesToUUID(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a UUIDv7 (time-ordered)
|
||||
*/
|
||||
export function generateUUIDv7(): string {
|
||||
const now = Date.now()
|
||||
const bytes = new Uint8Array(16)
|
||||
|
||||
// Unix timestamp in milliseconds (48 bits)
|
||||
bytes[0] = (now >> 40) & 0xff
|
||||
bytes[1] = (now >> 32) & 0xff
|
||||
bytes[2] = (now >> 24) & 0xff
|
||||
bytes[3] = (now >> 16) & 0xff
|
||||
bytes[4] = (now >> 8) & 0xff
|
||||
bytes[5] = now & 0xff
|
||||
|
||||
// Random bits
|
||||
crypto.getRandomValues(bytes.subarray(6))
|
||||
|
||||
// Set version (7) and variant bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x70
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
|
||||
return bytesToUUID(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID based on version
|
||||
*/
|
||||
export function generateUUID(version: UUIDVersion = 'v4'): string {
|
||||
switch (version) {
|
||||
case 'v1':
|
||||
return generateUUIDv1()
|
||||
case 'v7':
|
||||
return generateUUIDv7()
|
||||
case 'v4':
|
||||
default:
|
||||
return generateUUIDv4()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to UUID canonical format
|
||||
*/
|
||||
function bytesToUUID(bytes: Uint8Array): string {
|
||||
const hex = Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UUID string to bytes
|
||||
*/
|
||||
export function uuidToBytes(uuid: string): Uint8Array {
|
||||
const hex = uuid.replace(/[^0-9a-f]/gi, '')
|
||||
if (hex.length !== 32) {
|
||||
throw new Error('Invalid UUID format')
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize UUID string
|
||||
*/
|
||||
export function normalizeUUID(input: string): string | null {
|
||||
try {
|
||||
// Remove common prefixes and wrappers
|
||||
let cleaned = input.trim()
|
||||
cleaned = cleaned.replace(/^urn:uuid:/i, '')
|
||||
cleaned = cleaned.replace(/^[{(]|[})]$/g, '')
|
||||
|
||||
// Check if it's a valid hex string (with or without hyphens)
|
||||
const hex = cleaned.replace(/[^0-9a-f]/gi, '')
|
||||
if (hex.length !== 32) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate it's valid hex
|
||||
if (!/^[0-9a-f]{32}$/i.test(hex)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Return canonical format
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`.toLowerCase()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all format representations of a UUID
|
||||
*/
|
||||
export function getAllFormats(uuid: string): UUIDFormats {
|
||||
const bytes = uuidToBytes(uuid)
|
||||
const hex = uuid.replace(/-/g, '')
|
||||
|
||||
// Convert to decimal (128-bit number as string)
|
||||
const decimal = bytes.reduce((acc, byte, i) => {
|
||||
const shift = BigInt(15 - i) * 8n
|
||||
return acc + (BigInt(byte) << shift)
|
||||
}, 0n).toString()
|
||||
|
||||
// Convert to binary (grouped by 32 bits for readability)
|
||||
const binaryStr = Array.from(bytes)
|
||||
.map(b => b.toString(2).padStart(8, '0'))
|
||||
.join('')
|
||||
const binary = [
|
||||
binaryStr.slice(0, 32),
|
||||
binaryStr.slice(32, 64),
|
||||
binaryStr.slice(64, 96),
|
||||
binaryStr.slice(96, 128),
|
||||
].join(' ')
|
||||
|
||||
// Convert to Base64
|
||||
const base64 = btoa(String.fromCharCode(...bytes))
|
||||
|
||||
// Convert to URL-safe Base64
|
||||
const base64url = base64
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '')
|
||||
|
||||
return {
|
||||
canonical: uuid,
|
||||
hex: hex.toLowerCase(),
|
||||
braced: `{${uuid}}`,
|
||||
urn: `urn:uuid:${uuid}`,
|
||||
decimal,
|
||||
binary,
|
||||
base64,
|
||||
base64url,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect UUID version
|
||||
*/
|
||||
export function detectUUIDVersion(uuid: string): string {
|
||||
try {
|
||||
const bytes = uuidToBytes(uuid)
|
||||
const version = (bytes[6] >> 4) & 0x0f
|
||||
|
||||
switch (version) {
|
||||
case 1:
|
||||
return 'v1 (Time-based)'
|
||||
case 2:
|
||||
return 'v2 (DCE Security)'
|
||||
case 3:
|
||||
return 'v3 (MD5 hash)'
|
||||
case 4:
|
||||
return 'v4 (Random)'
|
||||
case 5:
|
||||
return 'v5 (SHA-1 hash)'
|
||||
case 7:
|
||||
return 'v7 (Time-ordered)'
|
||||
default:
|
||||
return `v${version} (Unknown)`
|
||||
}
|
||||
} catch {
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,840 @@
|
||||
/* ===== CSS Variables ===== */
|
||||
:root {
|
||||
--timing-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--timing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
--timing-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
--bg-primary: #f0f4f8;
|
||||
--bg-gradient-1: #667eea;
|
||||
--bg-gradient-2: #764ba2;
|
||||
--bg-gradient-3: #f093fb;
|
||||
|
||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.3);
|
||||
--glass-shadow: rgba(31, 38, 135, 0.15);
|
||||
|
||||
--text-primary: #1a202c;
|
||||
--text-secondary: #4a5568;
|
||||
--text-muted: #718096;
|
||||
|
||||
--accent-primary: #667eea;
|
||||
--accent-hover: #5a67d8;
|
||||
--accent-light: rgba(102, 126, 234, 0.1);
|
||||
|
||||
--success: #48bb78;
|
||||
--error: #f56565;
|
||||
--info: #4299e1;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--bg-primary: #0f1419;
|
||||
--bg-gradient-1: #2d3748;
|
||||
--bg-gradient-2: #4a5568;
|
||||
--bg-gradient-3: #667eea;
|
||||
|
||||
--glass-bg: rgba(26, 32, 44, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-shadow: rgba(0, 0, 0, 0.3);
|
||||
|
||||
--text-primary: #f7fafc;
|
||||
--text-secondary: #e2e8f0;
|
||||
--text-muted: #a0aec0;
|
||||
|
||||
--accent-primary: #667eea;
|
||||
--accent-hover: #7c3aed;
|
||||
--accent-light: rgba(102, 126, 234, 0.2);
|
||||
|
||||
--success: #48bb78;
|
||||
--error: #fc8181;
|
||||
--info: #63b3ed;
|
||||
}
|
||||
|
||||
/* ===== Base Styles ===== */
|
||||
* {
|
||||
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-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s var(--timing-ease), color 0.3s var(--timing-ease);
|
||||
}
|
||||
|
||||
code, pre, .format-value {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* ===== App Layout ===== */
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.background-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--bg-gradient-1) 0%,
|
||||
var(--bg-gradient-2) 50%,
|
||||
var(--bg-gradient-3) 100%
|
||||
);
|
||||
opacity: 0.1;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { transform: scale(1) rotate(0deg); }
|
||||
50% { transform: scale(1.1) rotate(5deg); }
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
.header {
|
||||
padding: 2rem 1rem 1rem;
|
||||
animation: slideDown 0.6s var(--timing-ease);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--bg-gradient-3));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 2rem;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--glass-border);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s var(--timing-bounce);
|
||||
box-shadow: 0 4px 6px var(--glass-shadow);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1) rotate(15deg);
|
||||
box-shadow: 0 6px 12px var(--glass-shadow);
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* ===== Main Content ===== */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* ===== Glass Card Base ===== */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 8px 32px var(--glass-shadow);
|
||||
padding: 2rem;
|
||||
transition: all 0.3s var(--timing-ease);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
box-shadow: 0 12px 40px var(--glass-shadow);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== UUID Main Card ===== */
|
||||
.uuid-main-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 12px 40px var(--glass-shadow);
|
||||
padding: 2.5rem;
|
||||
animation: fadeInUp 0.6s var(--timing-ease);
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.uuid-display {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uuid-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-primary);
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 1.5rem;
|
||||
background: var(--accent-light);
|
||||
border-radius: 12px;
|
||||
animation: fadeIn 0.5s var(--timing-ease);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ===== Controls ===== */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.version-select {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s var(--timing-ease);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.version-select:hover:not(:disabled) {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
}
|
||||
|
||||
.version-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.generate-button {
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-hover));
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s var(--timing-ease);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.generate-button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.generate-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.generate-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.generate-button svg {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.generate-button:hover:not(:disabled) svg {
|
||||
animation: rotate 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.lock-button {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s var(--timing-bounce);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.lock-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px var(--glass-shadow);
|
||||
}
|
||||
|
||||
.lock-button.locked {
|
||||
background: var(--accent-light);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ===== Auto Generate ===== */
|
||||
.auto-generate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background: var(--accent-light);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.auto-generate-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.auto-generate-label input[type='checkbox'] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-interval-input {
|
||||
width: 60px;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auto-interval-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== Format Cards ===== */
|
||||
.formats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
animation: fadeInUp 0.6s var(--timing-ease);
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.format-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s var(--timing-ease);
|
||||
box-shadow: 0 4px 12px var(--glass-shadow);
|
||||
}
|
||||
|
||||
.format-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px var(--glass-shadow);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.format-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.format-label {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.format-description {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s var(--timing-ease);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-primary);
|
||||
border-color: var(--accent-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.copy-button.copying {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-color: var(--success);
|
||||
animation: pulse 0.3s var(--timing-ease);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.15); }
|
||||
}
|
||||
|
||||
.format-value {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
padding: 1rem;
|
||||
background: var(--accent-light);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ===== Validation Panel ===== */
|
||||
.validation-panel {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 24px var(--glass-shadow);
|
||||
animation: fadeInUp 0.6s var(--timing-ease);
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.validation-panel h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.validation-description {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.validation-input-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.validation-input {
|
||||
flex: 1;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s var(--timing-ease);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
}
|
||||
|
||||
.validation-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
}
|
||||
|
||||
.validate-button {
|
||||
padding: 0.875rem 1.75rem;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s var(--timing-ease);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.validate-button:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: rgba(245, 101, 101, 0.1);
|
||||
border-left: 3px solid var(--error);
|
||||
border-radius: 8px;
|
||||
color: var(--error);
|
||||
font-weight: 500;
|
||||
animation: shake 0.4s var(--timing-ease);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.validation-success {
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: rgba(72, 187, 120, 0.1);
|
||||
border-left: 3px solid var(--success);
|
||||
border-radius: 8px;
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
animation: slideInRight 0.3s var(--timing-ease);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Info Panel ===== */
|
||||
.info-panel {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 24px var(--glass-shadow);
|
||||
animation: fadeInUp 0.6s var(--timing-ease);
|
||||
animation-delay: 0.4s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.info-panel h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.info-panel h4 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.info-panel p {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-panel ul {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-panel li {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--accent-light);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts kbd {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px var(--glass-shadow);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* ===== Toast Notifications ===== */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 8px 24px var(--glass-shadow);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
animation: slideInUp 0.3s var(--timing-bounce);
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left: 4px solid var(--error);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid var(--info);
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px var(--glass-shadow);
|
||||
}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.footer {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s var(--timing-ease);
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ===== Responsive Design ===== */
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1.5rem 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.uuid-main-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.uuid-value {
|
||||
font-size: 1.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control-group,
|
||||
.generate-button,
|
||||
.lock-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.formats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.validation-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.validate-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Accessibility ===== */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid var(--accent-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ===== Selection ===== */
|
||||
::selection {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"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,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/',
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: [],
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user