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

311
app/globals.css Normal file
View File

@@ -0,0 +1,311 @@
@import "tailwindcss";
/* Source directives - scan components for Tailwind classes */
@source "../components/editor/*.{js,ts,jsx,tsx}";
@source "../components/effects/*.{js,ts,jsx,tsx}";
@source "../components/tracks/*.{js,ts,jsx,tsx}";
@source "../components/automation/*.{js,ts,jsx,tsx}";
@source "../components/analysis/*.{js,ts,jsx,tsx}";
@source "../components/recording/*.{js,ts,jsx,tsx}";
@source "../components/export/*.{js,ts,jsx,tsx}";
@source "../components/project/*.{js,ts,jsx,tsx}";
@source "../components/layout/*.{js,ts,jsx,tsx}";
@source "../components/ui/*.{js,ts,jsx,tsx}";
@source "*.{js,ts,jsx,tsx}";
/* Custom dark mode variant */
@custom-variant dark (&:is(.dark *));
/* CSS Variables for theming */
@layer base {
:root {
/* Light mode colors using OKLCH */
--background: oklch(100% 0 0);
--foreground: oklch(9.8% 0.038 285.8);
--card: oklch(100% 0 0);
--card-foreground: oklch(9.8% 0.038 285.8);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(9.8% 0.038 285.8);
--primary: oklch(22.4% 0.053 285.8);
--primary-foreground: oklch(98% 0 0);
--secondary: oklch(96.1% 0 0);
--secondary-foreground: oklch(13.8% 0.038 285.8);
--muted: oklch(96.1% 0 0);
--muted-foreground: oklch(45.1% 0.015 285.9);
--accent: oklch(96.1% 0 0);
--accent-foreground: oklch(13.8% 0.038 285.8);
--destructive: oklch(60.2% 0.168 29.2);
--destructive-foreground: oklch(98% 0 0);
--border: oklch(89.8% 0 0);
--input: oklch(89.8% 0 0);
--ring: oklch(22.4% 0.053 285.8);
--radius: 0.5rem;
--success: oklch(60% 0.15 145);
--success-foreground: oklch(98% 0 0);
--warning: oklch(75% 0.15 85);
--warning-foreground: oklch(20% 0 0);
--info: oklch(65% 0.15 240);
--info-foreground: oklch(98% 0 0);
/* Audio-specific colors */
--waveform: oklch(50% 0.1 240);
--waveform-progress: oklch(60% 0.15 145);
--waveform-selection: oklch(65% 0.15 240);
--waveform-bg: oklch(98% 0 0);
}
.dark {
/* Dark mode colors using OKLCH */
--background: oklch(9.8% 0.038 285.8);
--foreground: oklch(98% 0 0);
--card: oklch(9.8% 0.038 285.8);
--card-foreground: oklch(98% 0 0);
--popover: oklch(9.8% 0.038 285.8);
--popover-foreground: oklch(98% 0 0);
--primary: oklch(98% 0 0);
--primary-foreground: oklch(13.8% 0.038 285.8);
--secondary: oklch(17.7% 0.038 285.8);
--secondary-foreground: oklch(98% 0 0);
--muted: oklch(17.7% 0.038 285.8);
--muted-foreground: oklch(63.9% 0.012 285.9);
--accent: oklch(17.7% 0.038 285.8);
--accent-foreground: oklch(98% 0 0);
--destructive: oklch(50% 0.2 29.2);
--destructive-foreground: oklch(98% 0 0);
--border: oklch(17.7% 0.038 285.8);
--input: oklch(17.7% 0.038 285.8);
--ring: oklch(83.1% 0.012 285.9);
--success: oklch(55% 0.15 145);
--success-foreground: oklch(98% 0 0);
--warning: oklch(70% 0.15 85);
--warning-foreground: oklch(20% 0 0);
--info: oklch(60% 0.15 240);
--info-foreground: oklch(98% 0 0);
/* Audio-specific colors */
--waveform: oklch(70% 0.15 240);
--waveform-progress: oklch(65% 0.15 145);
--waveform-selection: oklch(70% 0.15 240);
--waveform-bg: oklch(12% 0.038 285.8);
}
}
/* Theme inline - map CSS variables to Tailwind colors */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-waveform: var(--waveform);
--color-waveform-progress: var(--waveform-progress);
--color-waveform-selection: var(--waveform-selection);
--color-waveform-bg: var(--waveform-bg);
--radius: var(--radius);
}
/* Global styles */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
/* Custom animations */
@layer utilities {
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInFromRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes pulseSubtle {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
@keyframes progress {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-fadeIn {
animation: fadeIn 0.2s ease-out;
}
.animate-slideInFromRight {
animation: slideInFromRight 0.3s ease-out;
}
.animate-slideDown {
animation: slideDown 0.3s ease-out;
}
.animate-slideUp {
animation: slideUp 0.3s ease-out;
}
.animate-scaleIn {
animation: scaleIn 0.2s ease-out;
}
.animate-pulseSubtle {
animation: pulseSubtle 2s ease-in-out infinite;
}
.animate-shimmer {
animation: shimmer 2s linear infinite;
}
.animate-progress {
animation: progress 1.5s ease-in-out infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}
}
/* Custom scrollbar */
@layer utilities {
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
@apply bg-muted;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground/50;
}
}

40
app/layout.tsx Normal file
View File

@@ -0,0 +1,40 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Audio UI - Browser Audio Editor',
description: 'Professional audio editing in your browser. Multi-track editing, effects, recording, and more.',
keywords: ['audio editor', 'waveform editor', 'web audio', 'daw', 'music production', 'audio effects'],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
const theme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const shouldBeDark = theme === 'dark' || (!theme && prefersDark);
if (shouldBeDark) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body className="min-h-screen antialiased">
{children}
</body>
</html>
);
}

