Files
openchamber/packages/web/index.html
T

311 lines
15 KiB
HTML
Raw Normal View History

2025-12-07 19:32:53 +02:00
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<!-- Favicon -->
2025-12-21 22:59:06 +02:00
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
2025-12-07 19:32:53 +02:00
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicon-16.png" sizes="16x16" />
2025-12-21 22:59:06 +02:00
<link rel="mask-icon" href="/favicon.svg" color="#edb449" />
2025-12-07 19:32:53 +02:00
<!-- Apple touch icon - PNG format required for iOS PWA support -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png" />
<!-- Preload Nerd Fonts for terminal icon display -->
<link rel="preload" href="https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2"
as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2"
as="font" type="font/woff2" crossorigin="anonymous">
2025-12-07 19:32:53 +02:00
<!-- Web app manifest (data URL to avoid nginx auth issues) -->
<script>
const baseUrl = location.origin;
const manifest = {
"name": "OpenChamber - AI Coding Assistant",
"short_name": "OpenChamber",
"description": "Web interface companion for OpenCode AI coding agent",
"start_url": baseUrl + "/",
"display": "standalone",
"background_color": "#151313",
"theme_color": "#edb449",
"orientation": "portrait-primary",
"icons": [
{
"src": baseUrl + "/logo-dark.svg",
2025-12-21 22:59:06 +02:00
"sizes": "512x512",
2025-12-07 19:32:53 +02:00
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
2025-12-21 22:59:06 +02:00
"src": baseUrl + "/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any"
},
{
"src": baseUrl + "/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any"
2025-12-07 19:32:53 +02:00
},
{
"src": baseUrl + "/favicon-32.png",
"sizes": "32x32",
"type": "image/png"
2025-12-21 22:59:06 +02:00
},
{
"src": baseUrl + "/favicon-16.png",
"sizes": "16x16",
"type": "image/png"
2025-12-07 19:32:53 +02:00
}
],
"categories": ["developer", "tools", "productivity"],
"lang": "en"
};
const manifestBlob = new Blob([JSON.stringify(manifest)], {type: 'application/manifest+json'});
const manifestURL = URL.createObjectURL(manifestBlob);
const link = document.createElement('link');
link.rel = 'manifest';
link.href = manifestURL;
document.head.appendChild(link);
</script>
<script>
2025-12-17 12:57:09 +02:00
// Blocking script to detect and apply theme before first paint
2025-12-07 19:32:53 +02:00
(function() {
try {
2025-12-17 12:57:09 +02:00
var themeMode = localStorage.getItem('themeMode');
2025-12-07 19:32:53 +02:00
var variant = localStorage.getItem('selectedThemeVariant');
var useSystem = localStorage.getItem('useSystemTheme');
2025-12-17 12:57:09 +02:00
var isDark;
// Check themeMode first (new storage key)
if (themeMode === 'dark') {
isDark = true;
} else if (themeMode === 'light') {
isDark = false;
} else if (themeMode === 'system' || useSystem === null || useSystem === 'true') {
// System preference
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
} else if (variant === 'light' || variant === 'dark') {
// Legacy storage key fallback
isDark = variant === 'dark';
} else {
// Default to system
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
2025-12-07 19:32:53 +02:00
}
2025-12-17 12:57:09 +02:00
// Apply theme class and data attribute
document.documentElement.classList.add(isDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-splash-variant', isDark ? 'dark' : 'light');
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
2025-12-07 19:32:53 +02:00
} catch (error) {
2025-12-17 12:57:09 +02:00
console.warn('Failed to apply theme:', error);
2025-12-07 19:32:53 +02:00
}
})();
</script>
<!-- Theme color - Safari iOS 26+ prioritizes CSS background-color over this, but keep as fallback -->
<meta name="theme-color" content="#151313" />
<meta name="theme-color" content="#151313" media="(prefers-color-scheme: dark)" />
<!-- iOS Safari PWA styling -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="OpenChamber" />
<title>OpenChamber - AI Coding Assistant</title>
<meta name="description" content="Web interface companion for OpenCode AI coding agent" />
<meta name="application-name" content="OpenChamber" />
<meta name="apple-mobile-web-app-title" content="OpenChamber" />
<!-- Inline CSS for loading screen and Nerd Fonts (before Tailwind loads) -->
2025-12-07 19:32:53 +02:00
<style>
/* Nerd Font @font-face declarations for terminal icon support */
@font-face {
font-family: 'JetBrainsMono Nerd Font';
src:
local('JetBrainsMono Nerd Font'),
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2') format('woff2'),
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+E000-F8FF, U+F0000-FFFFF;
}
@font-face {
font-family: 'FiraCode Nerd Font';
src:
local('FiraCode Nerd Font'),
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2') format('woff2'),
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+E000-F8FF, U+F0000-FFFFF;
}
2025-12-07 19:32:53 +02:00
:root {
--splash-background: #151313;
2025-12-21 22:59:06 +02:00
--splash-stroke: white;
--splash-face-fill: rgba(255, 255, 255, 0.15);
--splash-cell-fill: rgba(255, 255, 255, 0.35);
--splash-logo-fill: white;
2025-12-07 19:32:53 +02:00
}
html[data-splash-variant='light'] {
--splash-background: #F6F4EF;
2025-12-21 22:59:06 +02:00
--splash-stroke: black;
--splash-face-fill: rgba(0, 0, 0, 0.15);
--splash-cell-fill: rgba(0, 0, 0, 0.4);
--splash-logo-fill: black;
2025-12-07 19:32:53 +02:00
}
2025-12-21 22:59:06 +02:00
@keyframes logo-pulse {
2025-12-07 19:32:53 +02:00
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
2025-12-21 22:59:06 +02:00
.logo-pulse {
animation: logo-pulse 3s ease-in-out infinite;
2025-12-07 19:32:53 +02:00
}
#initial-loading {
background-color: var(--splash-background);
color: var(--splash-foreground);
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
transition: opacity 0.3s ease-out;
position: absolute;
width: 100%;
z-index: 9999;
}
#initial-loading.fade-out {
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body class="h-full bg-background text-foreground">
<div id="root" class="h-full">
<!-- Loading fallback while React initializes -->
<div id="initial-loading">
<div style="display: flex; align-items: center; justify-content: center;">
2025-12-21 22:59:06 +02:00
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenChamber loading icon">
<!-- Isometric cube: edge=48, centerY=50 -->
<!-- Points: top(50,2), left(8.432,26), right(91.568,26), center(50,50), bottomLeft(8.432,74), bottomRight(91.568,74), bottom(50,98) -->
<!-- Left face - base fill with stroke -->
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- Left face grid cells (4x4) with varying opacity -->
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M29.216 38 L18.824 32 L18.824 44 L29.216 50 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M18.824 32 L8.432 26 L8.432 38 L18.824 44 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M50 62 L39.608 56 L39.608 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M39.608 56 L29.216 50 L29.216 62 L39.608 68 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M29.216 50 L18.824 44 L18.824 56 L29.216 62 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M18.824 44 L8.432 38 L8.432 50 L18.824 56 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 74 L39.608 68 L39.608 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M39.608 68 L29.216 62 L29.216 74 L39.608 80 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M29.216 62 L18.824 56 L18.824 68 L29.216 74 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M18.824 56 L8.432 50 L8.432 62 L18.824 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L39.608 80 L39.608 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<!-- Right face - base fill with stroke -->
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- Right face grid cells (4x4) with varying opacity -->
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M70.784 38 L81.176 32 L81.176 44 L70.784 50 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M81.176 32 L91.568 26 L91.568 38 L81.176 44 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 62 L60.392 56 L60.392 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M60.392 56 L70.784 50 L70.784 62 L60.392 68 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M70.784 50 L81.176 44 L81.176 56 L70.784 62 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M81.176 44 L91.568 38 L91.568 50 L81.176 56 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M50 74 L60.392 68 L60.392 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M60.392 68 L70.784 62 L70.784 74 L60.392 80 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M70.784 62 L81.176 56 L81.176 68 L70.784 74 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M81.176 56 L91.568 50 L91.568 62 L81.176 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L60.392 80 L60.392 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<!-- Top face - open (no fill), only stroke -->
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g class="logo-pulse" transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="var(--splash-logo-fill)"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="var(--splash-logo-fill)" fill-opacity="0.4"/>
</g>
2025-12-07 19:32:53 +02:00
</svg>
</div>
</div>
</div>
<script>
// Fallback: hide loading screen after 10 seconds if React fails to load
setTimeout(function() {
const loading = document.getElementById('initial-loading');
if (loading) {
console.warn('Loading screen timeout - forcing hide after 10s');
loading.classList.add('fade-out');
setTimeout(function() {
loading.remove();
}, 300);
}
}, 10000);
</script>
<!-- CSS Font Loading API for reliable Nerd Font loading -->
<script>
(function() {
const fonts = [
{
name: 'JetBrainsMono Nerd Font',
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2'
},
{
name: 'FiraCode Nerd Font',
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2'
}
];
const fontPromises = fonts.map(font => {
const fontFace = new FontFace(font.name, `url(${font.url}) format('woff2')`);
document.fonts.add(fontFace);
return fontFace.load().catch(err => {
console.warn(`Failed to load font: ${font.name}`, err);
});
});
Promise.allSettled(fontPromises).then(() => {
document.documentElement.classList.add('fonts-loaded');
});
})();
</script>
2025-12-07 19:32:53 +02:00
<!-- Polyfill for process before loading React -->
<script>
if (typeof process === 'undefined') {
window.process = { env: {} };
}
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>