@hybridlabor-api/aos 4.4.2-beta.7 → 4.4.2-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -616,110 +616,6 @@ must not do.
616
616
 
617
617
  ## Headless timeouts
618
618
 
619
- A trivial headless `agy` prompt was measured at **605s** in 2026-09.
620
- `agy-delegate` defaults to `--print-timeout 5m`, so it aborts at 300s and
621
- reports an empty body while the answer is still arriving. Pass `--timeout 15m`
622
- for anything non-trivial. A short timeout does not read as "slow", it reads as
623
- "broken".
624
-
625
- # AOS — Antigravity / Gemini
626
-
627
- **Read [AGENTS.md](AGENTS.md) first.** It holds every rule that applies to all
628
- harnesses: the non-negotiables, the release gate, Conventional Commits, the
629
- skill contract and category routing, the pipeline variants, and the delegation
630
- policy. Nothing from it is repeated here — a rule that lives in two files is a
631
- rule that will eventually disagree with itself.
632
-
633
- This file covers only what is specific to running AOS under Antigravity.
634
-
635
- ## Nothing enforces the gate here — you are the enforcement
636
-
637
- `.claude/hooks/go-gate.mjs` is a Claude Code hook and does not run on this
638
- harness. Not one of the four release commands is mechanically blocked for you,
639
- and neither is anything else.
640
-
641
- So the full rule in AGENTS.md applies here in its entirety, not just its
642
- hook-enforced subset: when the user asks for a plan, review, audit or
643
- multi-step action, you are read-only until they answer with the literal **GO**
644
- — including file writes and `git commit`. On Claude Code a hook would catch the
645
- four worst cases if discipline failed. Here there is no backstop, which makes
646
- this the harness where the rule matters most, not least.
647
-
648
- ## Model routing
649
-
650
- The tier names in `agy-delegate` map to models that go stale — the built-in
651
- `flash` still points at Gemini 3.7 while 3.8 ships. Re-check against
652
- `agy models` after every upgrade; the id carries both the version and the
653
- effort suffix.
654
-
655
- | Work | Model |
656
- |---|---|
657
- | Media, fast or mechanical coding, boilerplate | `Gemini 3.8 Flash (Medium)` |
658
- | Trivial one-liners | `Gemini 3.8 Flash (Low)` |
659
- | Review, architecture, hard reasoning | `Claude Sonnet 4.6 (Thinking)` |
660
-
661
- Set these once via `CLAUDE_PLUGIN_OPTION_TIER_{FLASH,FLASH_LO,PRO}` in
662
- `~/.zshenv` — **not** `~/.zshrc`, which non-interactive tool shells never
663
- source.
664
-
665
- Adversarial review is the case that most repays the stronger model: a Flash
666
- tier tends to agree with what it is shown, which is the one thing a reviewer
667
- must not do.
668
-
669
- ## Headless timeouts
670
-
671
- A trivial headless `agy` prompt was measured at **605s** in 2026-09.
672
- `agy-delegate` defaults to `--print-timeout 5m`, so it aborts at 300s and
673
- reports an empty body while the answer is still arriving. Pass `--timeout 15m`
674
- for anything non-trivial. A short timeout does not read as "slow", it reads as
675
- "broken".
676
-
677
- # AOS — Antigravity / Gemini
678
-
679
- **Read [AGENTS.md](AGENTS.md) first.** It holds every rule that applies to all
680
- harnesses: the non-negotiables, the release gate, Conventional Commits, the
681
- skill contract and category routing, the pipeline variants, and the delegation
682
- policy. Nothing from it is repeated here — a rule that lives in two files is a
683
- rule that will eventually disagree with itself.
684
-
685
- This file covers only what is specific to running AOS under Antigravity.
686
-
687
- ## Nothing enforces the gate here — you are the enforcement
688
-
689
- `.claude/hooks/go-gate.mjs` is a Claude Code hook and does not run on this
690
- harness. Not one of the four release commands is mechanically blocked for you,
691
- and neither is anything else.
692
-
693
- So the full rule in AGENTS.md applies here in its entirety, not just its
694
- hook-enforced subset: when the user asks for a plan, review, audit or
695
- multi-step action, you are read-only until they answer with the literal **GO**
696
- — including file writes and `git commit`. On Claude Code a hook would catch the
697
- four worst cases if discipline failed. Here there is no backstop, which makes
698
- this the harness where the rule matters most, not least.
699
-
700
- ## Model routing
701
-
702
- The tier names in `agy-delegate` map to models that go stale — the built-in
703
- `flash` still points at Gemini 3.7 while 3.8 ships. Re-check against
704
- `agy models` after every upgrade; the id carries both the version and the
705
- effort suffix.
706
-
707
- | Work | Model |
708
- |---|---|
709
- | Media, fast or mechanical coding, boilerplate | `Gemini 3.8 Flash (Medium)` |
710
- | Trivial one-liners | `Gemini 3.8 Flash (Low)` |
711
- | Review, architecture, hard reasoning | `Claude Sonnet 4.6 (Thinking)` |
712
-
713
- Set these once via `CLAUDE_PLUGIN_OPTION_TIER_{FLASH,FLASH_LO,PRO}` in
714
- `~/.zshenv` — **not** `~/.zshrc`, which non-interactive tool shells never
715
- source.
716
-
717
- Adversarial review is the case that most repays the stronger model: a Flash
718
- tier tends to agree with what it is shown, which is the one thing a reviewer
719
- must not do.
720
-
721
- ## Headless timeouts
722
-
723
619
  A trivial headless `agy` prompt was measured at **605s** in 2026-09.
724
620
  `agy-delegate` defaults to `--print-timeout 5m`, so it aborts at 300s and
725
621
  reports an empty body while the answer is still arriving. Pass `--timeout 15m`