171
app/page.tsx Normal file
View File

@@ -0,0 +1,171 @@
'use client';
import * as React from 'react';
import { Music, Settings } from 'lucide-react';
import { ThemeToggle } from '@/components/layout/ThemeToggle';
import { Button } from '@/components/ui/Button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { ToastProvider } from '@/components/ui/Toast';
export default function Home() {
return (
<ToastProvider>
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b border-border">
<div className="container mx-auto px-3 sm:px-4 py-3 sm:py-4 flex items-center justify-between gap-2">
<div className="min-w-0 flex-1 flex items-center gap-3">
<Music className="h-6 w-6 text-primary" />
<div>
<h1 className="text-xl sm:text-2xl font-bold text-foreground">Audio UI</h1>
<p className="text-xs sm:text-sm text-muted-foreground">
Professional audio editing in your browser
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
title="Settings"
>
<Settings className="h-5 w-5" />
</Button>
<ThemeToggle />
</div>
</div>
</header>
{/* Main content */}
<main className="container mx-auto px-3 sm:px-4 py-6 sm:py-8 md:py-16">
<div className="max-w-4xl mx-auto space-y-8">
{/* Welcome Card */}
<Card>
<CardHeader>
<CardTitle>Welcome to Audio UI</CardTitle>
<CardDescription>
A sophisticated browser-only audio editor built with Next.js 16 and Web Audio API
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
This project is currently in development. The following features are planned:
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Multi-track audio editing with professional mixer</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Advanced effects: EQ, compression, reverb, delay, and more</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Waveform visualization with zoom and scroll</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Audio recording from microphone</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Automation lanes for parameters</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Export to WAV, MP3, OGG, and FLAC</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary"></span>
<span>Project save/load with IndexedDB</span>
</li>
</ul>
</div>
</CardContent>
</Card>
{/* Tech Stack Card */}
<Card>
<CardHeader>
<CardTitle>Technology Stack</CardTitle>
<CardDescription>
Built with modern web technologies
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Frontend</h4>
<ul className="space-y-1 text-muted-foreground">
<li> Next.js 16 with React 19</li>
<li> TypeScript 5</li>
<li> Tailwind CSS 4</li>
<li> Lucide React Icons</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-2">Audio</h4>
<ul className="space-y-1 text-muted-foreground">
<li> Web Audio API</li>
<li> Canvas API</li>
<li> MediaRecorder API</li>
<li> AudioWorklets</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Privacy Card */}
<Card>
<CardHeader>
<CardTitle>Privacy First</CardTitle>
<CardDescription>
Your audio never leaves your device
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
All audio processing happens locally in your browser using the Web Audio API.
No files are uploaded to any server. Your projects are saved in your browser's
IndexedDB storage, giving you complete control over your data.
</p>
</CardContent>
</Card>
</div>
</main>
{/* Footer */}
<footer className="border-t border-border mt-8 sm:mt-12 md:mt-16">
<div className="container mx-auto px-3 sm:px-4 py-6 text-center text-xs sm:text-sm text-muted-foreground">
<p>
Powered by{' '}
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Web Audio API
</a>
{' '}and{' '}
<a
href="https://nextjs.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Next.js 16
</a>
</p>
<p className="mt-2">
All audio processing happens locally in your browser. No files are uploaded.
</p>
</div>
</footer>
</div>
</ToastProvider>
);
}