infaira-canvas 0.1.9

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.
@@ -0,0 +1,1670 @@
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>ICan Dev Harness — InfAIra Canvas</title>
7
+ <link rel="manifest" href="./site.webmanifest" />
8
+ <link rel="icon" type="image/png" href="./resources/infaira-icon.png" />
9
+ <meta name="theme-color" content="#7c6aff" />
10
+
11
+ <!-- Fonts -->
12
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
+ <link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
15
+
16
+ <!-- React 16 -->
17
+ <script src="https://unpkg.com/react@16.14.0/umd/react.production.min.js"></script>
18
+ <script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.production.min.js"></script>
19
+
20
+ <!-- ICan Components -->
21
+ <script src="./resources/ican-components.js"></script>
22
+
23
+ <style>
24
+ /* ═══════════════════════════════════════════════════════
25
+ ICan Dev Harness — Theme Variables
26
+ Dark (default) | Light | Glass Dark | Glass Light
27
+ ═══════════════════════════════════════════════════════ */
28
+
29
+ /* ── Dark Theme (default) ─────────────────────────────── */
30
+ :root {
31
+ --ican-bg: #09090b;
32
+ --ican-header-bg: #0f0f12;
33
+ --ican-primary-bg: #141417;
34
+ --ican-secondary-bg: #1a1a1f;
35
+ --ican-primary-text: #fafafa;
36
+ --ican-secondary-text: #a1a1aa;
37
+ --ican-tertiary-text: #52525b;
38
+ --ican-border: rgba(255, 255, 255, 0.06);
39
+ --ican-border-bright: rgba(255, 255, 255, 0.12);
40
+ --ican-border-focus: #7c6aff;
41
+ --ican-accent: #7c6aff;
42
+ --ican-accent-hover: #6b59e8;
43
+ --ican-accent-text: #a99aff;
44
+ --ican-accent-dim: rgba(124, 106, 255, 0.08);
45
+ --ican-error: #f87171;
46
+ --ican-success: #4ade80;
47
+ --ican-warning: #fbbf24;
48
+ --ican-info: #60a5fa;
49
+ --ican-success-dim: rgba(74, 222, 128, 0.08);
50
+ --ican-error-dim: rgba(248, 113, 113, 0.08);
51
+ --ican-warning-dim: rgba(251, 191, 36, 0.08);
52
+ --ican-card-bg: #141417;
53
+ --ican-card-shadow: 0 1px 2px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.03);
54
+ --ican-card-hover-shadow: 0 8px 30px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.06);
55
+ --ican-input-bg: #1a1a1f;
56
+ --ican-hover: rgba(255, 255, 255, 0.05);
57
+ --ican-modal-bg: #111114;
58
+ --ican-modal-shadow: 0 25px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05);
59
+ --ican-btn-primary-bg: #7c6aff;
60
+ --ican-btn-primary-text: #ffffff;
61
+ --ican-btn-secondary-bg: #1a1a1f;
62
+ --ican-btn-secondary-text: #fafafa;
63
+ --ican-scrollbar-thumb: rgba(255, 255, 255, 0.10);
64
+ --ican-backdrop-filter: none;
65
+ --ican-glass-border: rgba(255, 255, 255, 0.10);
66
+ --ican-font-brand: 'Comfortaa', cursive;
67
+ --ican-font-body: 'Inter', system-ui, -apple-system, sans-serif;
68
+ --ican-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
69
+ --ican-radius-xs: 4px;
70
+ --ican-radius-sm: 6px;
71
+ --ican-radius-md: 10px;
72
+ --ican-radius-lg: 16px;
73
+ --ican-transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1);
74
+ --ican-transition-normal: 220ms cubic-bezier(0.4, 0, 0.2, 1);
75
+ }
76
+
77
+ /* ── Light Theme ──────────────────────────────────────── */
78
+ [data-theme='light'] {
79
+ --ican-bg: #f5f5f7;
80
+ --ican-header-bg: rgba(255, 255, 255, 0.85);
81
+ --ican-primary-bg: #ffffff;
82
+ --ican-secondary-bg: #f0f0f4;
83
+ --ican-primary-text: #1d1d1f;
84
+ --ican-secondary-text: #6e6e80;
85
+ --ican-tertiary-text: #aeaeb8;
86
+ --ican-border: rgba(0, 0, 0, 0.06);
87
+ --ican-border-bright: rgba(0, 0, 0, 0.11);
88
+ --ican-border-focus: #5b4cd9;
89
+ --ican-accent: #5b4cd9;
90
+ --ican-accent-hover: #4a3cb5;
91
+ --ican-accent-text: #4a3cb5;
92
+ --ican-accent-dim: rgba(91, 76, 217, 0.07);
93
+ --ican-error: #dc2626;
94
+ --ican-success: #16a34a;
95
+ --ican-warning: #d97706;
96
+ --ican-info: #2563eb;
97
+ --ican-success-dim: rgba(22, 163, 74, 0.07);
98
+ --ican-error-dim: rgba(220, 38, 38, 0.07);
99
+ --ican-warning-dim: rgba(217, 119, 6, 0.07);
100
+ --ican-card-bg: #ffffff;
101
+ --ican-card-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.03);
102
+ --ican-card-hover-shadow: 0 10px 30px rgba(0,0,0,0.07), 0 0 0 1px rgba(0,0,0,0.04);
103
+ --ican-input-bg: #f5f5f7;
104
+ --ican-hover: rgba(0, 0, 0, 0.035);
105
+ --ican-modal-bg: #ffffff;
106
+ --ican-modal-shadow: 0 25px 65px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);
107
+ --ican-btn-primary-bg: #5b4cd9;
108
+ --ican-btn-primary-text: #ffffff;
109
+ --ican-btn-secondary-bg: #f0f0f5;
110
+ --ican-btn-secondary-text: #1d1d1f;
111
+ --ican-scrollbar-thumb: rgba(0, 0, 0, 0.14);
112
+ --ican-backdrop-filter: blur(20px) saturate(180%);
113
+ --ican-glass-border: rgba(0, 0, 0, 0.06);
114
+ }
115
+
116
+ /* ── Glass Dark Theme ─────────────────────────────────── */
117
+ [data-theme='glass-dark'] {
118
+ --ican-bg: transparent;
119
+ --ican-header-bg: rgba(255, 255, 255, 0.04);
120
+ --ican-primary-bg: rgba(255, 255, 255, 0.06);
121
+ --ican-secondary-bg: rgba(255, 255, 255, 0.05);
122
+ --ican-primary-text: rgba(255, 255, 255, 0.95);
123
+ --ican-secondary-text: rgba(220, 220, 245, 0.65);
124
+ --ican-tertiary-text: rgba(180, 180, 220, 0.40);
125
+ --ican-border: rgba(255, 255, 255, 0.13);
126
+ --ican-border-bright: rgba(255, 255, 255, 0.22);
127
+ --ican-border-focus: rgba(200, 190, 255, 0.90);
128
+ --ican-accent: #b4a8ff;
129
+ --ican-accent-hover: #a099ff;
130
+ --ican-accent-text: rgba(200, 190, 255, 0.95);
131
+ --ican-accent-dim: rgba(180, 168, 255, 0.14);
132
+ --ican-error: #fca5a5;
133
+ --ican-success: #86efac;
134
+ --ican-warning: #fcd34d;
135
+ --ican-info: #7dd3fc;
136
+ --ican-success-dim: rgba(134, 239, 172, 0.12);
137
+ --ican-error-dim: rgba(252, 165, 165, 0.12);
138
+ --ican-warning-dim: rgba(252, 211, 77, 0.12);
139
+ --ican-card-bg: rgba(255, 255, 255, 0.08);
140
+ --ican-card-shadow: 0 8px 32px rgba(0,0,0,0.30), 0 0 0 1px rgba(255,255,255,0.12), inset 0 1px 0 rgba(255,255,255,0.18);
141
+ --ican-card-hover-shadow: 0 16px 56px rgba(0,0,0,0.40), 0 0 0 1px rgba(255,255,255,0.18), inset 0 1px 0 rgba(255,255,255,0.22);
142
+ --ican-input-bg: rgba(255, 255, 255, 0.08);
143
+ --ican-hover: rgba(255, 255, 255, 0.08);
144
+ --ican-modal-bg: rgba(15, 10, 40, 0.72);
145
+ --ican-modal-shadow: 0 32px 100px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.12), inset 0 1px 0 rgba(255,255,255,0.16);
146
+ --ican-btn-primary-bg: rgba(140, 120, 255, 0.75);
147
+ --ican-btn-primary-text: #ffffff;
148
+ --ican-btn-secondary-bg: rgba(255, 255, 255, 0.10);
149
+ --ican-btn-secondary-text: rgba(255, 255, 255, 0.90);
150
+ --ican-scrollbar-thumb: rgba(255, 255, 255, 0.18);
151
+ --ican-backdrop-filter: blur(24px) saturate(200%) brightness(1.12);
152
+ --ican-glass-border: rgba(255, 255, 255, 0.18);
153
+ }
154
+
155
+ /* ── Glass Light Theme ────────────────────────────────── */
156
+ [data-theme='glass-light'] {
157
+ --ican-bg: transparent;
158
+ --ican-header-bg: rgba(255, 255, 255, 0.30);
159
+ --ican-primary-bg: rgba(255, 255, 255, 0.42);
160
+ --ican-secondary-bg: rgba(255, 255, 255, 0.35);
161
+ --ican-primary-text: rgba(20, 20, 40, 0.92);
162
+ --ican-secondary-text: rgba(60, 60, 100, 0.62);
163
+ --ican-tertiary-text: rgba(100, 100, 140, 0.45);
164
+ --ican-border: rgba(255, 255, 255, 0.55);
165
+ --ican-border-bright: rgba(255, 255, 255, 0.70);
166
+ --ican-border-focus: rgba(90, 70, 220, 0.80);
167
+ --ican-accent: #5b4cd9;
168
+ --ican-accent-hover: #4a3cb5;
169
+ --ican-accent-text: #4a3cb5;
170
+ --ican-accent-dim: rgba(91, 76, 217, 0.10);
171
+ --ican-error: #dc2626;
172
+ --ican-success: #16a34a;
173
+ --ican-warning: #d97706;
174
+ --ican-info: #2563eb;
175
+ --ican-success-dim: rgba(22, 163, 74, 0.10);
176
+ --ican-error-dim: rgba(220, 38, 38, 0.10);
177
+ --ican-warning-dim: rgba(217, 119, 6, 0.10);
178
+ --ican-card-bg: rgba(255, 255, 255, 0.48);
179
+ --ican-card-shadow: 0 4px 20px rgba(0,0,0,0.08), 0 0 0 1px rgba(255,255,255,0.60), inset 0 1px 0 rgba(255,255,255,0.80);
180
+ --ican-card-hover-shadow: 0 10px 40px rgba(0,0,0,0.12), 0 0 0 1px rgba(255,255,255,0.70), inset 0 1px 0 rgba(255,255,255,0.85);
181
+ --ican-input-bg: rgba(255, 255, 255, 0.50);
182
+ --ican-hover: rgba(255, 255, 255, 0.30);
183
+ --ican-modal-bg: rgba(255, 255, 255, 0.72);
184
+ --ican-modal-shadow: 0 24px 80px rgba(0,0,0,0.15), 0 0 0 1px rgba(255,255,255,0.65), inset 0 1px 0 rgba(255,255,255,0.90);
185
+ --ican-btn-primary-bg: rgba(91, 76, 217, 0.85);
186
+ --ican-btn-primary-text: #ffffff;
187
+ --ican-btn-secondary-bg: rgba(255, 255, 255, 0.55);
188
+ --ican-btn-secondary-text: rgba(30, 30, 60, 0.85);
189
+ --ican-scrollbar-thumb: rgba(0, 0, 0, 0.12);
190
+ --ican-backdrop-filter: blur(22px) saturate(190%) brightness(1.08);
191
+ --ican-glass-border: rgba(255, 255, 255, 0.50);
192
+ }
193
+
194
+ /* ── Glass Dark: vivid deep purple background ─────────── */
195
+ [data-theme='glass-dark'] body {
196
+ background:
197
+ radial-gradient(ellipse 70% 60% at 5% 10%, rgba(100, 70, 200, 0.40) 0%, transparent 70%),
198
+ radial-gradient(ellipse 80% 50% at 80% 5%, rgba( 50,100, 200, 0.30) 0%, transparent 65%),
199
+ radial-gradient(ellipse 60% 50% at 5% 90%, rgba( 80, 50, 180, 0.35) 0%, transparent 60%),
200
+ radial-gradient(ellipse 65% 55% at 90% 85%, rgba( 40, 80, 160, 0.25) 0%, transparent 60%),
201
+ #08061a;
202
+ }
203
+
204
+ /* ── Glass Light: bright pastel sky background ────────── */
205
+ [data-theme='glass-light'] body {
206
+ background:
207
+ radial-gradient(ellipse 55% 50% at 0% 15%, rgba(160,130,255,0.65) 0%, transparent 70%),
208
+ radial-gradient(ellipse 90% 45% at 55% 0%, rgba(100,180,255,0.75) 0%, transparent 65%),
209
+ radial-gradient(ellipse 45% 55% at 0% 60%, rgba(255,120,180,0.55) 0%, transparent 65%),
210
+ radial-gradient(ellipse 55% 50% at 85% 50%, rgba( 80,220,220,0.40) 0%, transparent 60%),
211
+ radial-gradient(ellipse 50% 40% at 5% 95%, rgba(255,170,120,0.50) 0%, transparent 60%),
212
+ radial-gradient(ellipse 60% 50% at 90% 90%, rgba(255,140,200,0.45) 0%, transparent 55%),
213
+ #d0e4ff;
214
+ }
215
+
216
+ /* ── Glass noise grain overlay ────────────────────────── */
217
+ [data-theme='glass-dark'] body::before,
218
+ [data-theme='glass-light'] body::before {
219
+ content: '';
220
+ position: fixed;
221
+ inset: 0;
222
+ z-index: 0;
223
+ pointer-events: none;
224
+ opacity: 0.04;
225
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
226
+ background-repeat: repeat;
227
+ background-size: 180px 180px;
228
+ }
229
+ [data-theme='glass-light'] body::before { opacity: 0.035; }
230
+
231
+ /* ── Glass: app layer above noise ─────────────────────── */
232
+ [data-theme='glass-dark'] #app,
233
+ [data-theme='glass-light'] #app { position: relative; z-index: 1; }
234
+
235
+ /* ── Glass: frosted header ────────────────────────────── */
236
+ [data-theme='glass-dark'] .ican-header,
237
+ [data-theme='glass-light'] .ican-header {
238
+ backdrop-filter: var(--ican-backdrop-filter);
239
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
240
+ border-color: var(--ican-glass-border);
241
+ }
242
+
243
+ /* ── Light: frosted header ────────────────────────────── */
244
+ [data-theme='light'] .ican-header {
245
+ backdrop-filter: blur(20px) saturate(180%);
246
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
247
+ }
248
+
249
+ /* ── Glass: frosted widget cards ──────────────────────── */
250
+ [data-theme='glass-dark'] .ican-widget-card,
251
+ [data-theme='glass-light'] .ican-widget-card {
252
+ backdrop-filter: blur(32px) saturate(200%) brightness(1.06);
253
+ -webkit-backdrop-filter: blur(32px) saturate(200%) brightness(1.06);
254
+ border-color: var(--ican-glass-border);
255
+ }
256
+
257
+ /* ── Glass: frosted panel ─────────────────────────────── */
258
+ [data-theme='glass-dark'] .ican-panel,
259
+ [data-theme='glass-light'] .ican-panel,
260
+ [data-theme='glass-dark'] .ican-theme-dropdown,
261
+ [data-theme='glass-light'] .ican-theme-dropdown {
262
+ backdrop-filter: blur(48px) saturate(210%);
263
+ -webkit-backdrop-filter: blur(48px) saturate(210%);
264
+ }
265
+
266
+ /* ── Glass: frosted conn bar ──────────────────────────── */
267
+ [data-theme='glass-dark'] .ican-conn-bar,
268
+ [data-theme='glass-light'] .ican-conn-bar {
269
+ backdrop-filter: var(--ican-backdrop-filter);
270
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
271
+ border-color: var(--ican-glass-border);
272
+ }
273
+
274
+ /* ═══════════════════════════════════════════════════════
275
+ Reset & Base
276
+ ═══════════════════════════════════════════════════════ */
277
+ *, *::before, *::after {
278
+ box-sizing: border-box;
279
+ margin: 0;
280
+ padding: 0;
281
+ transition-property: color, background-color, border-color, box-shadow, opacity, transform;
282
+ transition-duration: 150ms;
283
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
284
+ }
285
+
286
+ html { font-size: 14px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
287
+ html, body { height: 100%; }
288
+
289
+ body {
290
+ font-family: var(--ican-font-body);
291
+ color: var(--ican-primary-text);
292
+ background-color: var(--ican-bg);
293
+ line-height: 1.5;
294
+ }
295
+
296
+ *:focus-visible { outline: 2px solid var(--ican-border-focus); outline-offset: 2px; }
297
+
298
+ * { scrollbar-width: thin; scrollbar-color: var(--ican-scrollbar-thumb) transparent; }
299
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
300
+ ::-webkit-scrollbar-thumb { background: var(--ican-scrollbar-thumb); border-radius: 3px; }
301
+
302
+ /* ═══════════════════════════════════════════════════════
303
+ Layout Shell
304
+ ═══════════════════════════════════════════════════════ */
305
+ #app {
306
+ display: flex;
307
+ flex-direction: column;
308
+ height: 100vh;
309
+ overflow: hidden;
310
+ }
311
+
312
+ /* ═══════════════════════════════════════════════════════
313
+ Header (z-index 600 keeps it above the theme backdrop 499)
314
+ ═══════════════════════════════════════════════════════ */
315
+ .ican-header {
316
+ position: fixed;
317
+ top: 0; left: 0; right: 0;
318
+ z-index: 600;
319
+ height: 56px;
320
+ background: var(--ican-header-bg);
321
+ border-bottom: 1px solid var(--ican-border);
322
+ display: flex;
323
+ align-items: center;
324
+ padding: 0 18px;
325
+ gap: 10px;
326
+ flex-shrink: 0;
327
+ }
328
+
329
+ .ican-logo {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 9px;
333
+ text-decoration: none;
334
+ flex-shrink: 0;
335
+ }
336
+
337
+ .ican-logo img { height: 28px; width: auto; }
338
+
339
+ .ican-logo-name {
340
+ font-family: var(--ican-font-brand);
341
+ font-size: 17px;
342
+ font-weight: 600;
343
+ color: var(--ican-primary-text);
344
+ letter-spacing: -0.3px;
345
+ }
346
+
347
+ .ican-logo-name em { font-style: normal; color: var(--ican-accent); }
348
+
349
+ .ican-sep { width: 1px; height: 22px; background: var(--ican-border-bright); flex-shrink: 0; }
350
+
351
+ .ican-dev-badge {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ padding: 2px 8px;
355
+ background: var(--ican-accent-dim);
356
+ color: var(--ican-accent-text);
357
+ border-radius: var(--ican-radius-xs);
358
+ border: 1px solid rgba(124, 106, 255, 0.20);
359
+ font-size: 10px;
360
+ font-weight: 700;
361
+ font-family: var(--ican-font-mono);
362
+ letter-spacing: 0.8px;
363
+ text-transform: uppercase;
364
+ }
365
+
366
+ .ican-spacer { flex: 1; }
367
+
368
+ /* ─── Icon Button ─── */
369
+ .ican-icon-btn {
370
+ display: inline-flex;
371
+ align-items: center;
372
+ justify-content: center;
373
+ width: 34px;
374
+ height: 34px;
375
+ border: 1px solid var(--ican-border);
376
+ border-radius: var(--ican-radius-sm);
377
+ background: transparent;
378
+ color: var(--ican-secondary-text);
379
+ cursor: pointer;
380
+ flex-shrink: 0;
381
+ }
382
+
383
+ .ican-icon-btn:hover {
384
+ background: var(--ican-hover);
385
+ color: var(--ican-primary-text);
386
+ border-color: var(--ican-border-bright);
387
+ }
388
+
389
+ /* ─── Avatar ─── */
390
+ .ican-avatar {
391
+ width: 32px;
392
+ height: 32px;
393
+ border-radius: var(--ican-radius-sm);
394
+ background: linear-gradient(135deg, var(--ican-accent), #e879f9);
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ color: #fff;
399
+ font-size: 12px;
400
+ font-weight: 700;
401
+ font-family: var(--ican-font-brand);
402
+ flex-shrink: 0;
403
+ }
404
+
405
+ /* ─── Theme Switcher ─── */
406
+ .ican-theme-switcher { position: relative; flex-shrink: 0; }
407
+
408
+ .ican-theme-btn {
409
+ display: inline-flex;
410
+ align-items: center;
411
+ gap: 6px;
412
+ padding: 0 10px;
413
+ height: 34px;
414
+ border: 1px solid var(--ican-border);
415
+ border-radius: var(--ican-radius-sm);
416
+ background: transparent;
417
+ color: var(--ican-secondary-text);
418
+ cursor: pointer;
419
+ font-size: 12px;
420
+ font-family: var(--ican-font-body);
421
+ font-weight: 500;
422
+ }
423
+
424
+ .ican-theme-btn:hover {
425
+ background: var(--ican-hover);
426
+ color: var(--ican-primary-text);
427
+ border-color: var(--ican-border-bright);
428
+ }
429
+
430
+ .ican-theme-swatch {
431
+ width: 12px;
432
+ height: 12px;
433
+ border-radius: 50%;
434
+ flex-shrink: 0;
435
+ }
436
+
437
+ .ican-theme-dropdown {
438
+ position: absolute;
439
+ top: calc(100% + 6px);
440
+ right: 0;
441
+ z-index: 700;
442
+ min-width: 180px;
443
+ background: var(--ican-modal-bg);
444
+ border: 1px solid var(--ican-border-bright);
445
+ border-radius: var(--ican-radius-md);
446
+ box-shadow: var(--ican-modal-shadow);
447
+ padding: 6px;
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 2px;
451
+ }
452
+
453
+ .ican-theme-option {
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 10px;
457
+ padding: 8px 10px;
458
+ border-radius: var(--ican-radius-sm);
459
+ border: none;
460
+ background: transparent;
461
+ color: var(--ican-primary-text);
462
+ cursor: pointer;
463
+ font-size: 13px;
464
+ font-family: var(--ican-font-body);
465
+ font-weight: 500;
466
+ width: 100%;
467
+ text-align: left;
468
+ }
469
+
470
+ .ican-theme-option:hover { background: var(--ican-hover); }
471
+ .ican-theme-option.active { background: var(--ican-accent-dim); color: var(--ican-accent-text); }
472
+
473
+ .ican-theme-option-check { margin-left: auto; color: var(--ican-accent); }
474
+
475
+ /* ─── Primary Button ─── */
476
+ .ican-btn {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: 6px;
480
+ padding: 0 14px;
481
+ height: 34px;
482
+ border-radius: var(--ican-radius-sm);
483
+ font-size: 13px;
484
+ font-weight: 500;
485
+ font-family: var(--ican-font-body);
486
+ cursor: pointer;
487
+ border: 1px solid transparent;
488
+ }
489
+
490
+ .ican-btn-primary {
491
+ background: var(--ican-btn-primary-bg);
492
+ color: var(--ican-btn-primary-text);
493
+ }
494
+ .ican-btn-primary:hover { opacity: 0.88; }
495
+
496
+ .ican-btn-ghost {
497
+ background: transparent;
498
+ color: var(--ican-secondary-text);
499
+ border-color: var(--ican-border);
500
+ }
501
+ .ican-btn-ghost:hover {
502
+ background: var(--ican-hover);
503
+ color: var(--ican-primary-text);
504
+ border-color: var(--ican-border-bright);
505
+ }
506
+
507
+ .ican-btn:disabled {
508
+ opacity: 0.45;
509
+ cursor: not-allowed;
510
+ }
511
+
512
+ /* ═══════════════════════════════════════════════════════
513
+ Connection Bar
514
+ ═══════════════════════════════════════════════════════ */
515
+ .ican-conn-bar {
516
+ position: fixed;
517
+ top: 56px;
518
+ left: 0;
519
+ right: 0;
520
+ z-index: 590;
521
+ background: var(--ican-primary-bg);
522
+ border-bottom: 1px solid var(--ican-border);
523
+ padding: 8px 18px;
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 10px;
527
+ flex-wrap: wrap;
528
+ }
529
+
530
+ .ican-conn-label {
531
+ font-size: 11px;
532
+ font-weight: 600;
533
+ text-transform: uppercase;
534
+ letter-spacing: 0.6px;
535
+ color: var(--ican-tertiary-text);
536
+ font-family: var(--ican-font-mono);
537
+ flex-shrink: 0;
538
+ }
539
+
540
+ .ican-conn-input {
541
+ flex: 1;
542
+ min-width: 160px;
543
+ height: 30px;
544
+ padding: 0 10px;
545
+ border: 1px solid var(--ican-border-bright);
546
+ border-radius: var(--ican-radius-sm);
547
+ background: var(--ican-input-bg);
548
+ color: var(--ican-primary-text);
549
+ font-size: 12px;
550
+ font-family: var(--ican-font-mono);
551
+ }
552
+
553
+ .ican-conn-input::placeholder { color: var(--ican-tertiary-text); }
554
+ .ican-conn-input:focus { border-color: var(--ican-border-focus); outline: none; box-shadow: 0 0 0 3px var(--ican-accent-dim); }
555
+
556
+ .ican-conn-status {
557
+ display: inline-flex;
558
+ align-items: center;
559
+ gap: 5px;
560
+ font-size: 11px;
561
+ color: var(--ican-tertiary-text);
562
+ font-family: var(--ican-font-mono);
563
+ }
564
+
565
+ .ican-status-dot {
566
+ width: 7px; height: 7px;
567
+ border-radius: 50%;
568
+ background: var(--ican-tertiary-text);
569
+ }
570
+ .ican-status-dot.connected { background: var(--ican-success); box-shadow: 0 0 0 3px var(--ican-success-dim); }
571
+
572
+ /* ═══════════════════════════════════════════════════════
573
+ Main Content Area
574
+ ═══════════════════════════════════════════════════════ */
575
+ .ican-main {
576
+ margin-top: 56px;
577
+ flex: 1;
578
+ overflow-y: auto;
579
+ padding: 24px 20px;
580
+ background: transparent;
581
+ }
582
+
583
+ .ican-main--with-conn { margin-top: 104px; } /* 56 header + 48 conn bar */
584
+
585
+ /* ═══════════════════════════════════════════════════════
586
+ Widget Grid
587
+ ═══════════════════════════════════════════════════════ */
588
+ .ican-widget-grid {
589
+ display: grid;
590
+ grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
591
+ gap: 20px;
592
+ }
593
+
594
+ /* ═══════════════════════════════════════════════════════
595
+ Empty States
596
+ ═══════════════════════════════════════════════════════ */
597
+
598
+ /* State A: Waiting for `npm run build` */
599
+ .ican-waiting {
600
+ grid-column: 1 / -1;
601
+ display: flex;
602
+ flex-direction: column;
603
+ align-items: center;
604
+ justify-content: center;
605
+ padding: 80px 24px;
606
+ gap: 20px;
607
+ text-align: center;
608
+ }
609
+
610
+ .ican-waiting__icon {
611
+ width: 72px; height: 72px;
612
+ border-radius: var(--ican-radius-lg);
613
+ background: var(--ican-accent-dim);
614
+ border: 1px solid rgba(124, 106, 255, 0.18);
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ color: var(--ican-accent);
619
+ }
620
+
621
+ .ican-waiting__title {
622
+ font-family: var(--ican-font-brand);
623
+ font-size: 20px;
624
+ font-weight: 600;
625
+ color: var(--ican-primary-text);
626
+ }
627
+
628
+ .ican-waiting__sub {
629
+ font-size: 13px;
630
+ color: var(--ican-secondary-text);
631
+ max-width: 340px;
632
+ line-height: 1.6;
633
+ }
634
+
635
+ .ican-waiting__cmd {
636
+ display: inline-flex;
637
+ align-items: center;
638
+ gap: 8px;
639
+ padding: 10px 18px;
640
+ background: var(--ican-primary-bg);
641
+ border: 1px solid var(--ican-border-bright);
642
+ border-radius: var(--ican-radius-md);
643
+ font-family: var(--ican-font-mono);
644
+ font-size: 14px;
645
+ color: var(--ican-accent-text);
646
+ }
647
+
648
+ .ican-waiting__hint {
649
+ font-size: 12px;
650
+ color: var(--ican-tertiary-text);
651
+ }
652
+
653
+ /* State B: Widgets available — inline cards */
654
+ .ican-available {
655
+ grid-column: 1 / -1;
656
+ display: flex;
657
+ flex-direction: column;
658
+ align-items: center;
659
+ gap: 24px;
660
+ padding: 40px 0;
661
+ text-align: center;
662
+ }
663
+
664
+ .ican-available__heading {
665
+ display: flex;
666
+ flex-direction: column;
667
+ align-items: center;
668
+ gap: 8px;
669
+ }
670
+
671
+ .ican-available__title {
672
+ font-family: var(--ican-font-brand);
673
+ font-size: 18px;
674
+ font-weight: 600;
675
+ color: var(--ican-primary-text);
676
+ }
677
+
678
+ .ican-available__sub {
679
+ font-size: 13px;
680
+ color: var(--ican-secondary-text);
681
+ }
682
+
683
+ .ican-available__grid {
684
+ display: flex;
685
+ flex-wrap: wrap;
686
+ gap: 12px;
687
+ justify-content: center;
688
+ max-width: 680px;
689
+ }
690
+
691
+ .ican-available__card {
692
+ display: flex;
693
+ align-items: center;
694
+ gap: 12px;
695
+ padding: 14px 18px;
696
+ background: var(--ican-card-bg);
697
+ border: 1px solid var(--ican-border-bright);
698
+ border-radius: var(--ican-radius-md);
699
+ box-shadow: var(--ican-card-shadow);
700
+ min-width: 240px;
701
+ text-align: left;
702
+ }
703
+
704
+ .ican-available__card-icon {
705
+ width: 40px; height: 40px;
706
+ border-radius: var(--ican-radius-sm);
707
+ background: var(--ican-accent-dim);
708
+ border: 1px solid rgba(124, 106, 255, 0.15);
709
+ display: flex;
710
+ align-items: center;
711
+ justify-content: center;
712
+ color: var(--ican-accent);
713
+ flex-shrink: 0;
714
+ }
715
+
716
+ .ican-available__card-info { flex: 1; overflow: hidden; }
717
+
718
+ .ican-available__card-name {
719
+ font-weight: 600;
720
+ font-size: 13px;
721
+ color: var(--ican-primary-text);
722
+ overflow: hidden;
723
+ text-overflow: ellipsis;
724
+ white-space: nowrap;
725
+ }
726
+
727
+ .ican-available__card-id {
728
+ font-size: 10px;
729
+ font-family: var(--ican-font-mono);
730
+ color: var(--ican-tertiary-text);
731
+ margin-top: 2px;
732
+ overflow: hidden;
733
+ text-overflow: ellipsis;
734
+ white-space: nowrap;
735
+ }
736
+
737
+ /* ═══════════════════════════════════════════════════════
738
+ Widget Card
739
+ ═══════════════════════════════════════════════════════ */
740
+ .ican-widget-card {
741
+ background: var(--ican-card-bg);
742
+ border: 1px solid var(--ican-border);
743
+ border-radius: var(--ican-radius-lg);
744
+ box-shadow: var(--ican-card-shadow);
745
+ display: flex;
746
+ flex-direction: column;
747
+ min-height: 340px;
748
+ overflow: hidden;
749
+ }
750
+
751
+ .ican-widget-card-header {
752
+ display: flex;
753
+ align-items: center;
754
+ padding: 0 14px;
755
+ height: 46px;
756
+ border-bottom: 1px solid var(--ican-border);
757
+ gap: 8px;
758
+ flex-shrink: 0;
759
+ }
760
+
761
+ .ican-drag-handle {
762
+ color: var(--ican-tertiary-text);
763
+ cursor: grab;
764
+ display: flex;
765
+ align-items: center;
766
+ }
767
+ .ican-drag-handle:active { cursor: grabbing; }
768
+
769
+ .ican-widget-card-name {
770
+ font-family: var(--ican-font-brand);
771
+ font-size: 11px;
772
+ font-weight: 700;
773
+ letter-spacing: 0.6px;
774
+ text-transform: uppercase;
775
+ color: var(--ican-primary-text);
776
+ flex: 1;
777
+ overflow: hidden;
778
+ text-overflow: ellipsis;
779
+ white-space: nowrap;
780
+ }
781
+
782
+ .ican-widget-card-close {
783
+ display: inline-flex;
784
+ align-items: center;
785
+ justify-content: center;
786
+ width: 26px; height: 26px;
787
+ border: none;
788
+ border-radius: var(--ican-radius-xs);
789
+ background: transparent;
790
+ color: var(--ican-tertiary-text);
791
+ cursor: pointer;
792
+ flex-shrink: 0;
793
+ }
794
+ .ican-widget-card-close:hover { background: var(--ican-error-dim); color: var(--ican-error); }
795
+
796
+ .ican-widget-card-body { flex: 1; overflow: auto; position: relative; }
797
+
798
+ /* ─── Widget Error ─── */
799
+ .ican-widget-error {
800
+ display: flex;
801
+ flex-direction: column;
802
+ align-items: center;
803
+ justify-content: center;
804
+ height: 100%;
805
+ min-height: 200px;
806
+ padding: 24px;
807
+ gap: 8px;
808
+ color: var(--ican-error);
809
+ text-align: center;
810
+ }
811
+
812
+ .ican-widget-error-icon { color: var(--ican-error); }
813
+ .ican-widget-error-title { font-size: 13px; font-weight: 600; }
814
+ .ican-widget-error-msg {
815
+ font-size: 11px;
816
+ color: var(--ican-tertiary-text);
817
+ font-family: var(--ican-font-mono);
818
+ max-width: 100%;
819
+ overflow-wrap: break-word;
820
+ }
821
+
822
+ /* ═══════════════════════════════════════════════════════
823
+ Add Widget Panel (slide-in from right)
824
+ ═══════════════════════════════════════════════════════ */
825
+ .ican-panel-overlay {
826
+ position: fixed;
827
+ inset: 0;
828
+ z-index: 800;
829
+ background: rgba(0, 0, 0, 0.4);
830
+ opacity: 0;
831
+ pointer-events: none;
832
+ transition: opacity 200ms ease;
833
+ }
834
+ .ican-panel-overlay.open { opacity: 1; pointer-events: all; }
835
+
836
+ .ican-panel {
837
+ position: fixed;
838
+ top: 0; right: 0; bottom: 0;
839
+ z-index: 801;
840
+ width: 340px;
841
+ max-width: 100vw;
842
+ background: var(--ican-modal-bg);
843
+ border-left: 1px solid var(--ican-border-bright);
844
+ box-shadow: -4px 0 32px rgba(0,0,0,0.25);
845
+ display: flex;
846
+ flex-direction: column;
847
+ transform: translateX(100%);
848
+ transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
849
+ }
850
+ .ican-panel.open { transform: translateX(0); }
851
+
852
+ .ican-panel-header {
853
+ display: flex;
854
+ align-items: center;
855
+ padding: 0 18px;
856
+ height: 56px;
857
+ border-bottom: 1px solid var(--ican-border);
858
+ gap: 12px;
859
+ flex-shrink: 0;
860
+ }
861
+
862
+ .ican-panel-title {
863
+ font-family: var(--ican-font-brand);
864
+ font-size: 15px;
865
+ font-weight: 600;
866
+ color: var(--ican-primary-text);
867
+ flex: 1;
868
+ }
869
+
870
+ .ican-panel-body {
871
+ flex: 1;
872
+ overflow-y: auto;
873
+ padding: 14px;
874
+ display: flex;
875
+ flex-direction: column;
876
+ gap: 8px;
877
+ }
878
+
879
+ .ican-panel-empty {
880
+ display: flex;
881
+ flex-direction: column;
882
+ align-items: center;
883
+ justify-content: center;
884
+ padding: 48px 16px;
885
+ gap: 12px;
886
+ color: var(--ican-tertiary-text);
887
+ text-align: center;
888
+ }
889
+
890
+ .ican-registry-item {
891
+ display: flex;
892
+ align-items: center;
893
+ gap: 12px;
894
+ padding: 12px 14px;
895
+ border: 1px solid var(--ican-border);
896
+ border-radius: var(--ican-radius-md);
897
+ background: var(--ican-primary-bg);
898
+ }
899
+
900
+ .ican-registry-item-icon {
901
+ width: 38px; height: 38px;
902
+ border-radius: var(--ican-radius-sm);
903
+ background: var(--ican-accent-dim);
904
+ border: 1px solid rgba(124, 106, 255, 0.15);
905
+ display: flex;
906
+ align-items: center;
907
+ justify-content: center;
908
+ color: var(--ican-accent);
909
+ flex-shrink: 0;
910
+ }
911
+
912
+ .ican-registry-item-info { flex: 1; overflow: hidden; }
913
+
914
+ .ican-registry-item-name {
915
+ font-weight: 600;
916
+ font-size: 13px;
917
+ color: var(--ican-primary-text);
918
+ overflow: hidden;
919
+ text-overflow: ellipsis;
920
+ white-space: nowrap;
921
+ }
922
+
923
+ .ican-registry-item-id {
924
+ font-size: 10px;
925
+ color: var(--ican-tertiary-text);
926
+ font-family: var(--ican-font-mono);
927
+ margin-top: 2px;
928
+ overflow: hidden;
929
+ text-overflow: ellipsis;
930
+ white-space: nowrap;
931
+ }
932
+
933
+ /* ═══════════════════════════════════════════════════════
934
+ Toast Notifications
935
+ ═══════════════════════════════════════════════════════ */
936
+ .ican-toast-container {
937
+ position: fixed;
938
+ bottom: 20px;
939
+ right: 20px;
940
+ z-index: 9999;
941
+ display: flex;
942
+ flex-direction: column;
943
+ gap: 8px;
944
+ pointer-events: none;
945
+ }
946
+
947
+ .ican-toast {
948
+ padding: 10px 16px;
949
+ border-radius: var(--ican-radius-sm);
950
+ background: var(--ican-card-bg);
951
+ border: 1px solid var(--ican-border-bright);
952
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2);
953
+ font-size: 13px;
954
+ color: var(--ican-primary-text);
955
+ max-width: 320px;
956
+ pointer-events: all;
957
+ animation: toastIn 200ms ease forwards;
958
+ }
959
+
960
+ .ican-toast.success { border-left: 3px solid var(--ican-success); }
961
+ .ican-toast.error { border-left: 3px solid var(--ican-error); }
962
+ .ican-toast.info { border-left: 3px solid var(--ican-accent); }
963
+
964
+ @keyframes toastIn {
965
+ from { opacity: 0; transform: translateY(10px); }
966
+ to { opacity: 1; transform: translateY(0); }
967
+ }
968
+
969
+ /* ═══════════════════════════════════════════════════════
970
+ ICan Component CSS (used by ican-components.js)
971
+ ═══════════════════════════════════════════════════════ */
972
+ .ican-widget-wrapper { display: flex; flex-direction: column; width: 100%; height: 100%; }
973
+
974
+ .ican-card {
975
+ background: var(--ican-card-bg);
976
+ border: 1px solid var(--ican-border);
977
+ border-radius: var(--ican-radius-md);
978
+ padding: 14px;
979
+ }
980
+
981
+ .ican-loading {
982
+ display: flex;
983
+ align-items: center;
984
+ justify-content: center;
985
+ gap: 10px;
986
+ padding: 24px;
987
+ color: var(--ican-secondary-text);
988
+ font-size: 13px;
989
+ }
990
+
991
+ @keyframes spin { to { transform: rotate(360deg); } }
992
+
993
+ .ican-spinner {
994
+ width: 28px; height: 28px;
995
+ border: 2.5px solid var(--ican-border);
996
+ border-top-color: var(--ican-accent);
997
+ border-radius: 50%;
998
+ animation: spin 0.7s linear infinite;
999
+ flex-shrink: 0;
1000
+ }
1001
+
1002
+ .ican-error-display {
1003
+ display: flex;
1004
+ gap: 8px;
1005
+ padding: 12px 16px;
1006
+ background: var(--ican-error-dim);
1007
+ border-radius: var(--ican-radius-sm);
1008
+ color: var(--ican-error);
1009
+ font-size: 13px;
1010
+ }
1011
+
1012
+ .ican-badge {
1013
+ display: inline-flex;
1014
+ align-items: center;
1015
+ font-size: 10px;
1016
+ padding: 2px 7px;
1017
+ border-radius: 99px;
1018
+ font-weight: 600;
1019
+ }
1020
+
1021
+ .ican-badge--default { background: var(--ican-accent-dim); color: var(--ican-accent-text); }
1022
+ .ican-badge--success { background: var(--ican-success-dim); color: var(--ican-success); }
1023
+ .ican-badge--error { background: var(--ican-error-dim); color: var(--ican-error); }
1024
+ </style>
1025
+ </head>
1026
+ <body>
1027
+ <div id="app"></div>
1028
+ <div id="toast-root" class="ican-toast-container"></div>
1029
+
1030
+ <script>
1031
+ (function () {
1032
+ 'use strict';
1033
+
1034
+ /* ─── SVG Icon helpers (Heroicons outline 20px) ─────────── */
1035
+ var SVG = {
1036
+ /* cube / widget */
1037
+ cube: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/></svg>',
1038
+ /* squares / grid */
1039
+ squares: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zm0 9.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zm9.75-9.75A2.25 2.25 0 0115.75 3.75H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zm0 9.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/></svg>',
1040
+ /* plus */
1041
+ plus: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>',
1042
+ /* x-mark / close */
1043
+ xmark: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>',
1044
+ /* cog */
1045
+ cog: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>',
1046
+ /* swatch / theme */
1047
+ swatch: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"/></svg>',
1048
+ /* chevron-down */
1049
+ chevronDown: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>',
1050
+ /* check */
1051
+ check: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>',
1052
+ /* command-line / terminal */
1053
+ terminal: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"/></svg>',
1054
+ /* signal / connection */
1055
+ signal: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.008H12V12z"/></svg>',
1056
+ /* drag dots */
1057
+ drag: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8.5 3a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm7 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-7 7a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm7 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-7 7a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm7 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3z"/></svg>',
1058
+ /* warning */
1059
+ warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>',
1060
+ };
1061
+
1062
+ function icon(svgStr, style) {
1063
+ var wrapper = document.createElement('span');
1064
+ wrapper.innerHTML = svgStr;
1065
+ var el = wrapper.firstChild;
1066
+ if (style) { Object.assign(el.style, style); }
1067
+ return el;
1068
+ }
1069
+
1070
+ /* helper to render an SVG as a React element */
1071
+ var h = React.createElement;
1072
+
1073
+ function SvgIcon(props) {
1074
+ return h('span', {
1075
+ dangerouslySetInnerHTML: { __html: props.svg },
1076
+ style: Object.assign({ display: 'inline-flex', alignItems: 'center' }, props.style || {}),
1077
+ 'aria-hidden': 'true'
1078
+ });
1079
+ }
1080
+
1081
+ /* ─── Constants ─────────────────────────────────────────── */
1082
+ var LS_THEME = 'ican_theme';
1083
+ var LS_ADDED = 'ican_added_widgets';
1084
+ var LS_ORCH_URL = 'ican_orchUrl';
1085
+ var LS_API_KEY = 'ican_apiKey';
1086
+
1087
+ /* ─── Themes ─────────────────────────────────────────────── */
1088
+ var THEMES = [
1089
+ { key: 'dark', label: 'Dark', type: 'Dark', bg: '#09090b', mid: '#141417', accent: '#7c6aff', glass: false },
1090
+ { key: 'light', label: 'Light', type: 'Light', bg: '#f5f5f7', mid: '#ffffff', accent: '#5b4cd9', glass: false },
1091
+ { key: 'glass-dark', label: 'Glass Dark', type: 'Glass-Dark', bg: '#08061a', mid: 'rgba(80,50,180,0.4)', accent: '#b4a8ff', glass: true },
1092
+ { key: 'glass-light', label: 'Glass Light', type: 'Glass-Light', bg: '#d0e4ff', mid: 'rgba(160,130,255,0.5)', accent: '#5b4cd9', glass: true },
1093
+ ];
1094
+
1095
+ function getThemeMeta(key) {
1096
+ for (var i = 0; i < THEMES.length; i++) {
1097
+ if (THEMES[i].key === key) return THEMES[i];
1098
+ }
1099
+ return THEMES[0];
1100
+ }
1101
+
1102
+ /* ─── Widget Registry ────────────────────────────────────── */
1103
+ window.__ican_registry = new Map();
1104
+
1105
+ window.registerWidget = function (config) {
1106
+ if (!config || !config.id) {
1107
+ console.warn('[ICan Dev] registerWidget called without a valid config.id');
1108
+ return;
1109
+ }
1110
+ window.__ican_registry.set(config.id, config);
1111
+ console.log('[ICan Dev] Widget registered:', config.id, config.name || '');
1112
+ window.dispatchEvent(new CustomEvent('ican:registry-updated'));
1113
+ };
1114
+
1115
+ /* ─── icanContext factory ─────────────────────────────────── */
1116
+ function makeICanContext(themeKey) {
1117
+ var meta = getThemeMeta(themeKey);
1118
+ return {
1119
+ environment: 'dev',
1120
+ userKey: 'dev-user-001',
1121
+ language: 'en',
1122
+ root: 'root',
1123
+ orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1124
+ apiKey: localStorage.getItem(LS_API_KEY) || '',
1125
+ executeAction: function (model, action, params) {
1126
+ console.log('[ICan Dev] executeAction:', model, action, params);
1127
+ return Promise.resolve([]);
1128
+ },
1129
+ executeService: function (app, service, params) {
1130
+ console.log('[ICan Dev] executeService:', app, service, params);
1131
+ return Promise.resolve(null);
1132
+ },
1133
+ fireEvent: function (id) {
1134
+ console.log('[ICan Dev] fireEvent:', id);
1135
+ return Promise.resolve();
1136
+ },
1137
+ hasAppRole: function () { return true; },
1138
+ $L: function (code) { return code; },
1139
+ themeName: meta.label,
1140
+ themeType: meta.type,
1141
+ };
1142
+ }
1143
+
1144
+ /* ─── Theme helpers ──────────────────────────────────────── */
1145
+ function getStoredTheme() {
1146
+ return localStorage.getItem(LS_THEME) || 'dark';
1147
+ }
1148
+
1149
+ function applyTheme(key) {
1150
+ var el = document.documentElement;
1151
+ if (key === 'dark') { el.removeAttribute('data-theme'); }
1152
+ else { el.setAttribute('data-theme', key); }
1153
+ localStorage.setItem(LS_THEME, key);
1154
+ }
1155
+
1156
+ applyTheme(getStoredTheme());
1157
+
1158
+ /* ─── Persistence helpers ────────────────────────────────── */
1159
+ function loadAddedWidgets() {
1160
+ try {
1161
+ var raw = localStorage.getItem(LS_ADDED);
1162
+ return raw ? JSON.parse(raw) : [];
1163
+ } catch (_e) { return []; }
1164
+ }
1165
+
1166
+ function saveAddedWidgets(ids) {
1167
+ localStorage.setItem(LS_ADDED, JSON.stringify(ids));
1168
+ }
1169
+
1170
+ /* ─── Toast ──────────────────────────────────────────────── */
1171
+ function showToast(msg, type) {
1172
+ var root = document.getElementById('toast-root');
1173
+ if (!root) return;
1174
+ var el = document.createElement('div');
1175
+ el.className = 'ican-toast ' + (type || 'info');
1176
+ el.textContent = msg;
1177
+ root.appendChild(el);
1178
+ setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
1179
+ }
1180
+
1181
+ /* ═══════════════════════════════════════════════════════════
1182
+ Sub-components
1183
+ ═══════════════════════════════════════════════════════════ */
1184
+
1185
+ /* ─── WidgetRenderer ─────────────────────────────────────── */
1186
+ function WidgetRenderer(props) {
1187
+ var cfg = props.config;
1188
+ var ctx = props.icanContext;
1189
+
1190
+ if (!cfg || !cfg.component) {
1191
+ return h('div', { className: 'ican-widget-error' },
1192
+ h(SvgIcon, { svg: SVG.warning }),
1193
+ h('div', { className: 'ican-widget-error-title' }, 'No component defined'),
1194
+ h('div', { className: 'ican-widget-error-msg' }, cfg ? cfg.id : 'unknown')
1195
+ );
1196
+ }
1197
+
1198
+ try {
1199
+ return h(cfg.component, Object.assign({}, cfg.defaultProps || {}, { icanContext: ctx }));
1200
+ } catch (err) {
1201
+ return h('div', { className: 'ican-widget-error' },
1202
+ h(SvgIcon, { svg: SVG.warning }),
1203
+ h('div', { className: 'ican-widget-error-title' }, 'Render error'),
1204
+ h('div', { className: 'ican-widget-error-msg' }, String(err && err.message ? err.message : err))
1205
+ );
1206
+ }
1207
+ }
1208
+
1209
+ /* ─── ErrorBoundary ──────────────────────────────────────── */
1210
+ var ErrorBoundary = (function () {
1211
+ function ErrorBoundary(props) {
1212
+ React.Component.call(this, props);
1213
+ this.state = { hasError: false, error: null };
1214
+ }
1215
+ ErrorBoundary.prototype = Object.create(React.Component.prototype);
1216
+ ErrorBoundary.prototype.constructor = ErrorBoundary;
1217
+ ErrorBoundary.getDerivedStateFromError = function (error) {
1218
+ return { hasError: true, error: error };
1219
+ };
1220
+ ErrorBoundary.prototype.render = function () {
1221
+ if (this.state.hasError) {
1222
+ return h('div', { className: 'ican-widget-error' },
1223
+ h(SvgIcon, { svg: SVG.warning }),
1224
+ h('div', { className: 'ican-widget-error-title' }, 'Widget crashed'),
1225
+ h('div', { className: 'ican-widget-error-msg' },
1226
+ this.state.error ? String(this.state.error.message || this.state.error) : 'Unknown error'
1227
+ )
1228
+ );
1229
+ }
1230
+ return this.props.children;
1231
+ };
1232
+ return ErrorBoundary;
1233
+ }());
1234
+
1235
+ /* ─── WidgetCard ─────────────────────────────────────────── */
1236
+ function WidgetCard(props) {
1237
+ var cfg = props.config;
1238
+ var ctx = props.icanContext;
1239
+ var onRemove = props.onRemove;
1240
+
1241
+ return h('div', { className: 'ican-widget-card' },
1242
+ h('div', { className: 'ican-widget-card-header' },
1243
+ h('span', { className: 'ican-drag-handle', title: 'Drag to reorder', 'aria-hidden': 'true' },
1244
+ h(SvgIcon, { svg: SVG.drag })
1245
+ ),
1246
+ h('span', { className: 'ican-widget-card-name' }, cfg.name || cfg.id),
1247
+ h('button', {
1248
+ className: 'ican-widget-card-close',
1249
+ type: 'button',
1250
+ title: 'Remove widget',
1251
+ 'aria-label': 'Remove ' + (cfg.name || cfg.id),
1252
+ onClick: function () { onRemove(cfg.id); }
1253
+ }, h(SvgIcon, { svg: SVG.xmark }))
1254
+ ),
1255
+ h('div', { className: 'ican-widget-card-body' },
1256
+ h(ErrorBoundary, null,
1257
+ h(WidgetRenderer, { config: cfg, icanContext: ctx })
1258
+ )
1259
+ )
1260
+ );
1261
+ }
1262
+
1263
+ /* ─── AddWidgetPanel ─────────────────────────────────────── */
1264
+ function AddWidgetPanel(props) {
1265
+ var open = props.open;
1266
+ var onClose = props.onClose;
1267
+ var registry = props.registry;
1268
+ var addedIds = props.addedIds;
1269
+ var onAdd = props.onAdd;
1270
+
1271
+ var entries = [];
1272
+ registry.forEach(function (cfg, id) { entries.push({ id: id, config: cfg }); });
1273
+
1274
+ return h(React.Fragment, null,
1275
+ h('div', { className: 'ican-panel-overlay' + (open ? ' open' : ''), onClick: onClose, 'aria-hidden': 'true' }),
1276
+ h('aside', { className: 'ican-panel' + (open ? ' open' : ''), role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Add Widget' },
1277
+ h('div', { className: 'ican-panel-header' },
1278
+ h(SvgIcon, { svg: SVG.cube, style: { color: 'var(--ican-accent)' } }),
1279
+ h('span', { className: 'ican-panel-title' }, 'Add Widget'),
1280
+ h('button', { className: 'ican-icon-btn', type: 'button', onClick: onClose, 'aria-label': 'Close panel', title: 'Close' },
1281
+ h(SvgIcon, { svg: SVG.xmark })
1282
+ )
1283
+ ),
1284
+ h('div', { className: 'ican-panel-body' },
1285
+ entries.length === 0
1286
+ ? h('div', { className: 'ican-panel-empty' },
1287
+ h(SvgIcon, { svg: SVG.terminal, style: { color: 'var(--ican-accent)', marginBottom: 8 } }),
1288
+ h('div', { style: { fontSize: 13, lineHeight: 1.6 } },
1289
+ 'No widgets registered yet. Build your widget and run ',
1290
+ h('code', { style: { fontFamily: 'var(--ican-font-mono)', fontSize: 11, background: 'var(--ican-primary-bg)', padding: '1px 5px', borderRadius: 3 } }, 'npm run build'),
1291
+ ' to register it.'
1292
+ )
1293
+ )
1294
+ : entries.map(function (entry) {
1295
+ var alreadyAdded = addedIds.indexOf(entry.id) !== -1;
1296
+ return h('div', { className: 'ican-registry-item', key: entry.id },
1297
+ h('div', { className: 'ican-registry-item-icon' }, h(SvgIcon, { svg: SVG.squares })),
1298
+ h('div', { className: 'ican-registry-item-info' },
1299
+ h('div', { className: 'ican-registry-item-name' }, entry.config.name || entry.id),
1300
+ h('div', { className: 'ican-registry-item-id' }, entry.id)
1301
+ ),
1302
+ h('button', {
1303
+ className: 'ican-btn ' + (alreadyAdded ? 'ican-btn-ghost' : 'ican-btn-primary'),
1304
+ type: 'button',
1305
+ disabled: alreadyAdded,
1306
+ onClick: function () { onAdd(entry.id); },
1307
+ 'aria-label': (alreadyAdded ? 'Already added: ' : 'Add ') + (entry.config.name || entry.id),
1308
+ style: { padding: '0 10px', height: 30, fontSize: 12 }
1309
+ }, alreadyAdded
1310
+ ? h(React.Fragment, null, h(SvgIcon, { svg: SVG.check }), ' Added')
1311
+ : h(React.Fragment, null, h(SvgIcon, { svg: SVG.plus }), ' Add')
1312
+ )
1313
+ );
1314
+ })
1315
+ )
1316
+ )
1317
+ );
1318
+ }
1319
+
1320
+ /* ─── ConnectionBar ──────────────────────────────────────── */
1321
+ function ConnectionBar(props) {
1322
+ var orchUrl = props.orchUrl;
1323
+ var apiKey = props.apiKey;
1324
+ var onSave = props.onSave;
1325
+ var stateRef = { orchUrl: orchUrl, apiKey: apiKey };
1326
+
1327
+ return h('div', { className: 'ican-conn-bar' },
1328
+ h('span', { className: 'ican-conn-label' }, 'Orch URL'),
1329
+ h('input', {
1330
+ className: 'ican-conn-input',
1331
+ type: 'text',
1332
+ defaultValue: orchUrl,
1333
+ placeholder: 'https://your-orch-instance.example.com',
1334
+ 'aria-label': 'Orchestration URL',
1335
+ onChange: function (e) { stateRef.orchUrl = e.target.value; }
1336
+ }),
1337
+ h('span', { className: 'ican-conn-label' }, 'API Key'),
1338
+ h('input', {
1339
+ className: 'ican-conn-input',
1340
+ type: 'password',
1341
+ defaultValue: apiKey,
1342
+ placeholder: 'your-api-key',
1343
+ 'aria-label': 'API Key',
1344
+ onChange: function (e) { stateRef.apiKey = e.target.value; }
1345
+ }),
1346
+ h('button', {
1347
+ className: 'ican-btn ican-btn-primary',
1348
+ type: 'button',
1349
+ style: { padding: '0 12px', height: 30, fontSize: 12 },
1350
+ onClick: function () { onSave(stateRef.orchUrl, stateRef.apiKey); }
1351
+ }, 'Save'),
1352
+ h('div', { className: 'ican-conn-status' },
1353
+ h('div', { className: 'ican-status-dot' + (orchUrl ? ' connected' : '') }),
1354
+ orchUrl ? 'Connected' : 'Not configured'
1355
+ )
1356
+ );
1357
+ }
1358
+
1359
+ /* ═══════════════════════════════════════════════════════════
1360
+ Main App
1361
+ ═══════════════════════════════════════════════════════════ */
1362
+ var App = (function () {
1363
+ function App(props) {
1364
+ React.Component.call(this, props);
1365
+ var storedTheme = getStoredTheme();
1366
+ this.state = {
1367
+ theme: storedTheme,
1368
+ themeDdOpen: false,
1369
+ panelOpen: false,
1370
+ registry: new Map(window.__ican_registry),
1371
+ addedIds: loadAddedWidgets(),
1372
+ orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1373
+ apiKey: localStorage.getItem(LS_API_KEY) || '',
1374
+ showConn: false,
1375
+ };
1376
+ this._onRegistryUpdate = this._onRegistryUpdate.bind(this);
1377
+ this._setTheme = this._setTheme.bind(this);
1378
+ this._openPanel = this._openPanel.bind(this);
1379
+ this._closePanel = this._closePanel.bind(this);
1380
+ this._addWidget = this._addWidget.bind(this);
1381
+ this._removeWidget = this._removeWidget.bind(this);
1382
+ this._saveConnection = this._saveConnection.bind(this);
1383
+ }
1384
+
1385
+ App.prototype = Object.create(React.Component.prototype);
1386
+ App.prototype.constructor = App;
1387
+
1388
+ App.prototype.componentDidMount = function () {
1389
+ window.addEventListener('ican:registry-updated', this._onRegistryUpdate);
1390
+ };
1391
+
1392
+ App.prototype.componentWillUnmount = function () {
1393
+ window.removeEventListener('ican:registry-updated', this._onRegistryUpdate);
1394
+ };
1395
+
1396
+ App.prototype._onRegistryUpdate = function () {
1397
+ this.setState({ registry: new Map(window.__ican_registry) });
1398
+ };
1399
+
1400
+ App.prototype._setTheme = function (key) {
1401
+ applyTheme(key);
1402
+ this.setState({ theme: key, themeDdOpen: false });
1403
+ };
1404
+
1405
+ App.prototype._openPanel = function () {
1406
+ this.setState({ panelOpen: true });
1407
+ };
1408
+
1409
+ App.prototype._closePanel = function () {
1410
+ this.setState({ panelOpen: false });
1411
+ };
1412
+
1413
+ App.prototype._addWidget = function (id) {
1414
+ var addedIds = this.state.addedIds.slice();
1415
+ if (addedIds.indexOf(id) === -1) {
1416
+ addedIds.push(id);
1417
+ saveAddedWidgets(addedIds);
1418
+ this.setState({ addedIds: addedIds, panelOpen: false });
1419
+ showToast('Widget added to dashboard', 'success');
1420
+ }
1421
+ };
1422
+
1423
+ App.prototype._removeWidget = function (id) {
1424
+ var addedIds = this.state.addedIds.filter(function (x) { return x !== id; });
1425
+ saveAddedWidgets(addedIds);
1426
+ this.setState({ addedIds: addedIds });
1427
+ showToast('Widget removed', 'info');
1428
+ };
1429
+
1430
+ App.prototype._saveConnection = function (orchUrl, apiKey) {
1431
+ localStorage.setItem(LS_ORCH_URL, orchUrl);
1432
+ localStorage.setItem(LS_API_KEY, apiKey);
1433
+ this.setState({ orchUrl: orchUrl, apiKey: apiKey });
1434
+ showToast('Connection settings saved', 'success');
1435
+ };
1436
+
1437
+ App.prototype.render = function () {
1438
+ var self = this;
1439
+ var s = this.state;
1440
+ var icanContext = makeICanContext(s.theme);
1441
+ var themeMeta = getThemeMeta(s.theme);
1442
+
1443
+ /* collect added widget configs */
1444
+ var addedConfigs = s.addedIds
1445
+ .map(function (id) { return s.registry.get(id); })
1446
+ .filter(Boolean);
1447
+
1448
+ /* unadded widgets (for the inline available state) */
1449
+ var unadded = [];
1450
+ s.registry.forEach(function (cfg, id) {
1451
+ if (s.addedIds.indexOf(id) === -1) unadded.push({ id: id, config: cfg });
1452
+ });
1453
+
1454
+ /* is canvas completely empty? */
1455
+ var isEmpty = addedConfigs.length === 0;
1456
+
1457
+ return h(React.Fragment, null,
1458
+
1459
+ /* ── Header ── */
1460
+ h('header', { className: 'ican-header' },
1461
+
1462
+ /* Brand */
1463
+ h('a', { className: 'ican-logo', href: '#', 'aria-label': 'InfAIra ICan home' },
1464
+ h('img', { src: './resources/infaira-icon.png', alt: 'InfAIra icon' }),
1465
+ h('span', { className: 'ican-logo-name' },
1466
+ 'Inf', h('em', null, 'AI'), 'ra'
1467
+ )
1468
+ ),
1469
+ h('div', { className: 'ican-sep' }),
1470
+ h('span', { className: 'ican-dev-badge' }, 'DEV'),
1471
+
1472
+ h('div', { className: 'ican-spacer' }),
1473
+
1474
+ /* Connection toggle */
1475
+ h('button', {
1476
+ className: 'ican-icon-btn',
1477
+ type: 'button',
1478
+ title: 'Connection settings',
1479
+ 'aria-label': 'Connection settings',
1480
+ onClick: function () { self.setState({ showConn: !s.showConn }); }
1481
+ }, h(SvgIcon, { svg: SVG.signal })),
1482
+
1483
+ /* Add Widget button (shown when there are both added + unadded) */
1484
+ addedConfigs.length > 0 && unadded.length > 0
1485
+ ? h('button', {
1486
+ className: 'ican-btn ican-btn-primary',
1487
+ type: 'button',
1488
+ 'aria-label': 'Add widget',
1489
+ onClick: self._openPanel
1490
+ },
1491
+ h(SvgIcon, { svg: SVG.plus }),
1492
+ 'Add Widget'
1493
+ )
1494
+ : null,
1495
+
1496
+ /* Theme switcher */
1497
+ h('div', { className: 'ican-theme-switcher' },
1498
+ /* transparent backdrop — closes dropdown without React stopPropagation issues */
1499
+ s.themeDdOpen
1500
+ ? h('div', {
1501
+ style: { position: 'fixed', inset: 0, zIndex: 499 },
1502
+ onClick: function () { self.setState({ themeDdOpen: false }); }
1503
+ })
1504
+ : null,
1505
+
1506
+ h('button', {
1507
+ className: 'ican-theme-btn',
1508
+ type: 'button',
1509
+ 'aria-label': 'Switch theme',
1510
+ 'aria-expanded': s.themeDdOpen,
1511
+ onClick: function () { self.setState({ themeDdOpen: !s.themeDdOpen }); }
1512
+ },
1513
+ h(SvgIcon, { svg: SVG.swatch }),
1514
+ themeMeta.label,
1515
+ h(SvgIcon, { svg: SVG.chevronDown, style: { opacity: 0.6 } })
1516
+ ),
1517
+
1518
+ s.themeDdOpen
1519
+ ? h('div', { className: 'ican-theme-dropdown', role: 'listbox', 'aria-label': 'Theme options' },
1520
+ THEMES.map(function (t) {
1521
+ var isActive = s.theme === t.key;
1522
+ return h('button', {
1523
+ key: t.key,
1524
+ className: 'ican-theme-option' + (isActive ? ' active' : ''),
1525
+ type: 'button',
1526
+ role: 'option',
1527
+ 'aria-selected': isActive,
1528
+ onClick: function () { self._setTheme(t.key); }
1529
+ },
1530
+ h('span', {
1531
+ className: 'ican-theme-swatch',
1532
+ style: {
1533
+ background: t.glass
1534
+ ? 'linear-gradient(135deg, ' + t.bg + ', ' + t.accent + ')'
1535
+ : 'linear-gradient(135deg, ' + t.mid + ', ' + t.accent + ')',
1536
+ border: '1px solid rgba(255,255,255,0.15)'
1537
+ }
1538
+ }),
1539
+ t.label,
1540
+ isActive
1541
+ ? h('span', { className: 'ican-theme-option-check' }, h(SvgIcon, { svg: SVG.check }))
1542
+ : null
1543
+ );
1544
+ })
1545
+ )
1546
+ : null
1547
+ ),
1548
+
1549
+ /* Settings */
1550
+ h('button', {
1551
+ className: 'ican-icon-btn',
1552
+ type: 'button',
1553
+ title: 'Settings',
1554
+ 'aria-label': 'Settings'
1555
+ }, h(SvgIcon, { svg: SVG.cog })),
1556
+
1557
+ /* Avatar */
1558
+ h('div', { className: 'ican-avatar', title: 'Dev User', 'aria-label': 'Dev user avatar' }, 'D')
1559
+ ),
1560
+
1561
+ /* ── Connection Bar (collapsible) ── */
1562
+ s.showConn
1563
+ ? h(ConnectionBar, {
1564
+ orchUrl: s.orchUrl,
1565
+ apiKey: s.apiKey,
1566
+ onSave: self._saveConnection
1567
+ })
1568
+ : null,
1569
+
1570
+ /* ── Main Content ── */
1571
+ h('main', {
1572
+ className: 'ican-main' + (s.showConn ? ' ican-main--with-conn' : '')
1573
+ },
1574
+ h('div', { className: 'ican-widget-grid' },
1575
+
1576
+ /* ─ State A: Nothing registered — waiting for npm run build ─ */
1577
+ isEmpty && s.registry.size === 0
1578
+ ? h('div', { className: 'ican-waiting' },
1579
+ h('div', { className: 'ican-waiting__icon' }, h(SvgIcon, { svg: SVG.terminal })),
1580
+ h('div', { className: 'ican-waiting__title' }, 'Waiting for your widget…'),
1581
+ h('div', { className: 'ican-waiting__sub' },
1582
+ 'Build your widget bundle to register it here. This page auto-refreshes when ',
1583
+ h('code', { style: { fontFamily: 'var(--ican-font-mono)', fontSize: 12 } }, 'dist/main.js'),
1584
+ ' is ready.'
1585
+ ),
1586
+ h('div', { className: 'ican-waiting__cmd' },
1587
+ h(SvgIcon, { svg: SVG.terminal, style: { opacity: 0.7 } }),
1588
+ h('span', null, 'npm run build')
1589
+ ),
1590
+ h('div', { className: 'ican-waiting__hint' }, 'Keep this tab open — widgets register automatically on load.')
1591
+ )
1592
+
1593
+ /* ─ State B: Widgets registered, none added yet ─ */
1594
+ : isEmpty && s.registry.size > 0
1595
+ ? h('div', { className: 'ican-available' },
1596
+ h('div', { className: 'ican-available__heading' },
1597
+ h('div', { className: 'ican-available__title' }, 'Widgets ready to add'),
1598
+ h('div', { className: 'ican-available__sub' }, 'Click a widget below to add it to your dashboard.')
1599
+ ),
1600
+ h('div', { className: 'ican-available__grid' },
1601
+ unadded.map(function (entry) {
1602
+ return h('div', { className: 'ican-available__card', key: entry.id },
1603
+ h('div', { className: 'ican-available__card-icon' }, h(SvgIcon, { svg: SVG.squares })),
1604
+ h('div', { className: 'ican-available__card-info' },
1605
+ h('div', { className: 'ican-available__card-name' }, entry.config.name || entry.id),
1606
+ h('div', { className: 'ican-available__card-id' }, entry.id)
1607
+ ),
1608
+ h('button', {
1609
+ className: 'ican-btn ican-btn-primary',
1610
+ type: 'button',
1611
+ onClick: function () { self._addWidget(entry.id); },
1612
+ 'aria-label': 'Add ' + (entry.config.name || entry.id),
1613
+ style: { padding: '0 12px', height: 32, fontSize: 12 }
1614
+ }, h(SvgIcon, { svg: SVG.plus }), ' Add')
1615
+ );
1616
+ })
1617
+ )
1618
+ )
1619
+
1620
+ /* ─ State C: Widgets on canvas ─ */
1621
+ : addedConfigs.map(function (cfg) {
1622
+ return h(WidgetCard, {
1623
+ key: cfg.id,
1624
+ config: cfg,
1625
+ icanContext: icanContext,
1626
+ onRemove: self._removeWidget
1627
+ });
1628
+ })
1629
+ )
1630
+ ),
1631
+
1632
+ /* ── Add Widget Panel ── */
1633
+ h(AddWidgetPanel, {
1634
+ open: s.panelOpen,
1635
+ onClose: self._closePanel,
1636
+ registry: s.registry,
1637
+ addedIds: s.addedIds,
1638
+ onAdd: self._addWidget
1639
+ })
1640
+ );
1641
+ };
1642
+
1643
+ return App;
1644
+ }());
1645
+
1646
+ /* ─── Load widget bundle then mount ─────────────────────── */
1647
+ function mountApp() {
1648
+ ReactDOM.render(h(App, null), document.getElementById('app'));
1649
+ }
1650
+
1651
+ function loadWidgetScript(callback) {
1652
+ var script = document.createElement('script');
1653
+ script.src = './dist/main.js?t=' + Date.now();
1654
+ script.onload = function () {
1655
+ console.log('[ICan Dev] dist/main.js loaded successfully');
1656
+ callback();
1657
+ };
1658
+ script.onerror = function () {
1659
+ console.warn('[ICan Dev] dist/main.js not found. Run `npm run build` in your widget folder.');
1660
+ callback();
1661
+ };
1662
+ document.head.appendChild(script);
1663
+ }
1664
+
1665
+ loadWidgetScript(mountApp);
1666
+
1667
+ }());
1668
+ </script>
1669
+ </body>
1670
+ </html>