|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | + <head> |
| 4 | + <meta charset="UTF-8" /> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | + <title>Go Web — Iframe Embed Test</title> |
| 7 | + <style> |
| 8 | + * { box-sizing: border-box; margin: 0; padding: 0; } |
| 9 | + |
| 10 | + body { |
| 11 | + font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif; |
| 12 | + background: #f0f0f0; |
| 13 | + color: #333; |
| 14 | + padding: 40px; |
| 15 | + } |
| 16 | + |
| 17 | + h1 { |
| 18 | + font-size: 24px; |
| 19 | + margin-bottom: 8px; |
| 20 | + } |
| 21 | + |
| 22 | + p { |
| 23 | + color: #666; |
| 24 | + margin-bottom: 24px; |
| 25 | + line-height: 1.5; |
| 26 | + } |
| 27 | + |
| 28 | + .demo-container { |
| 29 | + height: 500px; |
| 30 | + border-radius: 12px; |
| 31 | + overflow: hidden; |
| 32 | + box-shadow: 0 4px 24px rgba(0,0,0,0.12); |
| 33 | + margin-bottom: 32px; |
| 34 | + } |
| 35 | + |
| 36 | + .controls { |
| 37 | + margin-bottom: 24px; |
| 38 | + display: flex; |
| 39 | + gap: 12px; |
| 40 | + flex-wrap: wrap; |
| 41 | + } |
| 42 | + |
| 43 | + button { |
| 44 | + padding: 8px 16px; |
| 45 | + border-radius: 6px; |
| 46 | + border: 1px solid #ccc; |
| 47 | + background: #fff; |
| 48 | + cursor: pointer; |
| 49 | + font-size: 14px; |
| 50 | + } |
| 51 | + |
| 52 | + button:hover { background: #f5f5f5; } |
| 53 | + button.active { background: #0071e3; color: #fff; border-color: #0071e3; } |
| 54 | + |
| 55 | + code { |
| 56 | + background: #e8e8e8; |
| 57 | + padding: 2px 6px; |
| 58 | + border-radius: 4px; |
| 59 | + font-size: 13px; |
| 60 | + } |
| 61 | + |
| 62 | + .code-block { |
| 63 | + background: #1e1e1e; |
| 64 | + color: #d4d4d4; |
| 65 | + padding: 16px 20px; |
| 66 | + border-radius: 8px; |
| 67 | + font-family: 'SF Mono', 'Fira Code', Menlo, monospace; |
| 68 | + font-size: 13px; |
| 69 | + line-height: 1.6; |
| 70 | + overflow-x: auto; |
| 71 | + margin-bottom: 32px; |
| 72 | + white-space: pre; |
| 73 | + } |
| 74 | + |
| 75 | + h2 { |
| 76 | + font-size: 18px; |
| 77 | + margin-bottom: 12px; |
| 78 | + } |
| 79 | + |
| 80 | + .info { |
| 81 | + background: #e3f2fd; |
| 82 | + border: 1px solid #90caf9; |
| 83 | + border-radius: 8px; |
| 84 | + padding: 12px 16px; |
| 85 | + margin-bottom: 24px; |
| 86 | + font-size: 14px; |
| 87 | + line-height: 1.5; |
| 88 | + } |
| 89 | + |
| 90 | + </style> |
| 91 | + </head> |
| 92 | + <body> |
| 93 | + <h1>Iframe Embed API Test</h1> |
| 94 | + <p> |
| 95 | + This page demonstrates the <code>@lynx-js/go-web</code> iframe embed API. |
| 96 | + It loads the Go component inside an iframe via a pure JS <code>mount()</code> call — |
| 97 | + no React dependency needed on the host page. |
| 98 | + </p> |
| 99 | + |
| 100 | + <div class="info"> |
| 101 | + <strong>How to test:</strong><br/> |
| 102 | + 1. Start dev server: <code>cd example && pnpm dev</code> (serves on <strong>localhost:5969</strong>)<br/> |
| 103 | + 2. Open this file in a browser (or <code>npx serve example-iframe-embed</code>)<br/> |
| 104 | + Example data is proxied from <strong>go.lynxjs.org</strong> via <code>/proxy-lynx-examples</code>. |
| 105 | + </div> |
| 106 | + |
| 107 | + <h2>Usage</h2> |
| 108 | + <div class="code-block"><div id="demo" style="height: 500px"></div> |
| 109 | +<script type="module"> |
| 110 | + import { mount } from 'https://go.lynxjs.org/embed.js'; |
| 111 | + mount('#demo', { |
| 112 | + example: 'hello-world', |
| 113 | + defaultFile: 'src/App.tsx', |
| 114 | + exampleBasePath: 'https://go.lynxjs.org/lynx-examples', |
| 115 | + }); |
| 116 | +</script></div> |
| 117 | + |
| 118 | + <h2>Live Demo</h2> |
| 119 | + |
| 120 | + <div class="controls" id="controls"></div> |
| 121 | + |
| 122 | + <div class="demo-container" id="demo"></div> |
| 123 | + |
| 124 | + <script type="module"> |
| 125 | + // --------------------------------------------------------------------------- |
| 126 | + // Config |
| 127 | + // --------------------------------------------------------------------------- |
| 128 | + |
| 129 | + // The embed React app runs on localhost (cd example && pnpm dev). |
| 130 | + // Example data is proxied through the dev server to avoid CORS issues. |
| 131 | + // The proxy rewrites /proxy-lynx-examples/* → go.lynxjs.org/lynx-examples/* |
| 132 | + const EMBED_BASE = 'http://localhost:5969'; |
| 133 | + const EXAMPLE_BASE_PATH = '/proxy-lynx-examples'; |
| 134 | + |
| 135 | + // --------------------------------------------------------------------------- |
| 136 | + // Inline mount() — mimics src/embed.ts for local testing |
| 137 | + // In production you'd: import { mount } from 'https://go.lynxjs.org/embed.js' |
| 138 | + // --------------------------------------------------------------------------- |
| 139 | + |
| 140 | + function mount(container, options) { |
| 141 | + const el = typeof container === 'string' |
| 142 | + ? document.querySelector(container) |
| 143 | + : container; |
| 144 | + |
| 145 | + if (!el) throw new Error('Container not found: ' + container); |
| 146 | + |
| 147 | + const embedUrl = new URL(EMBED_BASE + '/embed'); |
| 148 | + |
| 149 | + const iframe = document.createElement('iframe'); |
| 150 | + iframe.src = embedUrl.href; |
| 151 | + iframe.style.cssText = 'width: 100%; height: 100%; border: none;'; |
| 152 | + |
| 153 | + let currentOptions = { ...options }; |
| 154 | + |
| 155 | + function handleMessage(event) { |
| 156 | + if (event.source !== iframe.contentWindow) return; |
| 157 | + if (event.data?.type === 'go-embed:ready') { |
| 158 | + iframe.contentWindow.postMessage({ |
| 159 | + type: 'go-embed:init', |
| 160 | + options: currentOptions, |
| 161 | + }, '*'); |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + window.addEventListener('message', handleMessage); |
| 166 | + el.innerHTML = ''; |
| 167 | + el.appendChild(iframe); |
| 168 | + |
| 169 | + return { |
| 170 | + iframe, |
| 171 | + update(newOptions) { |
| 172 | + currentOptions = { ...currentOptions, ...newOptions }; |
| 173 | + iframe.contentWindow.postMessage({ |
| 174 | + type: 'go-embed:update', |
| 175 | + options: newOptions, |
| 176 | + }, '*'); |
| 177 | + }, |
| 178 | + destroy() { |
| 179 | + window.removeEventListener('message', handleMessage); |
| 180 | + el.innerHTML = ''; |
| 181 | + }, |
| 182 | + }; |
| 183 | + } |
| 184 | + |
| 185 | + // --------------------------------------------------------------------------- |
| 186 | + // Fetch available examples from production |
| 187 | + // --------------------------------------------------------------------------- |
| 188 | + |
| 189 | + async function fetchExampleList() { |
| 190 | + // Try a few known examples; the production site doesn't expose a directory listing. |
| 191 | + // Requests go through the dev server proxy to avoid CORS. |
| 192 | + const knownExamples = [ |
| 193 | + 'hello-world', 'text', 'view', 'image', 'css', |
| 194 | + 'scroll-view', 'list', 'navigator', |
| 195 | + ]; |
| 196 | + |
| 197 | + const available = []; |
| 198 | + await Promise.all( |
| 199 | + knownExamples.map(async (name) => { |
| 200 | + try { |
| 201 | + const res = await fetch(`${EMBED_BASE}${EXAMPLE_BASE_PATH}/${name}/example-metadata.json`, { method: 'HEAD' }); |
| 202 | + if (res.ok) available.push(name); |
| 203 | + } catch { /* skip */ } |
| 204 | + }), |
| 205 | + ); |
| 206 | + |
| 207 | + // Preserve original order |
| 208 | + return knownExamples.filter((n) => available.includes(n)); |
| 209 | + } |
| 210 | + |
| 211 | + // --------------------------------------------------------------------------- |
| 212 | + // Mount demos |
| 213 | + // --------------------------------------------------------------------------- |
| 214 | + |
| 215 | + const examples = await fetchExampleList(); |
| 216 | + if (examples.length === 0) { |
| 217 | + examples.push('hello-world'); // fallback |
| 218 | + } |
| 219 | + |
| 220 | + const EXAMPLES = examples.map((name) => ({ |
| 221 | + name, |
| 222 | + file: 'src/App.tsx', |
| 223 | + })); |
| 224 | + |
| 225 | + // Mount main demo |
| 226 | + let embed = mount('#demo', { |
| 227 | + example: EXAMPLES[0].name, |
| 228 | + defaultFile: EXAMPLES[0].file, |
| 229 | + defaultTab: 'web', |
| 230 | + exampleBasePath: EXAMPLE_BASE_PATH, |
| 231 | + }); |
| 232 | + |
| 233 | + // Create example switcher buttons |
| 234 | + const controls = document.getElementById('controls'); |
| 235 | + EXAMPLES.forEach((ex, i) => { |
| 236 | + const btn = document.createElement('button'); |
| 237 | + btn.textContent = ex.name; |
| 238 | + if (i === 0) btn.classList.add('active'); |
| 239 | + btn.addEventListener('click', () => { |
| 240 | + controls.querySelectorAll('button').forEach(b => b.classList.remove('active')); |
| 241 | + btn.classList.add('active'); |
| 242 | + embed.destroy(); |
| 243 | + embed = mount('#demo', { |
| 244 | + example: ex.name, |
| 245 | + defaultFile: ex.file, |
| 246 | + defaultTab: 'web', |
| 247 | + exampleBasePath: EXAMPLE_BASE_PATH, |
| 248 | + }); |
| 249 | + }); |
| 250 | + controls.appendChild(btn); |
| 251 | + }); |
| 252 | + |
| 253 | + </script> |
| 254 | + </body> |
| 255 | +</html> |
0 commit comments