diff --git a/CLAUDE.md b/CLAUDE.md index 2bda7ca..17bd6d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,10 +23,11 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro ### Technology Stack - **Frontend**: Next.js 15.1.6 with React 19, TypeScript, and custom CSS with glass morphism design -- **Backend**: Next.js API routes +- **Backend**: Next.js API routes with authentication middleware - **Database**: SQLite with sqlite3 driver - **OBS Integration**: obs-websocket-js for WebSocket communication with OBS Studio -- **Styling**: Solarized Dark theme with CSS custom properties, Tailwind CSS utilities, and accessible glass morphism components +- **Styling**: Consolidated CSS architecture with Solarized Dark theme, CSS custom properties, Tailwind CSS utilities, and accessible glass morphism components +- **Security**: API key authentication middleware for production deployments ### Project Structure - `/app` - Next.js App Router pages and API routes @@ -34,7 +35,8 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro - `/streams` - Streams management page (add new streams and view existing) - `/teams` - Team management page - `/edit/[id]` - Individual stream editing -- `/components` - Reusable React components (Header, Footer, Dropdown, Toast) +- `/components` - Reusable React components (Header, Footer, Dropdown, Toast, CollapsibleGroup) +- `/middleware.ts` - API authentication middleware for security - `/lib` - Core utilities and database connection - `database.ts` - SQLite database initialization and connection management - `obsClient.js` - OBS WebSocket client with persistent connection management @@ -76,6 +78,10 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro 10. **OBS Scene Control**: Direct scene switching controls with dynamic state tracking and real-time synchronization between UI and OBS +11. **Studio Mode Support**: Full preview/program scene management with transition controls for professional broadcasting + +12. **Collapsible Stream Groups**: Organized stream display with expandable team groups for better UI management + ### Environment Configuration - `FILE_DIRECTORY`: Directory for database and text files (default: ./files) - `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1) @@ -117,9 +123,10 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro #### OBS Scene Control - `POST /api/setScene` - Switch OBS to specified scene layout (1-Screen, 2-Screen, 4-Screen) - `GET /api/getCurrentScene` - Get currently active OBS scene for state synchronization +- `POST /api/triggerTransition` - Trigger studio mode transition from preview to program (requires studio mode enabled) #### System Status -- `GET /api/obsStatus` - Real-time OBS connection and streaming status +- `GET /api/obsStatus` - Real-time OBS connection, streaming, recording, and studio mode status ### Database Schema @@ -188,6 +195,9 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio ### Security Architecture **Authentication**: API key-based authentication protects all API endpoints through Next.js middleware +- Middleware intercepts all API requests when `API_KEY` is set +- Bypasses authentication for localhost in development +- Returns 401 for unauthorized requests **Input Validation**: Comprehensive validation using centralized security utilities in `/lib/security.ts`: - Screen parameter allowlisting prevents path traversal attacks @@ -212,6 +222,8 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Visual Feedback**: Clear "View Stream" links with proper contrast for accessibility - **Team Association**: Streams organized under teams with proper naming conventions - **Active Source Detection**: Properly reads current active sources from text files on page load and navigation +- **Collapsible Organization**: Streams grouped by team in expandable sections for cleaner UI +- **Enhanced Stream Display**: Shows stream status with preview/program indicators in studio mode ### Team & Group Management - **UUID-based Tracking**: Robust OBS group synchronization using scene UUIDs @@ -247,12 +259,15 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Error Recovery**: Graceful error handling with user-friendly messages - **Enhanced Footer**: Real-time team/stream counts, OBS connection status with visual indicators - **Optimistic Updates**: Immediate UI feedback with proper stream group name matching +- **Studio Mode Status**: Footer displays studio mode state with preview/program scene information +- **Transition Controls**: "Cut to Preview" button available when studio mode is active ### OBS Integration Improvements - **Text Size**: Team name overlays use 96pt font for better visibility - **Color Display**: Fixed background color display (#002b4b) using proper ABGR format - **Standardized APIs**: All endpoints use consistent `{ success: true, data: [...] }` response format - **Performance Optimization**: Reduced code duplication and improved API response handling +- **CSS Consolidation**: Eliminated repetitive styles, centralized theming in globals.css ### Developer Experience - **Type Safety**: Comprehensive TypeScript definitions throughout diff --git a/README.md b/README.md index d44ace2..ce5b8e9 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,22 @@ A professional [Next.js](https://nextjs.org) web application for managing live s ## Features +- **Studio Mode Support**: Full preview/program scene management with transition controls for professional broadcasting - **OBS Scene Control**: Switch between OBS layouts (1-Screen, 2-Screen, 4-Screen) with dynamic button states - **Multi-Screen Source Control**: Manage 7 different screen positions (large, left, right, and 4 corners) - **Real-time OBS Integration**: WebSocket connection with live status monitoring - **Enhanced Stream Management**: Create, edit, and delete streams with comprehensive OBS cleanup - **Team Organization**: Organize streams by teams with full CRUD operations and scene synchronization +- **Collapsible Stream Groups**: Organized stream display with expandable team groups for better UI management - **Comprehensive Deletion**: Remove streams/teams with complete OBS component cleanup (scenes, sources, text files) - **Audio Control**: Browser sources created with muted audio and OBS control enabled - **Modern UI**: Glass morphism design with responsive layout and accessibility features - **Professional Broadcasting**: Audio routing, scene management, and live status indicators - **Dual Integration**: WebSocket API + text file monitoring for maximum compatibility - **UUID-based Tracking**: Robust OBS group synchronization with rename-safe tracking -- **Enhanced Footer**: Real-time team/stream counts and OBS connection status -- **Optimized Performance**: Reduced code duplication and standardized API responses +- **Enhanced Footer**: Real-time team/stream counts, OBS connection status, and studio mode indicators +- **API Security**: Optional API key authentication for production deployments +- **Optimized Performance**: Consolidated CSS architecture and standardized API responses ## Quick Start @@ -109,54 +112,22 @@ npm run type-check # TypeScript validation - **Styling**: Custom CSS with glass morphism and Tailwind utilities - **CI/CD**: Forgejo workflows with self-hosted runners -## API Endpoints +## API Documentation -### Stream Management -- `GET /api/streams` - List all streams with team information -- `GET /api/streams/[id]` - Get individual stream details -- `POST /api/addStream` - Create new stream with browser source and team association -- `PUT /api/streams/[id]` - Update stream information -- `DELETE /api/streams/[id]` - Delete stream with comprehensive OBS cleanup: - - Removes stream's nested scene - - Deletes browser source - - Removes from all source switchers - - Clears text files referencing the stream +The application provides a comprehensive REST API for managing streams, teams, and OBS integration. -### Source Control -- `POST /api/setActive` - Set active stream for screen position (writes team-prefixed name to text file) -- `GET /api/getActive` - Get currently active sources for all screen positions +**📚 [Complete API Documentation](docs/API.md)** -### Team Management -- `GET /api/teams` - Get all teams with group information and sync status -- `POST /api/teams` - Create new team with optional OBS scene creation -- `PUT /api/teams/[teamId]` - Update team name, group_name, or group_uuid -- `DELETE /api/teams/[teamId]` - Delete team with comprehensive OBS cleanup: - - Deletes team scene/group - - Removes team text source - - Deletes all associated stream scenes - - Removes all browser sources with team prefix - - Clears all related text files -- `GET /api/getTeamName` - Get team name by ID +Key endpoints include: +- Stream management (CRUD operations) +- Source control for 7 screen positions +- Team and OBS group management +- Scene switching and studio mode controls +- Real-time status monitoring -### OBS Group/Scene Management -- `POST /api/createGroup` - Create OBS scene from team and store UUID -- `POST /api/syncGroups` - Synchronize all teams with OBS groups -- `GET /api/verifyGroups` - Verify database groups exist in OBS with UUID tracking - - Detects orphaned groups (excludes system scenes) - - Identifies name mismatches - - Shows sync status for all teams +All endpoints support API key authentication for production deployments. -### OBS Scene Control -- `POST /api/setScene` - Switch OBS to specified scene (1-Screen, 2-Screen, 4-Screen) -- `GET /api/getCurrentScene` - Get currently active OBS scene - -### System Status -- `GET /api/obsStatus` - Real-time OBS connection, streaming, and recording status - -### Authentication -All endpoints require API key authentication when `API_KEY` environment variable is set. - -See `CLAUDE.md` for detailed architecture documentation and implementation details. +See [`CLAUDE.md`](CLAUDE.md) for detailed architecture documentation and [`docs/API.md`](docs/API.md) for complete endpoint specifications. ## Known Issues @@ -167,7 +138,7 @@ See `CLAUDE.md` for detailed architecture documentation and implementation detai ### System Scene Exclusion Infrastructure scenes containing source switchers are excluded from orphaned group detection: -- 1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies +- 1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies, Resources - Additional scenes can be added to the `SYSTEM_SCENES` array in `/app/api/verifyGroups/route.ts` diff --git a/app/api/__tests__/streams.test.ts b/app/api/__tests__/streams.test.ts index 41f6389..1e1eea1 100644 --- a/app/api/__tests__/streams.test.ts +++ b/app/api/__tests__/streams.test.ts @@ -1,4 +1,4 @@ -import { GET } from '../streams/route'; +// import { GET } from '../streams/route'; // Mock the database module jest.mock('@/lib/database', () => ({ diff --git a/app/api/__tests__/teams.test.ts b/app/api/__tests__/teams.test.ts index bd09053..d7e431b 100644 --- a/app/api/__tests__/teams.test.ts +++ b/app/api/__tests__/teams.test.ts @@ -1,4 +1,4 @@ -import { GET } from '../teams/route'; +// import { GET } from '../teams/route'; // Mock the database module jest.mock('@/lib/database', () => ({ @@ -13,7 +13,7 @@ jest.mock('@/lib/apiHelpers', () => ({ status, json: async () => ({ success: true, data }), })), - createDatabaseError: jest.fn((operation, error) => ({ + createDatabaseError: jest.fn((operation) => ({ error: 'Database Error', status: 500, json: async () => ({ diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 9347106..75a62a3 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -63,7 +63,7 @@ function generateOBSSourceName(teamSceneName: string, streamName: string): strin } export async function POST(request: NextRequest) { - let name: string, url: string, team_id: number, obs_source_name: string; + let name: string, url: string, team_id: number, obs_source_name: string, lockSources: boolean; // Parse and validate request body try { @@ -78,6 +78,7 @@ export async function POST(request: NextRequest) { } ({ name, url, team_id } = validation.data!); + lockSources = body.lockSources !== false; // Default to true if not specified } catch { return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); @@ -125,7 +126,7 @@ export async function POST(request: NextRequest) { if (!sourceExists) { // Create stream group with text overlay - await createStreamGroup(groupName, name, teamInfo.team_name, url); + await createStreamGroup(groupName, name, teamInfo.team_name, url, lockSources); // Update team with group UUID if not set if (!teamInfo.group_uuid) { diff --git a/app/api/obsStatus/route.ts b/app/api/obsStatus/route.ts index 13e8dc4..91c85f6 100644 --- a/app/api/obsStatus/route.ts +++ b/app/api/obsStatus/route.ts @@ -19,9 +19,11 @@ export async function GET() { obsWebSocketVersion: string; }; currentScene?: string; + currentPreviewScene?: string; sceneCount?: number; streaming?: boolean; recording?: boolean; + studioModeEnabled?: boolean; error?: string; } = { host: OBS_HOST, @@ -58,15 +60,31 @@ export async function GET() { // Get recording status const recordStatus = await obs.call('GetRecordStatus'); + // Get studio mode status + const studioModeStatus = await obs.call('GetStudioModeEnabled'); + + // Get preview scene if studio mode is enabled + let currentPreviewScene; + if (studioModeStatus.studioModeEnabled) { + try { + const previewSceneInfo = await obs.call('GetCurrentPreviewScene'); + currentPreviewScene = previewSceneInfo.sceneName; + } catch (previewError) { + console.log('Could not get preview scene:', previewError); + } + } + connectionStatus.connected = true; connectionStatus.version = { obsVersion: versionInfo.obsVersion, obsWebSocketVersion: versionInfo.obsWebSocketVersion }; connectionStatus.currentScene = currentSceneInfo.sceneName; + connectionStatus.currentPreviewScene = currentPreviewScene; connectionStatus.sceneCount = sceneList.scenes.length; connectionStatus.streaming = streamStatus.outputActive; connectionStatus.recording = recordStatus.outputActive; + connectionStatus.studioModeEnabled = studioModeStatus.studioModeEnabled; } catch (err) { connectionStatus.error = err instanceof Error ? err.message : 'Unknown error occurred'; diff --git a/app/api/setScene/route.ts b/app/api/setScene/route.ts index 548e003..2363d32 100644 --- a/app/api/setScene/route.ts +++ b/app/api/setScene/route.ts @@ -32,16 +32,30 @@ export async function POST(request: NextRequest) { try { const obsClient = await getOBSClient(); - // Switch to the requested scene - await obsClient.call('SetCurrentProgramScene', { sceneName }); + // Check if studio mode is active + const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); - console.log(`Successfully switched to scene: ${sceneName}`); - - return NextResponse.json({ - success: true, - data: { sceneName }, - message: `Switched to ${sceneName} layout` - }); + if (studioModeEnabled) { + // In studio mode, switch the preview scene + await obsClient.call('SetCurrentPreviewScene', { sceneName }); + console.log(`Successfully switched preview to scene: ${sceneName} (Studio Mode)`); + + return NextResponse.json({ + success: true, + data: { sceneName, studioMode: true }, + message: `Preview set to ${sceneName} layout (Studio Mode) - ready to transition` + }); + } else { + // Normal mode, switch program scene directly + await obsClient.call('SetCurrentProgramScene', { sceneName }); + console.log(`Successfully switched to scene: ${sceneName}`); + + return NextResponse.json({ + success: true, + data: { sceneName, studioMode: false }, + message: `Switched to ${sceneName} layout` + }); + } } catch (obsError) { console.error('OBS WebSocket error:', obsError); return NextResponse.json( diff --git a/app/api/triggerTransition/route.ts b/app/api/triggerTransition/route.ts new file mode 100644 index 0000000..7136863 --- /dev/null +++ b/app/api/triggerTransition/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from 'next/server'; +import { getOBSClient } from '../../../lib/obsClient'; + +export async function POST() { + try { + const obsClient = await getOBSClient(); + + // Check if studio mode is active + const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); + + if (!studioModeEnabled) { + return NextResponse.json( + { + success: false, + error: 'Studio mode is not enabled', + message: 'Studio mode must be enabled to trigger transitions' + }, + { status: 400 } + ); + } + + try { + // Trigger the studio mode transition (preview to program) + await obsClient.call('TriggerStudioModeTransition'); + console.log('Successfully triggered studio mode transition'); + + // Get the updated scene information after transition + const [programResponse, previewResponse] = await Promise.all([ + obsClient.call('GetCurrentProgramScene'), + obsClient.call('GetCurrentPreviewScene') + ]); + + return NextResponse.json({ + success: true, + data: { + programScene: programResponse.currentProgramSceneName, + previewScene: previewResponse.currentPreviewSceneName + }, + message: 'Successfully transitioned preview to program' + }); + } catch (obsError) { + console.error('OBS WebSocket error during transition:', obsError); + return NextResponse.json( + { + success: false, + error: 'Failed to trigger transition in OBS', + details: obsError instanceof Error ? obsError.message : 'Unknown error' + }, + { status: 500 } + ); + } + } catch (error) { + console.error('Error triggering transition:', error); + return NextResponse.json( + { + success: false, + error: 'Failed to connect to OBS or trigger transition' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index e1107df..dac679e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -20,6 +20,14 @@ --solarized-red: #dc322f; --solarized-magenta: #d33682; --solarized-violet: #6c71c4; + + /* Gradient Custom Properties */ + --gradient-primary: linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan)); + --gradient-active: linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow)); + --gradient-danger: linear-gradient(135deg, var(--solarized-red), var(--solarized-orange)); + --gradient-preview: linear-gradient(135deg, var(--solarized-yellow), var(--solarized-orange)); + --gradient-transition: linear-gradient(135deg, var(--solarized-red), var(--solarized-magenta)); + --gradient-body: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); } /* Modern CSS Foundation */ @@ -34,8 +42,8 @@ html { } body { - background: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); - color: #93a1a1; + background: var(--gradient-body); + color: var(--solarized-base1); min-height: 100vh; line-height: 1.6; } @@ -65,7 +73,8 @@ body { } /* Glass Card Component */ -.glass { +.glass, +.glass-panel { background: rgba(7, 54, 66, 0.4); backdrop-filter: blur(10px); border: 1px solid rgba(88, 110, 117, 0.3); @@ -75,8 +84,8 @@ body { /* Modern Button System */ .btn { - background: linear-gradient(135deg, #268bd2, #2aa198); - color: #fdf6e3; + background: var(--gradient-primary); + color: var(--solarized-base3); border: none; padding: 12px 24px; border-radius: 12px; @@ -93,9 +102,10 @@ body { text-decoration: none; } -.btn.active { - background: linear-gradient(135deg, #859900, #b58900); - color: #fdf6e3; +.btn.active, +.btn-success { + background: var(--gradient-active); + color: var(--solarized-base3); box-shadow: 0 0 0 3px rgba(133, 153, 0, 0.5); transform: translateY(-1px); font-weight: 700; @@ -117,7 +127,7 @@ body { .btn-secondary { background: rgba(88, 110, 117, 0.3); border: 1px solid rgba(131, 148, 150, 0.4); - color: #93a1a1; + color: var(--solarized-base1); backdrop-filter: blur(10px); } @@ -127,25 +137,14 @@ body { box-shadow: 0 6px 20px rgba(88, 110, 117, 0.3); } -/* Success Button */ -.btn-success { - background: linear-gradient(135deg, #859900, #b58900); - color: #fdf6e3; -} - -.btn-success:hover { - background: linear-gradient(135deg, #b58900, #859900); - box-shadow: 0 6px 20px rgba(133, 153, 0, 0.4); -} - /* Danger Button */ .btn-danger { - background: linear-gradient(135deg, #dc322f, #cb4b16); - color: #fdf6e3; + background: var(--gradient-danger); + color: var(--solarized-base3); } .btn-danger:hover { - background: linear-gradient(135deg, #cb4b16, #dc322f); + background: linear-gradient(135deg, var(--solarized-orange), var(--solarized-red)); box-shadow: 0 6px 20px rgba(220, 50, 47, 0.4); } @@ -169,6 +168,18 @@ body { flex-shrink: 0; } +/* Scene Button Variants */ +.btn-scene-preview { + background: var(--gradient-preview); + color: var(--solarized-base3); +} + +.btn-scene-transition { + background: var(--gradient-transition); + color: var(--solarized-base3); + min-width: 120px; +} + /* Form spacing fixes since Tailwind gap classes aren't working */ .form-row { display: flex; @@ -194,7 +205,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: #93a1a1; + color: var(--solarized-base1); width: 100%; transition: all 0.3s ease; } @@ -205,7 +216,7 @@ body { .input:focus { outline: none; - border-color: #268bd2; + border-color: var(--solarized-blue); box-shadow: 0 0 0 3px rgba(38, 139, 210, 0.2); } @@ -215,7 +226,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: #93a1a1; + color: var(--solarized-base1); width: 100%; text-align: left; cursor: pointer; @@ -241,6 +252,28 @@ body { position: absolute; transform: translateZ(0); will-change: transform; + max-height: 400px; + overflow-y: auto; +} + +/* Custom scrollbar for dropdown */ +.dropdown-menu::-webkit-scrollbar { + width: 8px; +} + +.dropdown-menu::-webkit-scrollbar-track { + background: rgba(7, 54, 66, 0.5); + border-radius: 4px; +} + +.dropdown-menu::-webkit-scrollbar-thumb { + background: rgba(88, 110, 117, 0.6); + border-radius: 4px; + transition: background 0.2s ease; +} + +.dropdown-menu::-webkit-scrollbar-thumb:hover { + background: rgba(88, 110, 117, 0.8); } .dropdown-item { @@ -248,7 +281,7 @@ body { cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid rgba(88, 110, 117, 0.2); - color: #93a1a1; + color: var(--solarized-base1); } .dropdown-item:last-child { @@ -261,7 +294,7 @@ body { .dropdown-item.active { background: rgba(38, 139, 210, 0.3); - color: #fdf6e3; + color: var(--solarized-base3); } /* Icon Sizes */ @@ -356,6 +389,22 @@ body { margin-bottom: 32px; } +.ml-3 { + margin-left: 12px; +} + +.mr-1 { + margin-right: 4px; +} + +.mr-2 { + margin-right: 8px; +} + +.mr-4 { + margin-right: 16px; +} + .p-4 { padding: 16px; } @@ -366,4 +415,134 @@ body { .p-8 { padding: 32px; -} \ No newline at end of file +} + +/* Collapsible Group Styles */ +.collapsible-group { + margin-bottom: 16px; +} + +.collapsible-header { + width: 100%; + background: rgba(7, 54, 66, 0.3); + border: 1px solid rgba(88, 110, 117, 0.3); + border-radius: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 1px; +} + +.collapsible-header:hover { + background: rgba(7, 54, 66, 0.5); + border-color: rgba(131, 148, 150, 0.4); +} + +.collapsible-header-content { + display: flex; + align-items: center; + gap: 12px; +} + +.collapsible-icon { + flex-shrink: 0; + transition: transform 0.3s ease; + color: var(--solarized-base1); +} + +.collapsible-icon.open { + transform: rotate(90deg); +} + +.collapsible-title { + flex: 1; + font-size: 18px; + font-weight: 600; + color: var(--solarized-base3); + text-align: left; + margin: 0; +} + +.collapsible-count { + background: rgba(38, 139, 210, 0.2); + color: var(--solarized-blue); + padding: 4px 12px; + border-radius: 20px; + font-size: 14px; + font-weight: 600; +} + +.collapsible-content { + overflow: hidden; + transition: all 0.3s ease; + max-height: 0; + opacity: 0; +} + +.collapsible-content.open { + max-height: 5000px; + opacity: 1; + margin-top: 12px; +} + +.collapsible-content-inner { + padding: 20px; + background: rgba(7, 54, 66, 0.2); + border: 1px solid rgba(88, 110, 117, 0.2); + border-radius: 12px; +} + +/* Solarized Color Utilities */ +.text-base03 { color: var(--solarized-base03); } +.text-base02 { color: var(--solarized-base02); } +.text-base01 { color: var(--solarized-base01); } +.text-base00 { color: var(--solarized-base00); } +.text-base0 { color: var(--solarized-base0); } +.text-base1 { color: var(--solarized-base1); } +.text-base2 { color: var(--solarized-base2); } +.text-base3 { color: var(--solarized-base3); } +.text-blue { color: var(--solarized-blue); } +.text-cyan { color: var(--solarized-cyan); } +.text-green { color: var(--solarized-green); } +.text-yellow { color: var(--solarized-yellow); } +.text-orange { color: var(--solarized-orange); } +.text-red { color: var(--solarized-red); } +.text-magenta { color: var(--solarized-magenta); } +.text-violet { color: var(--solarized-violet); } + +.bg-base03 { background-color: var(--solarized-base03); } +.bg-base02 { background-color: var(--solarized-base02); } +.bg-base01 { background-color: var(--solarized-base01); } +.bg-base00 { background-color: var(--solarized-base00); } +.bg-base0 { background-color: var(--solarized-base0); } +.bg-base1 { background-color: var(--solarized-base1); } +.bg-base2 { background-color: var(--solarized-base2); } +.bg-base3 { background-color: var(--solarized-base3); } +.bg-blue { background-color: var(--solarized-blue); } +.bg-cyan { background-color: var(--solarized-cyan); } +.bg-green { background-color: var(--solarized-green); } +.bg-yellow { background-color: var(--solarized-yellow); } +.bg-orange { background-color: var(--solarized-orange); } +.bg-red { background-color: var(--solarized-red); } +.bg-magenta { background-color: var(--solarized-magenta); } +.bg-violet { background-color: var(--solarized-violet); } + +.border-base01 { border-color: var(--solarized-base01); } +.border-base02 { border-color: var(--solarized-base02); } +.border-blue { border-color: var(--solarized-blue); } +.border-cyan { border-color: var(--solarized-cyan); } +.border-green { border-color: var(--solarized-green); } +.border-yellow { border-color: var(--solarized-yellow); } +.border-orange { border-color: var(--solarized-orange); } +.border-red { border-color: var(--solarized-red); } + +/* Border opacity utilities */ +.border-green\/30 { border-color: rgba(133, 153, 0, 0.3); } +.border-yellow\/30 { border-color: rgba(181, 137, 0, 0.3); } +.border-blue\/30 { border-color: rgba(38, 139, 210, 0.3); } +.border-red\/30 { border-color: rgba(220, 50, 47, 0.3); } + +/* Focus utilities */ +.focus\:outline-none:focus { outline: none; } +.focus\:ring-2:focus { box-shadow: 0 0 0 2px var(--solarized-blue); } +.focus\:ring-blue:focus { box-shadow: 0 0 0 2px var(--solarized-blue); } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index c3ab1bc..4b3dbbc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,7 @@ import './globals.css'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; import { ErrorBoundary } from '@/components/ErrorBoundary'; -import PerformanceDashboard from '@/components/PerformanceDashboard'; +import { ApiKeyProvider } from '@/contexts/ApiKeyContext'; export const metadata = { title: 'Live Stream Manager', @@ -13,14 +13,15 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( -
-
- - {children} - -
-