@@ -0,0 +1,276 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const crypto = require('crypto');
6
+ const { execSync, spawn } = require('child_process');
7
+
8
+ const ISSUER = 'https://auth.awsbdb.rcentry.pro';
9
+ const TARBALL_URL = 'https://internal.awsbdb.rcentry.pro/aos-internal/aos-internal.tgz';
10
+ const CLIENT_ID = 'aos-installer';
11
+ const CONFIG_DIR = path.join(os.homedir(), '.agents');
12
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'aos-internal.oauth.json');
13
+
14
+ function httpsGet(url) {
15
+ return new Promise((resolve, reject) => {
16
+ https.get(url, { timeout: 10000 }, (res) => {
17
+ let data = '';
18
+ res.on('data', chunk => data += chunk);
19
+ res.on('end', () => {
20
+ try {
21
+ resolve(JSON.parse(data));
22
+ } catch (e) {
23
+ reject(new Error(`Failed to parse response: ${e.message}`));
24
+ }
25
+ });
26
+ }).on('error', reject).on('timeout', function() {
27
+ this.destroy();
28
+ reject(new Error('Request timeout'));
29
+ });
30
+ });
31
+ }
32
+
33
+ function httpsPost(url, body) {
34
+ return new Promise((resolve, reject) => {
35
+ const bodyStr = JSON.stringify(body);
36
+ const req = https.request(url, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/x-www-form-urlencoded',
40
+ 'Content-Length': Buffer.byteLength(bodyStr)
41
+ },
42
+ timeout: 10000
43
+ }, (res) => {
44
+ let data = '';
45
+ res.on('data', chunk => data += chunk);
46
+ res.on('end', () => {
47
+ try {
48
+ resolve({ status: res.statusCode, body: JSON.parse(data) });
49
+ } catch (e) {
50
+ reject(new Error(`Failed to parse response: ${e.message}`));
51
+ }
52
+ });
53
+ });
54
+ req.on('error', reject);
55
+ req.on('timeout', function() {
56
+ this.destroy();
57
+ reject(new Error('Request timeout'));
58
+ });
59
+
60
+ const params = new URLSearchParams(body);
61
+ req.write(params.toString());
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ function httpsGetBinary(url, headers) {
67
+ return new Promise((resolve, reject) => {
68
+ const req = https.get(url, { headers, timeout: 30000 }, (res) => {
69
+ if (res.statusCode !== 200) {
70
+ reject(new Error(`HTTP ${res.statusCode}`));
71
+ return;
72
+ }
73
+ const chunks = [];
74
+ res.on('data', chunk => chunks.push(chunk));
75
+ res.on('end', () => resolve(Buffer.concat(chunks)));
76
+ });
77
+ req.on('error', reject);
78
+ req.on('timeout', function() {
79
+ this.destroy();
80
+ reject(new Error('Request timeout'));
81
+ });
82
+ });
83
+ }
84
+
85
+ function generatePKCE() {
86
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
87
+ const codeChallenge = crypto.createHash('sha256')
88
+ .update(codeVerifier)
89
+ .digest('base64url');
90
+ return { codeVerifier, codeChallenge };
91
+ }
92
+
93
+ function tryOpenBrowser(verificationUri) {
94
+ const cmd = process.platform === 'darwin' ? 'open' :
95
+ process.platform === 'win32' ? 'start' : 'xdg-open';
96
+ try {
97
+ if (process.platform === 'win32') {
98
+ execSync(`start ${verificationUri}`, { stdio: 'ignore', shell: true });
99
+ } else {
100
+ execSync(`${cmd} "${verificationUri}"`, { stdio: 'ignore' });
101
+ }
102
+ } catch (e) {
103
+ // Browser open failed, but that's OK -- user can type the code manually
104
+ }
105
+ }
106
+
107
+ function loadStoredToken() {
108
+ try {
109
+ if (!fs.existsSync(TOKEN_FILE)) return null;
110
+ const data = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
111
+ return data.refresh_token || null;
112
+ } catch (e) {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ function saveToken(refreshToken) {
118
+ try {
119
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
120
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify({ refresh_token: refreshToken }, null, 2), 'utf8');
121
+ fs.chmodSync(TOKEN_FILE, 0o600);
122
+ } catch (e) {
123
+ // If we can't save the token, continue anyway - the user will just re-auth next time
124
+ }
125
+ }
126
+
127
+ async function refreshAccessToken(refreshToken, discoveryResp) {
128
+ try {
129
+ const { codeVerifier } = generatePKCE();
130
+ const resp = await httpsPost(discoveryResp.token_endpoint, {
131
+ grant_type: 'refresh_token',
132
+ refresh_token: refreshToken,
133
+ client_id: CLIENT_ID,
134
+ code_verifier: codeVerifier
135
+ });
136
+ if (resp.body.access_token) {
137
+ if (resp.body.refresh_token) {
138
+ saveToken(resp.body.refresh_token);
139
+ }
140
+ return resp.body.access_token;
141
+ }
142
+ return null;
143
+ } catch (e) {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ async function pollForToken(discoveryResp, deviceCode, codeVerifier, interval, expiresIn) {
149
+ const deadline = Date.now() + (expiresIn * 1000);
150
+ let currentInterval = interval;
151
+
152
+ while (Date.now() < deadline) {
153
+ await new Promise(r => setTimeout(r, currentInterval * 1000));
154
+
155
+ try {
156
+ const resp = await httpsPost(discoveryResp.token_endpoint, {
157
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
158
+ device_code: deviceCode,
159
+ client_id: CLIENT_ID,
160
+ code_verifier: codeVerifier
161
+ });
162
+
163
+ const err = resp.body.error;
164
+ if (err === 'authorization_pending') {
165
+ continue;
166
+ } else if (err === 'slow_down') {
167
+ currentInterval += 5;
168
+ continue;
169
+ } else if (err === 'expired_token' || err === 'access_denied') {
170
+ return null;
171
+ } else if (resp.body.access_token) {
172
+ if (resp.body.refresh_token) {
173
+ saveToken(resp.body.refresh_token);
174
+ }
175
+ return resp.body.access_token;
176
+ }
177
+ } catch (e) {
178
+ // Network error, keep polling
179
+ continue;
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+
185
+ async function tryFetchInternalAddon(log) {
186
+ try {
187
+ const discovery = await httpsGet(`${ISSUER}/.well-known/openid-configuration`);
188
+
189
+ if (!discovery.device_authorization_endpoint) {
190
+ log.warn('Internal add-on package not available — device_authorization_endpoint missing.');
191
+ return true;
192
+ }
193
+
194
+ const scopes = discovery.scopes_supported || [];
195
+ if (!scopes.includes('authelia.bearer.authz')) {
196
+ log.warn('Internal add-on package not available — authelia.bearer.authz scope not supported.');
197
+ return true;
198
+ }
199
+
200
+ let accessToken = null;
201
+
202
+ const storedRefresh = loadStoredToken();
203
+ if (storedRefresh) {
204
+ accessToken = await refreshAccessToken(storedRefresh, discovery);
205
+ }
206
+
207
+ if (!accessToken) {
208
+ const { codeVerifier, codeChallenge } = generatePKCE();
209
+
210
+ const deviceResp = await httpsPost(discovery.device_authorization_endpoint, {
211
+ client_id: CLIENT_ID,
212
+ scope: 'openid groups offline_access authelia.bearer.authz',
213
+ code_challenge: codeChallenge,
214
+ code_challenge_method: 'S256'
215
+ });
216
+
217
+ const deviceCode = deviceResp.body.device_code;
218
+ const userCode = deviceResp.body.user_code;
219
+ const verificationUri = deviceResp.body.verification_uri ||
220
+ deviceResp.body.verification_uri_complete;
221
+ const pollInterval = deviceResp.body.interval || 5;
222
+ const expiresIn = deviceResp.body.expires_in || 1800;
223
+
224
+ if (!deviceCode || !userCode || !verificationUri) {
225
+ log.warn('Internal add-on package not available — device flow not available.');
226
+ return true;
227
+ }
228
+
229
+ log.message('');
230
+ log.message(' To authorize the internal add-on package:');
231
+ log.message(` Code: ${userCode}`);
232
+ log.message(` URL: ${verificationUri}`);
233
+ log.message('');
234
+
235
+ tryOpenBrowser(verificationUri);
236
+
237
+ accessToken = await pollForToken(discovery, deviceCode, codeVerifier, pollInterval, expiresIn);
238
+ if (!accessToken) {
239
+ log.warn('Internal add-on package not available — authorization timeout.');
240
+ return true;
241
+ }
242
+ }
243
+
244
+ const tarballData = await httpsGetBinary(TARBALL_URL, {
245
+ 'Authorization': `Bearer ${accessToken}`
246
+ });
247
+
248
+ const tempDir = path.join(CONFIG_DIR, '.tmp');
249
+ fs.mkdirSync(tempDir, { recursive: true });
250
+ const tempTarball = path.join(tempDir, 'aos-internal.tgz');
251
+ fs.writeFileSync(tempTarball, tarballData);
252
+
253
+ try {
254
+ const installDir = path.join(CONFIG_DIR, 'aos-internal');
255
+ fs.mkdirSync(installDir, { recursive: true });
256
+ execSync(`npm install "${tempTarball}"`, {
257
+ cwd: installDir,
258
+ stdio: 'pipe',
259
+ maxBuffer: 16 * 1024 * 1024
260
+ });
261
+ log.step('Internal add-on package installed');
262
+ return true;
263
+ } catch (e) {
264
+ log.warn('Internal add-on package not available — npm install failed.');
265
+ return true;
266
+ } finally {
267
+ try { fs.unlinkSync(tempTarball); } catch (e) {}
268
+ try { fs.rmdirSync(tempDir); } catch (e) {}
269
+ }
270
+ } catch (e) {
271
+ log.warn('Internal add-on package not available — continuing without it.');
272
+ return true;
273
+ }
274
+ }
275
+
276
+ module.exports = { tryFetchInternalAddon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridlabor-api/aos",
3
- "version": "4.4.2-beta.7",
3
+ "version": "4.4.2-beta.8",
4
4
  "description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
5
5
  "main": "installer.js",
6
6
  "engines": {
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: live-preview-canvas
3
+ description: A prototyping skill that generates a local HTML mock with a built-in feedback layer, allowing users to drop visual comments (red dots) for UI fixes.
4
+ category: design-ui-ux
5
+ ---
6
+ # live-preview-canvas
7
+
8
+ **live-preview-canvas** is an AOS (BDB Agent OS) skill designed to bridge the gap between design and implementation feedback. It combines rapid local prototyping with direct visual feedback.
9
+
10
+ ## Workflow
11
+
12
+ 1. **Generate Prototype**: The agent uses the `base_template.html` to generate a local HTML mock of a UI component or page.
13
+ 2. **Review & Comment**: The user opens the HTML file in their browser. They can click anywhere on the UI to leave a visual comment (represented by a red dot) and enter text detailing what needs to be fixed or changed.
14
+ 3. **Export Feedback**: The user clicks the "Export Feedback" button to generate a JSON payload of all comments, including coordinates and text.
15
+ 4. **Agent Action**: The agent reads the exported feedback (either pasted back by the user or saved to a file) and correlates the visual comments with the UI structure to apply precise fixes.
16
+
17
+ ## Usage
18
+
19
+ When a user requests a UI prototype or a visual iteration, copy the contents of `scripts/base_template.html` and inject your HTML/CSS/JS for the prototype into the designated areas. Instruct the user to open the file, click to leave feedback, and share the exported JSON back for the next iteration.
@@ -0,0 +1,406 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Live Preview: Multi-Agent Show Control Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ brand: {
15
+ 400: '#818cf8',
16
+ 500: '#6366f1',
17
+ 600: '#4f46e5'
18
+ }
19
+ }
20
+ }
21
+ }
22
+ };
23
+ </script>
24
+ <style>
25
+ body {
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
27
+ background: #090d16;
28
+ color: #f1f5f9;
29
+ min-height: 100vh;
30
+ display: flex;
31
+ flex-direction: column;
32
+ }
33
+ .custom-scrollbar::-webkit-scrollbar {
34
+ width: 6px;
35
+ height: 6px;
36
+ }
37
+ .custom-scrollbar::-webkit-scrollbar-track {
38
+ background: #0f172a;
39
+ }
40
+ .custom-scrollbar::-webkit-scrollbar-thumb {
41
+ background: #334155;
42
+ border-radius: 3px;
43
+ }
44
+ </style>
45
+ </head>
46
+ <body class="bg-slate-950 text-slate-100 antialiased pb-28">
47
+
48
+ <!-- =========================================================================
49
+ Navigation & Review Bar
50
+ ========================================================================= -->
51
+ <header id="show-control-header" data-component="header" class="border-b border-slate-800 bg-slate-900/90 backdrop-blur sticky top-0 z-40 px-6 py-3 flex items-center justify-between">
52
+ <div class="flex items-center space-x-3">
53
+ <div class="h-9 w-9 rounded-lg bg-gradient-to-tr from-indigo-600 to-violet-500 flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/20">
54
+ ⚡
55
+ </div>
56
+ <div>
57
+ <div class="flex items-center space-x-2">
58
+ <h1 id="app-title" class="text-sm font-semibold text-white tracking-wide">BDB Agent OS &bull; Live Show Controller</h1>
59
+ <span class="text-[10px] uppercase font-mono px-2 py-0.5 rounded bg-indigo-950 text-indigo-300 border border-indigo-800/60">Live Mock</span>
60
+ </div>
61
+ <p id="app-subtitle" class="text-xs text-slate-400">Interactive Preview for Layout Review & Element Red-Pinning</p>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Live Status & In-Memory Trigger -->
66
+ <div id="header-actions" class="flex items-center space-x-3">
67
+ <div id="heartbeat-pill" class="flex items-center space-x-1.5 px-3 py-1 rounded-full text-xs font-mono bg-emerald-950 text-emerald-300 border border-emerald-800">
68
+ <span class="h-2 w-2 rounded-full bg-emerald-400 animate-ping"></span>
69
+ <span>ONLINE &bull; 60 FPS</span>
70
+ </div>
71
+ <button id="btn-emergency-blackout" onclick="triggerPanic()" class="text-xs font-semibold bg-rose-600/20 hover:bg-rose-600 text-rose-300 hover:text-white px-3 py-1.5 rounded-lg border border-rose-700/50 transition">
72
+ Blackout
73
+ </button>
74
+ </div>
75
+ </header>
76
+
77
+ <!-- =========================================================================
78
+ Main Workspace
79
+ ========================================================================= -->
80
+ <main class="flex-1 max-w-6xl w-full mx-auto px-6 py-6 space-y-6">
81
+
82
+ <!-- Reviewer Context Alert -->
83
+ <section id="review-banner" data-component="review-banner" class="p-4 rounded-xl bg-indigo-950/40 border border-indigo-800/50 flex items-center justify-between">
84
+ <div class="flex items-start space-x-3">
85
+ <div class="text-indigo-400 text-lg">💡</div>
86
+ <div>
87
+ <p class="text-xs font-medium text-indigo-200">
88
+ <strong>Designer / Reviewer Notice:</strong> You can click any card, button, or label to pin an annotation. Use the floating bar below to toggle between Layouts and the Panic state.
89
+ </p>
90
+ </div>
91
+ </div>
92
+ <span class="text-xs font-mono text-indigo-300 bg-indigo-900/60 px-2.5 py-1 rounded border border-indigo-700/50">
93
+ Auto-Reloads on Save
94
+ </span>
95
+ </section>
96
+
97
+ <!-- Top KPI Grid -->
98
+ <section id="kpi-grid" data-component="kpi-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
99
+ <div id="kpi-touchdesigner" class="p-4 rounded-xl bg-slate-900 border border-slate-800 hover:border-slate-700 transition">
100
+ <div class="flex justify-between items-start">
101
+ <span class="text-xs text-slate-400 uppercase font-medium">TouchDesigner</span>
102
+ <span class="text-[10px] font-mono text-emerald-400 bg-emerald-950 px-1.5 py-0.5 rounded border border-emerald-800">OK</span>
103
+ </div>
104
+ <div class="mt-2 text-2xl font-bold text-white">4.1ms</div>
105
+ <p class="text-xs text-slate-500 mt-1">GPU Cook Cost (RTX 4090)</p>
106
+ </div>
107
+
108
+ <div id="kpi-grandma" class="p-4 rounded-xl bg-slate-900 border border-slate-800 hover:border-slate-700 transition">
109
+ <div class="flex justify-between items-start">
110
+ <span class="text-xs text-slate-400 uppercase font-medium">grandMA3 Session</span>
111
+ <span class="text-[10px] font-mono text-emerald-400 bg-emerald-950 px-1.5 py-0.5 rounded border border-emerald-800">LOCKED</span>
112
+ </div>
113
+ <div class="mt-2 text-2xl font-bold text-white">Uni 1-8</div>
114
+ <p class="text-xs text-slate-500 mt-1">DMX over sACN / ArtNet</p>
115
+ </div>
116
+
117
+ <div id="kpi-resolume" class="p-4 rounded-xl bg-slate-900 border border-slate-800 hover:border-slate-700 transition">
118
+ <div class="flex justify-between items-start">
119
+ <span class="text-xs text-slate-400 uppercase font-medium">Resolume Arena</span>
120
+ <span class="text-[10px] font-mono text-amber-400 bg-amber-950 px-1.5 py-0.5 rounded border border-amber-800">STANDBY</span>
121
+ </div>
122
+ <div class="mt-2 text-2xl font-bold text-white">Deck 02</div>
123
+ <p class="text-xs text-slate-500 mt-1">Layer 3: Generative Pass</p>
124
+ </div>
125
+
126
+ <div id="kpi-agents" class="p-4 rounded-xl bg-slate-900 border border-slate-800 hover:border-slate-700 transition">
127
+ <div class="flex justify-between items-start">
128
+ <span class="text-xs text-slate-400 uppercase font-medium">Pipeline Cues</span>
129
+ <span class="text-[10px] font-mono text-indigo-400 bg-indigo-950 px-1.5 py-0.5 rounded border border-indigo-800">AUTO</span>
130
+ </div>
131
+ <div class="mt-2 text-2xl font-bold text-white" id="cue-counter">Cue 14/40</div>
132
+ <p class="text-xs text-slate-500 mt-1">Next: Intro Drop Strobes</p>
133
+ </div>
134
+ </section>
135
+
136
+ <!-- VARIANT A: Full Orchestration Board (Default) -->
137
+ <div id="view-variant-a" class="preview-variant space-y-6">
138
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
139
+
140
+ <!-- Left 2 Cols: Active Cue Stack & Audio Spectrum Mock -->
141
+ <div class="lg:col-span-2 space-y-6">
142
+ <section id="cue-stack-section" data-component="cue-stack" class="p-5 rounded-xl bg-slate-900 border border-slate-800">
143
+ <div class="flex items-center justify-between mb-4">
144
+ <h2 class="text-sm font-semibold text-white">Live Cue Sequencer</h2>
145
+ <div class="flex space-x-2">
146
+ <button onclick="advanceCue()" class="text-xs bg-indigo-600 hover:bg-indigo-500 text-white font-medium px-3 py-1 rounded-lg transition shadow-sm">
147
+ ▶ Go Next Cue
148
+ </button>
149
+ <button onclick="resetCues()" class="text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-2.5 py-1 rounded-lg border border-slate-700 transition">
150
+ Restart
151
+ </button>
152
+ </div>
153
+ </div>
154
+
155
+ <div id="cue-list" class="space-y-2 font-mono text-xs">
156
+ <div id="cue-item-12" class="p-3 rounded-lg bg-slate-950/60 border border-slate-800/80 flex items-center justify-between text-slate-400">
157
+ <div class="flex items-center space-x-3">
158
+ <span class="text-slate-500 font-bold">#12</span>
159
+ <span class="text-slate-300">Ambient Drone - Blue Wash</span>
160
+ </div>
161
+ <span class="text-[10px] text-slate-500">COMPLETE</span>
162
+ </div>
163
+
164
+ <div id="cue-item-13" class="p-3 rounded-lg bg-slate-950/60 border border-slate-800/80 flex items-center justify-between text-slate-400">
165
+ <div class="flex items-center space-x-3">
166
+ <span class="text-slate-500 font-bold">#13</span>
167
+ <span class="text-slate-300">Beat Drop Transition - Strobe Pulse</span>
168
+ </div>
169
+ <span class="text-[10px] text-slate-500">COMPLETE</span>
170
+ </div>
171
+
172
+ <div id="cue-item-14" class="p-3 rounded-lg bg-indigo-950/40 border border-indigo-500/50 flex items-center justify-between text-white shadow-sm">
173
+ <div class="flex items-center space-x-3">
174
+ <span class="text-indigo-400 font-bold">#14</span>
175
+ <span class="font-medium text-indigo-100">Main Drop: Audio-Reactive Feedback Tunnel</span>
176
+ </div>
177
+ <span class="text-[10px] bg-indigo-600 text-white px-2 py-0.5 rounded font-sans uppercase tracking-wider font-semibold">RUNNING</span>
178
+ </div>
179
+
180
+ <div id="cue-item-15" class="p-3 rounded-lg bg-slate-950/40 border border-slate-800/50 flex items-center justify-between text-slate-400">
181
+ <div class="flex items-center space-x-3">
182
+ <span class="text-slate-500 font-bold">#15</span>
183
+ <span>Post-Chorus: Particle Dissolve</span>
184
+ </div>
185
+ <span class="text-[10px] text-slate-500">STANDBY</span>
186
+ </div>
187
+ </div>
188
+ </section>
189
+
190
+ <!-- Interactive Mock Control Sliders -->
191
+ <section id="reactive-controls" data-component="reactive-controls" class="p-5 rounded-xl bg-slate-900 border border-slate-800">
192
+ <h3 class="text-sm font-semibold text-white mb-4">Real-Time Parameter Modulation</h3>
193
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
194
+ <div>
195
+ <div class="flex justify-between text-xs text-slate-300 mb-1">
196
+ <span>Feedback Gain</span>
197
+ <span id="label-gain" class="font-mono text-indigo-400">0.82</span>
198
+ </div>
199
+ <input type="range" min="0" max="1" step="0.01" value="0.82" oninput="updateSlider('gain', this.value)" class="w-full accent-indigo-500 cursor-pointer">
200
+ </div>
201
+
202
+ <div>
203
+ <div class="flex justify-between text-xs text-slate-300 mb-1">
204
+ <span>FFT Bass Dampening</span>
205
+ <span id="label-bass" class="font-mono text-indigo-400">0.45</span>
206
+ </div>
207
+ <input type="range" min="0" max="1" step="0.01" value="0.45" oninput="updateSlider('bass', this.value)" class="w-full accent-indigo-500 cursor-pointer">
208
+ </div>
209
+ </div>
210
+ </section>
211
+ </div>
212
+
213
+ <!-- Right 1 Col: Live Event Log -->
214
+ <div class="space-y-6">
215
+ <section id="log-feed-section" data-component="log-feed" class="p-5 rounded-xl bg-slate-900 border border-slate-800 flex flex-col h-[460px]">
216
+ <div class="flex items-center justify-between mb-3">
217
+ <h3 class="text-sm font-semibold text-white">Event Dispatcher Log</h3>
218
+ <button onclick="clearLogs()" class="text-[11px] text-slate-400 hover:text-slate-200">Clear</button>
219
+ </div>
220
+ <div id="log-feed" class="flex-1 overflow-y-auto font-mono text-[11px] space-y-2 custom-scrollbar pr-1">
221
+ <div class="text-slate-500"><span class="text-slate-600">[03:32:01]</span> OSC /cue/14 fired to port 8000</div>
222
+ <div class="text-emerald-400"><span class="text-slate-600">[03:32:02]</span> TD TOP cook ok (4.1ms)</div>
223
+ <div class="text-indigo-300"><span class="text-slate-600">[03:32:04]</span> grandMA3 Macro 21 active</div>
224
+ <div class="text-slate-400"><span class="text-slate-600">[03:32:05]</span> MIDI beat sync: 128.0 BPM</div>
225
+ </div>
226
+ </section>
227
+ </div>
228
+
229
+ </div>
230
+ </div>
231
+
232
+ <!-- VARIANT B: Compact Data Sheet (Table View) -->
233
+ <div id="view-variant-b" class="preview-variant hidden space-y-4">
234
+ <section id="table-section" data-component="compact-table" class="rounded-xl bg-slate-900 border border-slate-800 overflow-hidden">
235
+ <table class="w-full text-left text-xs text-slate-300">
236
+ <thead class="bg-slate-950 text-slate-400 uppercase font-mono text-[10px] border-b border-slate-800">
237
+ <tr>
238
+ <th class="px-4 py-3">Fixture / Node</th>
239
+ <th class="px-4 py-3">Protocol</th>
240
+ <th class="px-4 py-3">Address</th>
241
+ <th class="px-4 py-3">FPS / Rate</th>
242
+ <th class="px-4 py-3 text-right">Status</th>
243
+ </tr>
244
+ </thead>
245
+ <tbody class="divide-y divide-slate-800 font-mono">
246
+ <tr class="hover:bg-slate-800/40">
247
+ <td class="px-4 py-3 font-semibold text-white font-sans">Beam 350 Array</td>
248
+ <td class="px-4 py-3">sACN E1.31</td>
249
+ <td class="px-4 py-3">10.0.1.50:5568</td>
250
+ <td class="px-4 py-3">44 Hz</td>
251
+ <td class="px-4 py-3 text-right text-emerald-400">SYNCED</td>
252
+ </tr>
253
+ <tr class="hover:bg-slate-800/40">
254
+ <td class="px-4 py-3 font-semibold text-white font-sans">Laser Pangolin FB4</td>
255
+ <td class="px-4 py-3">ILDA / IP</td>
256
+ <td class="px-4 py-3">10.0.2.10:1200</td>
257
+ <td class="px-4 py-3">30 kpps</td>
258
+ <td class="px-4 py-3 text-right text-emerald-400">ARMED</td>
259
+ </tr>
260
+ <tr class="hover:bg-slate-800/40">
261
+ <td class="px-4 py-3 font-semibold text-white font-sans">Resolume Arena Deck</td>
262
+ <td class="px-4 py-3">OSC + Syphon</td>
263
+ <td class="px-4 py-3">127.0.0.1:7000</td>
264
+ <td class="px-4 py-3">60 FPS</td>
265
+ <td class="px-4 py-3 text-right text-indigo-400">ACTIVE</td>
266
+ </tr>
267
+ </tbody>
268
+ </table>
269
+ </section>
270
+ </div>
271
+
272
+ <!-- VARIANT C: Panic / Blackout / Disconnected View -->
273
+ <div id="view-variant-c" class="preview-variant hidden">
274
+ <section id="panic-section" data-component="panic-view" class="p-8 rounded-xl bg-slate-900 border-2 border-rose-600/60 text-center space-y-4">
275
+ <div class="inline-flex h-14 w-14 rounded-full bg-rose-950 text-rose-400 items-center justify-center text-2xl font-bold border border-rose-800">
276
+ 🛑
277
+ </div>
278
+ <h2 class="text-lg font-bold text-white">Emergency Blackout Triggered</h2>
279
+ <p class="text-xs text-slate-400 max-w-md mx-auto">
280
+ All sACN universes clamped to 0. TouchDesigner master OP bypassed. Resolume output blackout layer 1 active.
281
+ </p>
282
+ <div class="pt-2 flex justify-center space-x-3">
283
+ <button onclick="switchVariant('A')" class="bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold px-4 py-2 rounded-lg transition shadow-md shadow-indigo-600/20">
284
+ Resume Normal Flow (Variant A)
285
+ </button>
286
+ </div>
287
+ </section>
288
+ </div>
289
+
290
+ </main>
291
+
292
+ <!-- =========================================================================
293
+ Floating Variant Switcher Bar
294
+ ========================================================================= -->
295
+ <footer id="prototype-switcher-bar" data-component="switcher-bar" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-slate-900/90 backdrop-blur-md border border-slate-700/80 shadow-2xl rounded-full px-3 py-2 flex items-center space-x-2">
296
+ <button onclick="cycleVariant(-1)" aria-label="Previous Variant" class="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded-full transition">
297
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
298
+ </button>
299
+
300
+ <div class="flex items-center space-x-1">
301
+ <button onclick="switchVariant('A')" id="tab-btn-A" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition bg-indigo-600 text-white shadow-sm">
302
+ Variant A (Control Board)
303
+ </button>
304
+ <button onclick="switchVariant('B')" id="tab-btn-B" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition text-slate-400 hover:text-slate-200">
305
+ Variant B (Table Matrix)
306
+ </button>
307
+ <button onclick="switchVariant('C')" id="tab-btn-C" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition text-slate-400 hover:text-slate-200">
308
+ Variant C (Panic State)
309
+ </button>
310
+ </div>
311
+
312
+ <button onclick="cycleVariant(1)" aria-label="Next Variant" class="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded-full transition">
313
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
314
+ </button>
315
+ </footer>
316
+
317
+ <!-- =========================================================================
318
+ Interactive State Script
319
+ ========================================================================= -->
320
+ <script>
321
+ const VARIANTS = ['A', 'B', 'C'];
322
+ let currentVariantIndex = 0;
323
+ let cueNum = 14;
324
+
325
+ function getVariantFromUrl() {
326
+ const params = new URLSearchParams(window.location.search);
327
+ const v = params.get('variant');
328
+ return (v && VARIANTS.includes(v.toUpperCase())) ? v.toUpperCase() : 'A';
329
+ }
330
+
331
+ function switchVariant(variantKey) {
332
+ const normalized = variantKey.toUpperCase();
333
+ currentVariantIndex = VARIANTS.indexOf(normalized);
334
+ if (currentVariantIndex === -1) currentVariantIndex = 0;
335
+
336
+ document.querySelectorAll('.preview-variant').forEach(el => el.classList.add('hidden'));
337
+ const activeEl = document.getElementById(`view-variant-${normalized.toLowerCase()}`);
338
+ if (activeEl) activeEl.classList.remove('hidden');
339
+
340
+ document.querySelectorAll('.switcher-pill').forEach(pill => {
341
+ pill.classList.remove('bg-indigo-600', 'text-white', 'shadow-sm');
342
+ pill.classList.add('text-slate-400');
343
+ });
344
+ const activePill = document.getElementById(`tab-btn-${normalized}`);
345
+ if (activePill) {
346
+ activePill.classList.add('bg-indigo-600', 'text-white', 'shadow-sm');
347
+ activePill.classList.remove('text-slate-400');
348
+ }
349
+
350
+ const url = new URL(window.location);
351
+ url.searchParams.set('variant', normalized);
352
+ window.history.replaceState({}, '', url);
353
+ }
354
+
355
+ function cycleVariant(delta) {
356
+ currentVariantIndex = (currentVariantIndex + delta + VARIANTS.length) % VARIANTS.length;
357
+ switchVariant(VARIANTS[currentVariantIndex]);
358
+ }
359
+
360
+ function advanceCue() {
361
+ cueNum++;
362
+ document.getElementById('cue-counter').textContent = `Cue ${cueNum}/40`;
363
+ addLog(`Dispatched Cue #${cueNum} over OSC`);
364
+ }
365
+
366
+ function resetCues() {
367
+ cueNum = 14;
368
+ document.getElementById('cue-counter').textContent = `Cue ${cueNum}/40`;
369
+ addLog(`Reset cue counter to Cue #14`);
370
+ }
371
+
372
+ function updateSlider(key, val) {
373
+ document.getElementById(`label-${key}`).textContent = val;
374
+ }
375
+
376
+ function addLog(msg) {
377
+ const feed = document.getElementById('log-feed');
378
+ const time = new Date().toLocaleTimeString();
379
+ const div = document.createElement('div');
380
+ div.className = 'text-indigo-300';
381
+ div.innerHTML = `<span class="text-slate-600">[${time}]</span> ${msg}`;
382
+ feed.prepend(div);
383
+ }
384
+
385
+ function clearLogs() {
386
+ document.getElementById('log-feed').innerHTML = '<div class="text-slate-500 font-mono text-[11px]">Logs cleared.</div>';
387
+ }
388
+
389
+ function triggerPanic() {
390
+ switchVariant('C');
391
+ addLog('⚠️ BLACKOUT TRIGGERED');
392
+ }
393
+
394
+ window.addEventListener('keydown', (e) => {
395
+ const tag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
396
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
397
+ if (e.key === 'ArrowRight') cycleVariant(1);
398
+ if (e.key === 'ArrowLeft') cycleVariant(-1);
399
+ });
400
+
401
+ window.addEventListener('DOMContentLoaded', () => {
402
+ switchVariant(getVariantFromUrl());
403
+ });
404
+ </script>
405
+ </body>
406
+ </html>
@@ -0,0 +1,291 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Live Preview Canvas — Interactive Mockup Template</title>
7
+ <!-- Tailwind CSS via CDN for rapid, consistent styling -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ brand: {
16
+ 50: '#f5f7ff',
17
+ 500: '#4f46e5',
18
+ 600: '#4338ca',
19
+ 700: '#3730a3'
20
+ }
21
+ }
22
+ }
23
+ }
24
+ };
25
+ </script>
26
+ <style>
27
+ /* Baseline styles & reset */
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
30
+ margin: 0;
31
+ padding: 0;
32
+ background-color: #0b0f19;
33
+ color: #e2e8f0;
34
+ min-height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ }
38
+ /* Stable highlighting for Plan Canvas element selection */
39
+ [data-preview-anchor]:hover {
40
+ outline: 2px dashed rgba(99, 102, 241, 0.4);
41
+ outline-offset: 2px;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body class="bg-slate-950 text-slate-100 antialiased selection:bg-indigo-500 selection:text-white pb-24">
46
+
47
+ <!-- =========================================================================
48
+ 1. Top Header Bar
49
+ Semantic IDs and data-component attributes enable crystal-clear annotations
50
+ ========================================================================= -->
51
+ <header id="preview-header" data-component="header" class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-40 px-6 py-4 flex items-center justify-between">
52
+ <div class="flex items-center space-x-3">
53
+ <div class="h-8 w-8 rounded-lg bg-indigo-600 flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/30">
54
+ P
55
+ </div>
56
+ <div>
57
+ <h1 class="text-base font-semibold text-white leading-tight">Feature Preview: <span id="feature-title" class="text-indigo-400">Component Mockup</span></h1>
58
+ <p class="text-xs text-slate-400">Interactive live prototype for visual review & red-pinning</p>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- State Indicator / Action pill -->
63
+ <div class="flex items-center space-x-3">
64
+ <span id="active-state-badge" class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-emerald-950 text-emerald-300 border border-emerald-800">
65
+ <span class="w-1.5 h-1.5 mr-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
66
+ Interactive (In-Memory)
67
+ </span>
68
+ <button id="btn-reset-mock" onclick="resetMockState()" class="text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-3 py-1.5 rounded border border-slate-700 transition">
69
+ Reset State
70
+ </button>
71
+ </div>
72
+ </header>
73
+
74
+ <!-- =========================================================================
75
+ 2. Main Canvas Content Area
76
+ ========================================================================= -->
77
+ <main class="flex-1 max-w-5xl w-full mx-auto px-6 py-8">
78
+
79
+ <!-- Information banner for reviewers -->
80
+ <section id="review-banner" data-component="instruction-banner" class="mb-6 p-4 rounded-xl bg-slate-900 border border-slate-800 flex items-start justify-between">
81
+ <div>
82
+ <h2 class="text-sm font-semibold text-slate-200">Visual Audit Mode Active</h2>
83
+ <p class="text-xs text-slate-400 mt-1 leading-relaxed">
84
+ Click any element to add a pinned comment, or use the bottom switcher to evaluate different layout variants and error states.
85
+ </p>
86
+ </div>
87
+ <div class="text-right text-xs text-slate-500 font-mono">
88
+ Hotkeys: <kbd class="px-1.5 py-0.5 bg-slate-800 rounded border border-slate-700 text-slate-300">←</kbd> <kbd class="px-1.5 py-0.5 bg-slate-800 rounded border border-slate-700 text-slate-300">→</kbd> to cycle
89
+ </div>
90
+ </section>
91
+
92
+ <!-- VARIANT A: Standard View -->
93
+ <div id="view-variant-a" class="preview-variant space-y-6">
94
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
95
+ <!-- Card 1 -->
96
+ <article id="card-stats-1" data-component="stat-card" class="p-5 rounded-xl bg-slate-900 border border-slate-800 shadow-sm hover:border-slate-700 transition">
97
+ <div class="text-xs font-medium text-slate-400 uppercase tracking-wider">Metric Alpha</div>
98
+ <div class="text-2xl font-bold text-white mt-2">1,482</div>
99
+ <div class="mt-2 text-xs text-emerald-400 flex items-center font-medium">
100
+ <span>↑ +12.4%</span>
101
+ <span class="text-slate-500 ml-1.5">vs last week</span>
102
+ </div>
103
+ </article>
104
+
105
+ <!-- Card 2 -->
106
+ <article id="card-stats-2" data-component="stat-card" class="p-5 rounded-xl bg-slate-900 border border-slate-800 shadow-sm hover:border-slate-700 transition">
107
+ <div class="text-xs font-medium text-slate-400 uppercase tracking-wider">Latency / Cook Cost</div>
108
+ <div class="text-2xl font-bold text-white mt-2">4.2 ms</div>
109
+ <div class="mt-2 text-xs text-indigo-400 flex items-center font-medium">
110
+ <span>Optimal 60 FPS</span>
111
+ </div>
112
+ </article>
113
+
114
+ <!-- Card 3 -->
115
+ <article id="card-stats-3" data-component="stat-card" class="p-5 rounded-xl bg-slate-900 border border-slate-800 shadow-sm hover:border-slate-700 transition">
116
+ <div class="text-xs font-medium text-slate-400 uppercase tracking-wider">Active Handlers</div>
117
+ <div class="text-2xl font-bold text-white mt-2" id="counter-value">9</div>
118
+ <div class="mt-2 flex space-x-2">
119
+ <button onclick="adjustCounter(1)" class="text-xs bg-indigo-600/30 hover:bg-indigo-600 text-indigo-200 px-2.5 py-1 rounded transition">+ Increment</button>
120
+ <button onclick="adjustCounter(-1)" class="text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-2.5 py-1 rounded transition">- Decrement</button>
121
+ </div>
122
+ </article>
123
+ </div>
124
+
125
+ <!-- Action Panel / Primary Form -->
126
+ <section id="main-panel" data-component="action-panel" class="p-6 rounded-xl bg-slate-900 border border-slate-800">
127
+ <h3 class="text-base font-semibold text-white mb-4">Interactive Controls & Configuration</h3>
128
+ <div class="space-y-4">
129
+ <div>
130
+ <label for="input-channel" class="block text-xs font-medium text-slate-300 mb-1.5">Channel Name</label>
131
+ <input type="text" id="input-channel" value="live-preview/stream-01" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition">
132
+ </div>
133
+ <div class="flex items-center space-x-4">
134
+ <button id="btn-primary-action" onclick="simulateAction()" class="bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium px-4 py-2 rounded-lg shadow-md shadow-indigo-600/20 transition flex items-center space-x-2">
135
+ <span>Execute Action</span>
136
+ </button>
137
+ <span id="action-status" class="text-xs text-slate-400">Ready</span>
138
+ </div>
139
+ </div>
140
+ </section>
141
+ </div>
142
+
143
+ <!-- VARIANT B: Alternative Layout (Dense List) -->
144
+ <div id="view-variant-b" class="preview-variant hidden space-y-4">
145
+ <div id="dense-list-container" data-component="dense-list" class="rounded-xl bg-slate-900 border border-slate-800 divide-y divide-slate-800">
146
+ <div class="p-4 flex items-center justify-between">
147
+ <div>
148
+ <span class="text-sm font-medium text-white">Item #1: Audio Reactive Engine</span>
149
+ <p class="text-xs text-slate-400">FFT Peak extraction on CHOP chain 4</p>
150
+ </div>
151
+ <span class="text-xs bg-emerald-950 text-emerald-400 px-2 py-0.5 rounded border border-emerald-800">Active</span>
152
+ </div>
153
+ <div class="p-4 flex items-center justify-between">
154
+ <div>
155
+ <span class="text-sm font-medium text-white">Item #2: DMX Fixture Router</span>
156
+ <p class="text-xs text-slate-400">Universe 1-4 mapped to ArtNet node</p>
157
+ </div>
158
+ <span class="text-xs bg-slate-800 text-slate-400 px-2 py-0.5 rounded">Standby</span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- VARIANT C: Error / Edge-Case State -->
164
+ <div id="view-variant-c" class="preview-variant hidden">
165
+ <div id="error-boundary-mock" data-component="error-state" class="text-center py-12 px-6 rounded-xl bg-slate-900 border border-rose-900/40">
166
+ <div class="inline-flex h-12 w-12 rounded-full bg-rose-950/80 text-rose-400 items-center justify-center mb-3 border border-rose-800/60">
167
+ ⚠️
168
+ </div>
169
+ <h3 class="text-base font-semibold text-white">Connection Stream Interrupted</h3>
170
+ <p class="text-xs text-slate-400 max-w-sm mx-auto mt-1.5 mb-6">
171
+ The upstream WebRTC signal timed out after 3000ms. Check loopback connection or proxy gateway.
172
+ </p>
173
+ <button onclick="switchVariant('A')" class="bg-rose-600 hover:bg-rose-500 text-white text-xs font-semibold px-4 py-2 rounded-lg transition shadow-md shadow-rose-600/20">
174
+ Retry & Return to Variant A
175
+ </button>
176
+ </div>
177
+ </div>
178
+
179
+ </main>
180
+
181
+ <!-- =========================================================================
182
+ 3. Floating Prototype Switcher Bar (Prototype Skill UI.md convention)
183
+ Allows immediate flipping between variants and states
184
+ ========================================================================= -->
185
+ <footer id="prototype-switcher-bar" data-component="switcher-bar" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-slate-900/90 backdrop-blur-md border border-slate-700/80 shadow-2xl rounded-full px-3 py-2 flex items-center space-x-2">
186
+ <button onclick="cycleVariant(-1)" aria-label="Previous Variant" class="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded-full transition">
187
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
188
+ </button>
189
+
190
+ <div class="flex items-center space-x-1">
191
+ <button onclick="switchVariant('A')" id="tab-btn-A" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition bg-indigo-600 text-white shadow-sm">
192
+ Variant A (Grid)
193
+ </button>
194
+ <button onclick="switchVariant('B')" id="tab-btn-B" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition text-slate-400 hover:text-slate-200">
195
+ Variant B (List)
196
+ </button>
197
+ <button onclick="switchVariant('C')" id="tab-btn-C" class="switcher-pill px-3 py-1 rounded-full text-xs font-medium transition text-slate-400 hover:text-slate-200">
198
+ Variant C (Error)
199
+ </button>
200
+ </div>
201
+
202
+ <button onclick="cycleVariant(1)" aria-label="Next Variant" class="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded-full transition">
203
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
204
+ </button>
205
+ </footer>
206
+
207
+ <!-- =========================================================================
208
+ 4. In-Memory Mock Interaction Logic
209
+ ========================================================================= -->
210
+ <script>
211
+ const VARIANTS = ['A', 'B', 'C'];
212
+ let currentVariantIndex = 0;
213
+ let counter = 9;
214
+
215
+ function getVariantFromUrl() {
216
+ const params = new URLSearchParams(window.location.search);
217
+ const v = params.get('variant');
218
+ return (v && VARIANTS.includes(v.toUpperCase())) ? v.toUpperCase() : 'A';
219
+ }
220
+
221
+ function switchVariant(variantKey) {
222
+ const normalized = variantKey.toUpperCase();
223
+ currentVariantIndex = VARIANTS.indexOf(normalized);
224
+ if (currentVariantIndex === -1) currentVariantIndex = 0;
225
+
226
+ // Update visibility
227
+ document.querySelectorAll('.preview-variant').forEach(el => el.classList.add('hidden'));
228
+ const activeEl = document.getElementById(`view-variant-${normalized.toLowerCase()}`);
229
+ if (activeEl) activeEl.classList.remove('hidden');
230
+
231
+ // Update pills
232
+ document.querySelectorAll('.switcher-pill').forEach(pill => {
233
+ pill.classList.remove('bg-indigo-600', 'text-white', 'shadow-sm');
234
+ pill.classList.add('text-slate-400');
235
+ });
236
+ const activePill = document.getElementById(`tab-btn-${normalized}`);
237
+ if (activePill) {
238
+ activePill.classList.add('bg-indigo-600', 'text-white', 'shadow-sm');
239
+ activePill.classList.remove('text-slate-400');
240
+ }
241
+
242
+ // Sync URL search param cleanly without reloading
243
+ const url = new URL(window.location);
244
+ url.searchParams.set('variant', normalized);
245
+ window.history.replaceState({}, '', url);
246
+ }
247
+
248
+ function cycleVariant(delta) {
249
+ currentVariantIndex = (currentVariantIndex + delta + VARIANTS.length) % VARIANTS.length;
250
+ switchVariant(VARIANTS[currentVariantIndex]);
251
+ }
252
+
253
+ function adjustCounter(delta) {
254
+ counter += delta;
255
+ document.getElementById('counter-value').textContent = counter;
256
+ }
257
+
258
+ function simulateAction() {
259
+ const statusEl = document.getElementById('action-status');
260
+ statusEl.textContent = 'Processing in memory...';
261
+ statusEl.className = 'text-xs text-amber-400 animate-pulse';
262
+ setTimeout(() => {
263
+ statusEl.textContent = 'Action executed successfully!';
264
+ statusEl.className = 'text-xs text-emerald-400 font-medium';
265
+ }, 400);
266
+ }
267
+
268
+ function resetMockState() {
269
+ counter = 9;
270
+ document.getElementById('counter-value').textContent = counter;
271
+ const statusEl = document.getElementById('action-status');
272
+ statusEl.textContent = 'Ready';
273
+ statusEl.className = 'text-xs text-slate-400';
274
+ switchVariant('A');
275
+ }
276
+
277
+ // Keyboard navigation (Arrow keys cycle variants when not typing in inputs)
278
+ window.addEventListener('keydown', (e) => {
279
+ const tag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
280
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
281
+ if (e.key === 'ArrowRight') cycleVariant(1);
282
+ if (e.key === 'ArrowLeft') cycleVariant(-1);
283
+ });
284
+
285
+ // Initialize variant on page load
286
+ window.addEventListener('DOMContentLoaded', () => {
287
+ switchVariant(getVariantFromUrl());
288
+ });
289
+ </script>
290
+ </body>
291
+ </html>
@@ -0,0 +1,178 @@
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>Live Preview Canvas</title>
7
+ <style>
8
+ /* Inject Prototype CSS Here */
9
+ body {
10
+ font-family: system-ui, -apple-system, sans-serif;
11
+ margin: 0;
12
+ padding: 0;
13
+ background-color: #f9fafb;
14
+ }
15
+
16
+ .prototype-container {
17
+ /* Put your prototype content here */
18
+ min-height: 100vh;
19
+ padding: 2rem;
20
+ }
21
+
22
+ /* --- Feedback Layer Styles --- */
23
+ #feedback-layer {
24
+ position: fixed;
25
+ top: 0;
26
+ left: 0;
27
+ width: 100vw;
28
+ height: 100vh;
29
+ pointer-events: none;
30
+ z-index: 9998;
31
+ }
32
+
33
+ .feedback-dot {
34
+ position: absolute;
35
+ width: 16px;
36
+ height: 16px;
37
+ background-color: #ef4444;
38
+ border: 2px solid white;
39
+ border-radius: 50%;
40
+ transform: translate(-50%, -50%);
41
+ cursor: pointer;
42
+ pointer-events: auto;
43
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
44
+ }
45
+
46
+ .feedback-dot:hover::after {
47
+ content: attr(data-comment);
48
+ position: absolute;
49
+ top: 100%;
50
+ left: 50%;
51
+ transform: translateX(-50%);
52
+ background: white;
53
+ padding: 8px;
54
+ border-radius: 4px;
55
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
56
+ white-space: nowrap;
57
+ color: black;
58
+ font-size: 12px;
59
+ z-index: 9999;
60
+ margin-top: 4px;
61
+ }
62
+
63
+ #feedback-controls {
64
+ position: fixed;
65
+ bottom: 20px;
66
+ right: 20px;
67
+ background: white;
68
+ padding: 16px;
69
+ border-radius: 8px;
70
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
71
+ z-index: 9999;
72
+ pointer-events: auto;
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 8px;
76
+ border: 1px solid #e5e7eb;
77
+ }
78
+
79
+ #feedback-controls button {
80
+ padding: 8px 16px;
81
+ background: #3b82f6;
82
+ color: white;
83
+ border: none;
84
+ border-radius: 4px;
85
+ cursor: pointer;
86
+ font-weight: 500;
87
+ }
88
+
89
+ #feedback-controls button:hover {
90
+ background: #2563eb;
91
+ }
92
+
93
+ #feedback-output {
94
+ display: none;
95
+ width: 300px;
96
+ height: 150px;
97
+ font-family: monospace;
98
+ font-size: 12px;
99
+ resize: none;
100
+ padding: 8px;
101
+ border: 1px solid #d1d5db;
102
+ border-radius: 4px;
103
+ }
104
+ </style>
105
+ </head>
106
+ <body>
107
+
108
+ <!-- Inject Prototype HTML Here -->
109
+ <div class="prototype-container">
110
+ <h1>Prototype Area</h1>
111
+ <p>Click anywhere to leave a comment.</p>
112
+ </div>
113
+
114
+ <!-- Feedback Layer -->
115
+ <div id="feedback-layer"></div>
116
+ <div id="feedback-controls">
117
+ <button id="export-btn">Export Feedback</button>
118
+ <textarea id="feedback-output" readonly placeholder="Exported JSON will appear here..."></textarea>
119
+ </div>
120
+
121
+ <script>
122
+ const comments = [];
123
+ const layer = document.getElementById('feedback-layer');
124
+ const exportBtn = document.getElementById('export-btn');
125
+ const output = document.getElementById('feedback-output');
126
+ const controls = document.getElementById('feedback-controls');
127
+
128
+ // Handle clicks on the document to add feedback
129
+ document.addEventListener('click', (e) => {
130
+ // Ignore clicks inside the feedback controls or on existing dots
131
+ if (controls.contains(e.target) || e.target.classList.contains('feedback-dot')) {
132
+ return;
133
+ }
134
+
135
+ const text = prompt('Enter your feedback for this element:');
136
+ if (!text) return;
137
+
138
+ const x = e.clientX;
139
+ const y = e.clientY;
140
+
141
+ // Save to state
142
+ const comment = { id: Date.now(), x, y, text };
143
+ comments.push(comment);
144
+
145
+ // Draw dot
146
+ const dot = document.createElement('div');
147
+ dot.className = 'feedback-dot';
148
+ dot.style.left = `${x}px`;
149
+ dot.style.top = `${y}px`;
150
+ dot.setAttribute('data-comment', text);
151
+
152
+ // Allow deleting comment on right click
153
+ dot.addEventListener('contextmenu', (evt) => {
154
+ evt.preventDefault();
155
+ if(confirm('Delete this comment?')) {
156
+ const idx = comments.findIndex(c => c.id === comment.id);
157
+ if(idx > -1) comments.splice(idx, 1);
158
+ dot.remove();
159
+ }
160
+ });
161
+
162
+ layer.appendChild(dot);
163
+ });
164
+
165
+ // Export functionality
166
+ exportBtn.addEventListener('click', () => {
167
+ output.style.display = 'block';
168
+ output.value = JSON.stringify(comments, null, 2);
169
+ output.select();
170
+ document.execCommand('copy');
171
+ exportBtn.textContent = 'Copied to Clipboard!';
172
+ setTimeout(() => {
173
+ exportBtn.textContent = 'Export Feedback';
174
+ }, 2000);
175
+ });
176
+ </script>
177
+ </body>
178
+ </html>