feat: initialize Next.js 16 project with Tailwind CSS 4 and Docker support

Phase 1 Implementation:
- Set up Next.js 16 with React 19, TypeScript 5, and Turbopack
- Configure Tailwind CSS 4 with OKLCH color system
- Implement dark/light theme support
- Create core UI components: Button, Card, Slider, Progress, Toast
- Add ThemeToggle component for theme switching
- Set up project directory structure for audio editor
- Create storage utilities for settings management
- Add Dockerfile with multi-stage build (Node + nginx)
- Configure nginx for static file serving with caching
- Add docker-compose.yml for easy deployment
- Configure static export mode for production

Tech Stack:
- Next.js 16 with Turbopack
- React 19
- TypeScript 5
- Tailwind CSS 4
- pnpm 9.0.0
- nginx 1.27 (for Docker deployment)

Build verified and working ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-17 15:23:00 +01:00
parent 88749dafae
commit 591f726899
20 changed files with 5475 additions and 0 deletions

47
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,47 @@
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
return (
<button
className={cn(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
{
'bg-primary text-primary-foreground hover:bg-primary/90':
variant === 'default',
'bg-destructive text-destructive-foreground hover:bg-destructive/90':
variant === 'destructive',
'border border-input bg-background hover:bg-accent hover:text-accent-foreground':
variant === 'outline',
'bg-secondary text-secondary-foreground hover:bg-secondary/80':
variant === 'secondary',
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
'text-primary underline-offset-4 hover:underline': variant === 'link',
},
{
'h-10 px-4 py-2': size === 'default',
'h-9 rounded-md px-3': size === 'sm',
'h-11 rounded-md px-8': size === 'lg',
'h-10 w-10': size === 'icon',
},
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button };

78
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border border-border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
max?: number;
showValue?: boolean;
variant?: 'default' | 'success' | 'warning' | 'destructive';
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
(
{
className,
value = 0,
max = 100,
showValue = false,
variant = 'default',
...props
},
ref
) => {
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
return (
<div ref={ref} className={cn('w-full', className)} {...props}>
{showValue && (
<div className="flex justify-between mb-1">
<span className="text-sm font-medium text-foreground">
Progress
</span>
<span className="text-sm text-muted-foreground">
{Math.round(percentage)}%
</span>
</div>
)}
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
<div
className={cn(
'h-full transition-all duration-300 ease-in-out',
{
'bg-primary': variant === 'default',
'bg-success': variant === 'success',
'bg-warning': variant === 'warning',
'bg-destructive': variant === 'destructive',
}
)}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
);
Progress.displayName = 'Progress';
export { Progress };

81
components/ui/Slider.tsx Normal file
View File

@@ -0,0 +1,81 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface SliderProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
label?: string;
showValue?: boolean;
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
(
{
className,
value = 0,
onChange,
min = 0,
max = 100,
step = 1,
label,
showValue = false,
disabled,
...props
},
ref
) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(parseFloat(e.target.value));
};
return (
<div className={cn('w-full', className)}>
{(label || showValue) && (
<div className="flex items-center justify-between mb-2">
{label && (
<label className="text-sm font-medium text-foreground">
{label}
</label>
)}
{showValue && (
<span className="text-sm text-muted-foreground">{value}</span>
)}
</div>
)}
<input
ref={ref}
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
disabled={disabled}
className={cn(
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4',
'[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary',
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-colors',
'[&::-webkit-slider-thumb]:hover:bg-primary/90',
'[&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full',
'[&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer',
'[&::-moz-range-thumb]:transition-colors [&::-moz-range-thumb]:hover:bg-primary/90'
)}
{...props}
/>
</div>
);
}
);
Slider.displayName = 'Slider';
export { Slider };

134
components/ui/Toast.tsx Normal file
View File

@@ -0,0 +1,134 @@
'use client';
import * as React from 'react';
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
export interface Toast {
id: string;
title?: string;
description?: string;
variant?: 'default' | 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
}
const ToastContext = React.createContext<ToastContextType | undefined>(
undefined
);
export function useToast() {
const context = React.useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<Toast[]>([]);
const addToast = React.useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substring(2, 9);
const newToast: Toast = { id, ...toast };
setToasts((prev) => [...prev, newToast]);
// Auto remove after duration
const duration = toast.duration ?? 5000;
if (duration > 0) {
setTimeout(() => {
removeToast(id);
}, duration);
}
}, []);
const removeToast = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
}
function ToastContainer({
toasts,
onRemove,
}: {
toasts: Toast[];
onRemove: (id: string) => void;
}) {
if (toasts.length === 0) return null;
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-md w-full pointer-events-none">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
))}
</div>
);
}
function ToastItem({
toast,
onRemove,
}: {
toast: Toast;
onRemove: (id: string) => void;
}) {
const variant = toast.variant ?? 'default';
const icons = {
default: Info,
success: CheckCircle,
error: AlertCircle,
warning: AlertTriangle,
info: Info,
};
const Icon = icons[variant];
return (
<div
className={cn(
'flex items-start gap-3 rounded-lg border p-4 shadow-lg pointer-events-auto',
'animate-slideInFromRight',
{
'bg-card border-border': variant === 'default',
'bg-success/10 border-success text-success-foreground':
variant === 'success',
'bg-destructive/10 border-destructive text-destructive-foreground':
variant === 'error',
'bg-warning/10 border-warning text-warning-foreground':
variant === 'warning',
'bg-info/10 border-info text-info-foreground': variant === 'info',
}
)}
>
<Icon className="h-5 w-5 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
{toast.title && (
<div className="font-semibold text-sm">{toast.title}</div>
)}
{toast.description && (
<div className="text-sm opacity-90 mt-1">{toast.description}</div>
)}
</div>
<button
onClick={() => onRemove(toast.id)}
className="flex-shrink-0 rounded-md p-1 hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
);
}