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,2225 @@
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>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-grid-layout CSS -->
17
+ <link rel="stylesheet" href="https://unpkg.com/react-grid-layout@1.3.4/css/styles.css" />
18
+ <link rel="stylesheet" href="https://unpkg.com/react-resizable@3.0.5/css/styles.css" />
19
+
20
+ <!-- React 16 -->
21
+ <script src="https://unpkg.com/react@16.14.0/umd/react.production.min.js"></script>
22
+ <script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.production.min.js"></script>
23
+
24
+ <!-- react-grid-layout -->
25
+ <script src="https://unpkg.com/react-grid-layout@1.3.4/dist/react-grid-layout.min.js"></script>
26
+
27
+ <!-- ICan Components -->
28
+ <script src="./resources/ican-components.js"></script>
29
+
30
+ <style>
31
+ /* ═══════════════════════════════════════════════════════
32
+ ICan Dev Harness — Theme Variables
33
+ Dark (default) | Light | Glass Dark | Glass Light
34
+ ═══════════════════════════════════════════════════════ */
35
+
36
+ /* ── Dark Theme (default) ─────────────────────────────── */
37
+ :root {
38
+ --ican-bg: #09090b;
39
+ --ican-header-bg: #0f0f12;
40
+ --ican-primary-bg: #141417;
41
+ --ican-secondary-bg: #1a1a1f;
42
+ --ican-primary-text: #fafafa;
43
+ --ican-secondary-text: #a1a1aa;
44
+ --ican-tertiary-text: #52525b;
45
+ --ican-border: rgba(255, 255, 255, 0.06);
46
+ --ican-border-bright: rgba(255, 255, 255, 0.12);
47
+ --ican-border-focus: #7c6aff;
48
+ --ican-accent: #7c6aff;
49
+ --ican-accent-hover: #6b59e8;
50
+ --ican-accent-text: #a99aff;
51
+ --ican-accent-dim: rgba(124, 106, 255, 0.08);
52
+ --ican-error: #f87171;
53
+ --ican-success: #4ade80;
54
+ --ican-warning: #fbbf24;
55
+ --ican-info: #60a5fa;
56
+ --ican-success-dim: rgba(74, 222, 128, 0.08);
57
+ --ican-error-dim: rgba(248, 113, 113, 0.08);
58
+ --ican-warning-dim: rgba(251, 191, 36, 0.08);
59
+ --ican-card-bg: #141417;
60
+ --ican-card-shadow: 0 1px 2px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.03);
61
+ --ican-card-hover-shadow: 0 8px 30px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.06);
62
+ --ican-input-bg: #1a1a1f;
63
+ --ican-hover: rgba(255, 255, 255, 0.05);
64
+ --ican-modal-bg: #111114;
65
+ --ican-modal-shadow: 0 25px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05);
66
+ --ican-btn-primary-bg: #7c6aff;
67
+ --ican-btn-primary-text: #ffffff;
68
+ --ican-btn-secondary-bg: #1a1a1f;
69
+ --ican-btn-secondary-text: #fafafa;
70
+ --ican-scrollbar-thumb: rgba(255, 255, 255, 0.10);
71
+ --ican-backdrop-filter: none;
72
+ --ican-glass-border: rgba(255, 255, 255, 0.10);
73
+ --ican-font-brand: 'Comfortaa', cursive;
74
+ --ican-font-body: 'Inter', system-ui, -apple-system, sans-serif;
75
+ --ican-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
76
+ --ican-radius-xs: 4px;
77
+ --ican-radius-sm: 6px;
78
+ --ican-radius-md: 10px;
79
+ --ican-radius-lg: 16px;
80
+ --ican-transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1);
81
+ --ican-transition-normal: 220ms cubic-bezier(0.4, 0, 0.2, 1);
82
+ }
83
+
84
+ /* ── Light Theme ──────────────────────────────────────── */
85
+ [data-theme='light'] {
86
+ --ican-bg: #f5f5f7;
87
+ --ican-header-bg: rgba(255, 255, 255, 0.85);
88
+ --ican-primary-bg: #ffffff;
89
+ --ican-secondary-bg: #f0f0f4;
90
+ --ican-primary-text: #1d1d1f;
91
+ --ican-secondary-text: #6e6e80;
92
+ --ican-tertiary-text: #aeaeb8;
93
+ --ican-border: rgba(0, 0, 0, 0.06);
94
+ --ican-border-bright: rgba(0, 0, 0, 0.11);
95
+ --ican-border-focus: #5b4cd9;
96
+ --ican-accent: #5b4cd9;
97
+ --ican-accent-hover: #4a3cb5;
98
+ --ican-accent-text: #4a3cb5;
99
+ --ican-accent-dim: rgba(91, 76, 217, 0.07);
100
+ --ican-error: #dc2626;
101
+ --ican-success: #16a34a;
102
+ --ican-warning: #d97706;
103
+ --ican-info: #2563eb;
104
+ --ican-success-dim: rgba(22, 163, 74, 0.07);
105
+ --ican-error-dim: rgba(220, 38, 38, 0.07);
106
+ --ican-warning-dim: rgba(217, 119, 6, 0.07);
107
+ --ican-card-bg: #ffffff;
108
+ --ican-card-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.03);
109
+ --ican-card-hover-shadow: 0 10px 30px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.04);
110
+ --ican-input-bg: #f5f5f7;
111
+ --ican-hover: rgba(0, 0, 0, 0.035);
112
+ --ican-modal-bg: #ffffff;
113
+ --ican-modal-shadow: 0 25px 65px rgba(0,0,0,0.12), 0 8px 20px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04);
114
+ --ican-btn-primary-bg: #5b4cd9;
115
+ --ican-btn-primary-text: #ffffff;
116
+ --ican-btn-secondary-bg: #f0f0f5;
117
+ --ican-btn-secondary-text: #1d1d1f;
118
+ --ican-scrollbar-thumb: rgba(0, 0, 0, 0.14);
119
+ --ican-backdrop-filter: blur(20px) saturate(180%);
120
+ --ican-glass-border: rgba(0, 0, 0, 0.06);
121
+ }
122
+
123
+ /* ── Glass Dark Theme ─────────────────────────────────── */
124
+ [data-theme='glass-dark'] {
125
+ --ican-bg: transparent;
126
+ --ican-header-bg: rgba(255, 255, 255, 0.04);
127
+ --ican-primary-bg: rgba(255, 255, 255, 0.06);
128
+ --ican-secondary-bg: rgba(255, 255, 255, 0.05);
129
+ --ican-primary-text: rgba(255, 255, 255, 0.95);
130
+ --ican-secondary-text: rgba(220, 220, 245, 0.65);
131
+ --ican-tertiary-text: rgba(180, 180, 220, 0.40);
132
+ --ican-border: rgba(255, 255, 255, 0.13);
133
+ --ican-border-bright: rgba(255, 255, 255, 0.22);
134
+ --ican-border-focus: rgba(200, 190, 255, 0.90);
135
+ --ican-accent: #b4a8ff;
136
+ --ican-accent-hover: #a099ff;
137
+ --ican-accent-text: rgba(200, 190, 255, 0.95);
138
+ --ican-accent-dim: rgba(180, 168, 255, 0.14);
139
+ --ican-error: #fca5a5;
140
+ --ican-success: #86efac;
141
+ --ican-warning: #fcd34d;
142
+ --ican-info: #7dd3fc;
143
+ --ican-success-dim: rgba(134, 239, 172, 0.12);
144
+ --ican-error-dim: rgba(252, 165, 165, 0.12);
145
+ --ican-warning-dim: rgba(252, 211, 77, 0.12);
146
+ --ican-card-bg: rgba(255, 255, 255, 0.08);
147
+ --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);
148
+ --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);
149
+ --ican-input-bg: rgba(255, 255, 255, 0.08);
150
+ --ican-hover: rgba(255, 255, 255, 0.08);
151
+ --ican-modal-bg: rgba(15, 10, 40, 0.72);
152
+ --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);
153
+ --ican-btn-primary-bg: rgba(140, 120, 255, 0.75);
154
+ --ican-btn-primary-text: #ffffff;
155
+ --ican-btn-secondary-bg: rgba(255, 255, 255, 0.10);
156
+ --ican-btn-secondary-text: rgba(255, 255, 255, 0.90);
157
+ --ican-scrollbar-thumb: rgba(255, 255, 255, 0.18);
158
+ --ican-backdrop-filter: blur(24px) saturate(200%) brightness(1.12);
159
+ --ican-glass-border: rgba(255, 255, 255, 0.18);
160
+ }
161
+
162
+ /* ── Glass Light Theme ────────────────────────────────── */
163
+ [data-theme='glass-light'] {
164
+ --ican-bg: transparent;
165
+ --ican-header-bg: rgba(255, 255, 255, 0.30);
166
+ --ican-primary-bg: rgba(255, 255, 255, 0.42);
167
+ --ican-secondary-bg: rgba(255, 255, 255, 0.35);
168
+ --ican-primary-text: rgba(20, 20, 40, 0.92);
169
+ --ican-secondary-text: rgba(60, 60, 100, 0.62);
170
+ --ican-tertiary-text: rgba(100, 100, 140, 0.45);
171
+ --ican-border: rgba(255, 255, 255, 0.55);
172
+ --ican-border-bright: rgba(255, 255, 255, 0.70);
173
+ --ican-border-focus: rgba(90, 70, 220, 0.80);
174
+ --ican-accent: #5b4cd9;
175
+ --ican-accent-hover: #4a3cb5;
176
+ --ican-accent-text: #4a3cb5;
177
+ --ican-accent-dim: rgba(91, 76, 217, 0.10);
178
+ --ican-error: #dc2626;
179
+ --ican-success: #16a34a;
180
+ --ican-warning: #d97706;
181
+ --ican-info: #2563eb;
182
+ --ican-success-dim: rgba(22, 163, 74, 0.10);
183
+ --ican-error-dim: rgba(220, 38, 38, 0.10);
184
+ --ican-warning-dim: rgba(217, 119, 6, 0.10);
185
+ --ican-card-bg: rgba(255, 255, 255, 0.48);
186
+ --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);
187
+ --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);
188
+ --ican-input-bg: rgba(255, 255, 255, 0.50);
189
+ --ican-hover: rgba(255, 255, 255, 0.30);
190
+ --ican-modal-bg: rgba(255, 255, 255, 0.72);
191
+ --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);
192
+ --ican-btn-primary-bg: rgba(91, 76, 217, 0.85);
193
+ --ican-btn-primary-text: #ffffff;
194
+ --ican-btn-secondary-bg: rgba(255, 255, 255, 0.55);
195
+ --ican-btn-secondary-text: rgba(30, 30, 60, 0.85);
196
+ --ican-scrollbar-thumb: rgba(0, 0, 0, 0.12);
197
+ --ican-backdrop-filter: blur(22px) saturate(190%) brightness(1.08);
198
+ --ican-glass-border: rgba(255, 255, 255, 0.50);
199
+ }
200
+
201
+ /* ── Glass Dark: vivid deep purple background ─────────── */
202
+ [data-theme='glass-dark'] body {
203
+ background:
204
+ radial-gradient(ellipse 70% 60% at 5% 10%, rgba(100, 70, 200, 0.40) 0%, transparent 70%),
205
+ radial-gradient(ellipse 80% 50% at 80% 5%, rgba(50, 100, 200, 0.30) 0%, transparent 65%),
206
+ radial-gradient(ellipse 60% 50% at 5% 90%, rgba(80, 50, 180, 0.35) 0%, transparent 60%),
207
+ radial-gradient(ellipse 65% 55% at 90% 85%, rgba(40, 80, 160, 0.25) 0%, transparent 60%),
208
+ #08061a;
209
+ }
210
+
211
+ /* ── Glass Light: bright pastel sky background ────────── */
212
+ [data-theme='glass-light'] body {
213
+ background:
214
+ radial-gradient(ellipse 55% 50% at 0% 15%, rgba(160, 130, 255, 0.65) 0%, transparent 70%),
215
+ radial-gradient(ellipse 90% 45% at 55% 0%, rgba(100, 180, 255, 0.75) 0%, transparent 65%),
216
+ radial-gradient(ellipse 45% 55% at 0% 60%, rgba(255, 120, 180, 0.55) 0%, transparent 65%),
217
+ radial-gradient(ellipse 55% 50% at 85% 50%, rgba(80, 220, 220, 0.40) 0%, transparent 60%),
218
+ radial-gradient(ellipse 50% 40% at 5% 95%, rgba(255, 170, 120, 0.50) 0%, transparent 60%),
219
+ radial-gradient(ellipse 60% 50% at 90% 90%, rgba(255, 140, 200, 0.45) 0%, transparent 55%),
220
+ radial-gradient(ellipse 50% 40% at 45% 50%, rgba(255, 200, 120, 0.20) 0%, transparent 55%),
221
+ #d0e4ff;
222
+ }
223
+
224
+ /* ── Glass noise grain overlay ────────────────────────── */
225
+ [data-theme='glass-dark'] body::before,
226
+ [data-theme='glass-light'] body::before {
227
+ content: '';
228
+ position: fixed;
229
+ inset: 0;
230
+ z-index: 0;
231
+ pointer-events: none;
232
+ opacity: 0.04;
233
+ 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");
234
+ background-repeat: repeat;
235
+ background-size: 180px 180px;
236
+ }
237
+ [data-theme='glass-light'] body::before { opacity: 0.035; }
238
+
239
+ /* ── Glass: app layer above noise ─────────────────────── */
240
+ [data-theme='glass-dark'] #app,
241
+ [data-theme='glass-light'] #app { position: relative; z-index: 1; }
242
+
243
+ /* ── Glass: frosted header ────────────────────────────── */
244
+ [data-theme='glass-dark'] .ican-header,
245
+ [data-theme='glass-light'] .ican-header {
246
+ backdrop-filter: var(--ican-backdrop-filter);
247
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
248
+ border-color: var(--ican-glass-border);
249
+ box-shadow: inset 0 -1px 0 var(--ican-glass-border);
250
+ }
251
+
252
+ /* ── Glass: frosted toolbar ───────────────────────────── */
253
+ [data-theme='glass-dark'] .ican-toolbar,
254
+ [data-theme='glass-light'] .ican-toolbar {
255
+ backdrop-filter: var(--ican-backdrop-filter);
256
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
257
+ border-color: var(--ican-glass-border);
258
+ }
259
+
260
+ /* ── Glass: widget card — nearly transparent so gradient bleeds through,
261
+ matching portal's .widget-cell approach. The widget's own internal
262
+ cards (glass-card mixin) apply the real frost effect on top. ──── */
263
+ [data-theme='glass-light'] .ican-widget-card {
264
+ background: rgba(255, 255, 255, 0.06) !important;
265
+ backdrop-filter: blur(2px);
266
+ -webkit-backdrop-filter: blur(2px);
267
+ border-color: rgba(255, 255, 255, 0.35);
268
+ box-shadow: 0 4px 24px rgba(100, 120, 200, 0.08), 0 0 0 1px rgba(255, 255, 255, 0.28);
269
+ }
270
+
271
+ [data-theme='glass-dark'] .ican-widget-card {
272
+ background: rgba(255, 255, 255, 0.03) !important;
273
+ backdrop-filter: blur(2px);
274
+ -webkit-backdrop-filter: blur(2px);
275
+ border-color: rgba(255, 255, 255, 0.10);
276
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.20), 0 0 0 1px rgba(255, 255, 255, 0.08);
277
+ }
278
+
279
+ /* ── Glass: frosted modals & panels ───────────────────── */
280
+ [data-theme='glass-dark'] .ican-panel,
281
+ [data-theme='glass-light'] .ican-panel {
282
+ backdrop-filter: blur(48px) saturate(210%);
283
+ -webkit-backdrop-filter: blur(48px) saturate(210%);
284
+ border-color: var(--ican-glass-border);
285
+ }
286
+
287
+ [data-theme='glass-dark'] .ican-theme-dropdown,
288
+ [data-theme='glass-light'] .ican-theme-dropdown {
289
+ backdrop-filter: blur(48px) saturate(210%);
290
+ -webkit-backdrop-filter: blur(48px) saturate(210%);
291
+ }
292
+
293
+ /* ── Glass: frosted connection panel ──────────────────── */
294
+ [data-theme='glass-dark'] .ican-conn-panel,
295
+ [data-theme='glass-light'] .ican-conn-panel {
296
+ backdrop-filter: var(--ican-backdrop-filter);
297
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
298
+ border-color: var(--ican-glass-border);
299
+ }
300
+
301
+ /* ── Light: frosted header ────────────────────────────── */
302
+ [data-theme='light'] .ican-header {
303
+ backdrop-filter: blur(20px) saturate(180%);
304
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
305
+ }
306
+
307
+ /* ═══════════════════════════════════════════════════════
308
+ Reset & Base
309
+ ═══════════════════════════════════════════════════════ */
310
+ *, *::before, *::after {
311
+ box-sizing: border-box;
312
+ margin: 0;
313
+ padding: 0;
314
+ transition-property: color, background-color, border-color, box-shadow, opacity, transform;
315
+ transition-duration: 150ms;
316
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
317
+ }
318
+
319
+ html {
320
+ font-size: 14px;
321
+ -webkit-font-smoothing: antialiased;
322
+ -moz-osx-font-smoothing: grayscale;
323
+ }
324
+
325
+ html, body { height: 100%; }
326
+
327
+ body {
328
+ font-family: var(--ican-font-body);
329
+ color: var(--ican-primary-text);
330
+ background-color: var(--ican-bg);
331
+ line-height: 1.5;
332
+ }
333
+
334
+ *:focus-visible {
335
+ outline: 2px solid var(--ican-border-focus);
336
+ outline-offset: 2px;
337
+ }
338
+
339
+ * {
340
+ scrollbar-width: thin;
341
+ scrollbar-color: var(--ican-scrollbar-thumb) transparent;
342
+ }
343
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
344
+ ::-webkit-scrollbar-thumb { background: var(--ican-scrollbar-thumb); border-radius: 3px; }
345
+
346
+ /* ═══════════════════════════════════════════════════════
347
+ Layout Shell
348
+ ═══════════════════════════════════════════════════════ */
349
+ #app {
350
+ display: flex;
351
+ flex-direction: column;
352
+ height: 100vh;
353
+ overflow: hidden;
354
+ }
355
+
356
+ /* ═══════════════════════════════════════════════════════
357
+ Header
358
+ ═══════════════════════════════════════════════════════ */
359
+ .ican-header {
360
+ position: fixed;
361
+ top: 0; left: 0; right: 0;
362
+ z-index: 600; /* must be above theme dropdown backdrop (499) */
363
+ height: 56px;
364
+ background: var(--ican-header-bg);
365
+ border-bottom: 1px solid var(--ican-border);
366
+ display: flex;
367
+ align-items: center;
368
+ padding: 0 18px;
369
+ gap: 10px;
370
+ flex-shrink: 0;
371
+ }
372
+
373
+ .ican-logo {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 9px;
377
+ text-decoration: none;
378
+ flex-shrink: 0;
379
+ }
380
+
381
+ .ican-logo img {
382
+ height: 28px;
383
+ width: auto;
384
+ }
385
+
386
+ .ican-logo-name {
387
+ font-family: var(--ican-font-brand);
388
+ font-size: 17px;
389
+ font-weight: 600;
390
+ color: var(--ican-primary-text);
391
+ letter-spacing: -0.3px;
392
+ }
393
+
394
+ .ican-logo-name em {
395
+ font-style: normal;
396
+ color: var(--ican-accent);
397
+ }
398
+
399
+ .ican-sep {
400
+ width: 1px;
401
+ height: 22px;
402
+ background: var(--ican-border-bright);
403
+ flex-shrink: 0;
404
+ }
405
+
406
+ .ican-dev-badge {
407
+ display: inline-flex;
408
+ align-items: center;
409
+ padding: 2px 8px;
410
+ background: var(--ican-accent-dim);
411
+ color: var(--ican-accent-text);
412
+ border-radius: var(--ican-radius-xs);
413
+ border: 1px solid rgba(124, 106, 255, 0.20);
414
+ font-size: 10px;
415
+ font-weight: 700;
416
+ font-family: var(--ican-font-mono);
417
+ letter-spacing: 1px;
418
+ text-transform: uppercase;
419
+ flex-shrink: 0;
420
+ }
421
+
422
+ [data-theme='light'] .ican-dev-badge { border-color: rgba(91, 76, 217, 0.20); }
423
+ [data-theme='glass-dark'] .ican-dev-badge { border-color: rgba(200, 190, 255, 0.25); }
424
+ [data-theme='glass-light'] .ican-dev-badge { border-color: rgba(91, 76, 217, 0.25); }
425
+
426
+ .ican-header-spacer { flex: 1; }
427
+
428
+ /* ═══════════════════════════════════════════════════════
429
+ Icon Button
430
+ ═══════════════════════════════════════════════════════ */
431
+ .ican-icon-btn {
432
+ display: inline-flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ width: 34px;
436
+ height: 34px;
437
+ border: 1px solid var(--ican-border-bright);
438
+ border-radius: var(--ican-radius-sm);
439
+ background: transparent;
440
+ color: var(--ican-secondary-text);
441
+ cursor: pointer;
442
+ flex-shrink: 0;
443
+ }
444
+
445
+ .ican-icon-btn:hover {
446
+ background: var(--ican-hover);
447
+ color: var(--ican-primary-text);
448
+ }
449
+
450
+ .ican-icon-btn.active {
451
+ background: var(--ican-accent-dim);
452
+ color: var(--ican-accent);
453
+ border-color: rgba(124, 106, 255, 0.25);
454
+ }
455
+
456
+ [data-theme='light'] .ican-icon-btn.active { border-color: rgba(91, 76, 217, 0.25); }
457
+
458
+ /* ═══════════════════════════════════════════════════════
459
+ Avatar
460
+ ═══════════════════════════════════════════════════════ */
461
+ .ican-avatar {
462
+ width: 32px;
463
+ height: 32px;
464
+ border-radius: var(--ican-radius-sm);
465
+ background: linear-gradient(135deg, var(--ican-accent), rgba(168, 85, 247, 0.85));
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ color: #fff;
470
+ font-size: 12px;
471
+ font-weight: 700;
472
+ font-family: var(--ican-font-brand);
473
+ flex-shrink: 0;
474
+ cursor: default;
475
+ user-select: none;
476
+ }
477
+
478
+ /* ═══════════════════════════════════════════════════════
479
+ Theme Switcher
480
+ ═══════════════════════════════════════════════════════ */
481
+ .ican-theme-wrapper {
482
+ position: relative;
483
+ flex-shrink: 0;
484
+ }
485
+
486
+ .ican-theme-btn {
487
+ display: inline-flex;
488
+ align-items: center;
489
+ gap: 7px;
490
+ padding: 0 11px;
491
+ height: 34px;
492
+ border: 1px solid var(--ican-border-bright);
493
+ border-radius: var(--ican-radius-sm);
494
+ background: transparent;
495
+ color: var(--ican-secondary-text);
496
+ cursor: pointer;
497
+ font-size: 12px;
498
+ font-family: var(--ican-font-body);
499
+ font-weight: 500;
500
+ white-space: nowrap;
501
+ }
502
+
503
+ .ican-theme-btn:hover {
504
+ background: var(--ican-hover);
505
+ color: var(--ican-primary-text);
506
+ }
507
+
508
+ .ican-theme-btn-swatch {
509
+ width: 14px;
510
+ height: 14px;
511
+ border-radius: 4px;
512
+ flex-shrink: 0;
513
+ border: 1px solid rgba(255, 255, 255, 0.15);
514
+ }
515
+
516
+ .ican-theme-chevron {
517
+ display: inline-block;
518
+ transition: transform 150ms ease;
519
+ }
520
+ .ican-theme-chevron.open { transform: rotate(180deg); }
521
+
522
+ .ican-theme-dropdown {
523
+ position: absolute;
524
+ top: calc(100% + 6px);
525
+ right: 0;
526
+ min-width: 226px;
527
+ background: var(--ican-modal-bg);
528
+ border: 1px solid var(--ican-border-bright);
529
+ border-radius: var(--ican-radius-lg);
530
+ box-shadow: var(--ican-modal-shadow);
531
+ padding: 6px;
532
+ z-index: 500;
533
+ opacity: 0;
534
+ pointer-events: none;
535
+ transform: scale(0.96) translateY(-4px);
536
+ transition: opacity 180ms ease, transform 180ms ease;
537
+ transform-origin: top right;
538
+ }
539
+
540
+ .ican-theme-dropdown.open {
541
+ opacity: 1;
542
+ pointer-events: all;
543
+ transform: scale(1) translateY(0);
544
+ }
545
+
546
+ .ican-theme-dd-label {
547
+ padding: 6px 10px 8px;
548
+ font-size: 10px;
549
+ font-weight: 700;
550
+ letter-spacing: 0.8px;
551
+ text-transform: uppercase;
552
+ color: var(--ican-tertiary-text);
553
+ font-family: var(--ican-font-mono);
554
+ }
555
+
556
+ .ican-theme-option {
557
+ display: flex;
558
+ align-items: center;
559
+ gap: 10px;
560
+ padding: 8px 10px;
561
+ border-radius: var(--ican-radius-md);
562
+ cursor: pointer;
563
+ border: 1px solid transparent;
564
+ background: transparent;
565
+ width: 100%;
566
+ text-align: left;
567
+ transition: background 120ms;
568
+ }
569
+
570
+ .ican-theme-option:hover { background: var(--ican-hover); }
571
+
572
+ .ican-theme-option.selected {
573
+ background: var(--ican-accent-dim);
574
+ border-color: rgba(124, 106, 255, 0.16);
575
+ }
576
+ [data-theme='light'] .ican-theme-option.selected { border-color: rgba(91, 76, 217, 0.16); }
577
+ [data-theme='glass-light'] .ican-theme-option.selected { border-color: rgba(91, 76, 217, 0.20); }
578
+
579
+ .ican-theme-option-swatch {
580
+ position: relative;
581
+ width: 38px;
582
+ height: 28px;
583
+ border-radius: 7px;
584
+ flex-shrink: 0;
585
+ overflow: hidden;
586
+ border: 1px solid rgba(255, 255, 255, 0.12);
587
+ }
588
+
589
+ .ican-theme-option-dot {
590
+ position: absolute;
591
+ bottom: 4px;
592
+ right: 4px;
593
+ width: 7px;
594
+ height: 7px;
595
+ border-radius: 50%;
596
+ }
597
+
598
+ .ican-theme-option-info { flex: 1; min-width: 0; }
599
+
600
+ .ican-theme-option-name {
601
+ font-size: 13px;
602
+ font-weight: 500;
603
+ color: var(--ican-primary-text);
604
+ }
605
+
606
+ .ican-theme-option-desc {
607
+ font-size: 11px;
608
+ color: var(--ican-tertiary-text);
609
+ margin-top: 1px;
610
+ }
611
+
612
+ .ican-theme-option-check {
613
+ color: var(--ican-accent);
614
+ flex-shrink: 0;
615
+ }
616
+
617
+ /* ═══════════════════════════════════════════════════════
618
+ Toolbar
619
+ ═══════════════════════════════════════════════════════ */
620
+ .ican-toolbar {
621
+ position: fixed;
622
+ top: 56px;
623
+ left: 0; right: 0;
624
+ z-index: 99;
625
+ height: 48px;
626
+ background: var(--ican-secondary-bg);
627
+ border-bottom: 1px solid var(--ican-border);
628
+ display: flex;
629
+ align-items: center;
630
+ padding: 0 18px;
631
+ gap: 8px;
632
+ }
633
+
634
+ .ican-toolbar-title {
635
+ font-family: var(--ican-font-brand);
636
+ font-size: 14px;
637
+ font-weight: 600;
638
+ color: var(--ican-primary-text);
639
+ }
640
+
641
+ .ican-toolbar-count {
642
+ font-size: 11px;
643
+ font-weight: 500;
644
+ color: var(--ican-tertiary-text);
645
+ font-family: var(--ican-font-mono);
646
+ padding: 1px 7px;
647
+ background: var(--ican-border);
648
+ border-radius: 99px;
649
+ }
650
+
651
+ .ican-toolbar-spacer { flex: 1; }
652
+
653
+ /* ═══════════════════════════════════════════════════════
654
+ Buttons
655
+ ═══════════════════════════════════════════════════════ */
656
+ .ican-btn {
657
+ display: inline-flex;
658
+ align-items: center;
659
+ gap: 6px;
660
+ padding: 0 13px;
661
+ height: 32px;
662
+ border-radius: var(--ican-radius-sm);
663
+ font-size: 13px;
664
+ font-weight: 500;
665
+ font-family: var(--ican-font-body);
666
+ cursor: pointer;
667
+ border: 1px solid transparent;
668
+ flex-shrink: 0;
669
+ white-space: nowrap;
670
+ user-select: none;
671
+ }
672
+
673
+ .ican-btn-primary {
674
+ background: var(--ican-btn-primary-bg);
675
+ color: var(--ican-btn-primary-text);
676
+ }
677
+ .ican-btn-primary:hover { opacity: 0.88; }
678
+
679
+ .ican-btn-secondary {
680
+ background: var(--ican-btn-secondary-bg);
681
+ color: var(--ican-btn-secondary-text);
682
+ border-color: var(--ican-border-bright);
683
+ }
684
+ .ican-btn-secondary:hover { background: var(--ican-hover); }
685
+
686
+ .ican-btn-secondary.active {
687
+ background: var(--ican-accent-dim);
688
+ color: var(--ican-accent);
689
+ border-color: rgba(124, 106, 255, 0.25);
690
+ }
691
+ [data-theme='light'] .ican-btn-secondary.active { border-color: rgba(91, 76, 217, 0.25); }
692
+
693
+ /* ═══════════════════════════════════════════════════════
694
+ Connection Panel
695
+ ═══════════════════════════════════════════════════════ */
696
+ .ican-conn-panel {
697
+ position: fixed;
698
+ top: 56px;
699
+ left: 0; right: 0;
700
+ z-index: 98;
701
+ height: 64px;
702
+ background: var(--ican-primary-bg);
703
+ border-bottom: 1px solid var(--ican-border);
704
+ display: flex;
705
+ align-items: center;
706
+ padding: 0 18px;
707
+ gap: 10px;
708
+ overflow: hidden;
709
+ }
710
+
711
+ .ican-conn-field {
712
+ display: flex;
713
+ flex-direction: column;
714
+ gap: 3px;
715
+ flex: 1;
716
+ min-width: 0;
717
+ }
718
+
719
+ .ican-conn-label {
720
+ font-size: 9px;
721
+ font-weight: 700;
722
+ text-transform: uppercase;
723
+ letter-spacing: 0.8px;
724
+ color: var(--ican-tertiary-text);
725
+ font-family: var(--ican-font-mono);
726
+ }
727
+
728
+ .ican-conn-input {
729
+ height: 30px;
730
+ padding: 0 10px;
731
+ border: 1px solid var(--ican-border-bright);
732
+ border-radius: var(--ican-radius-sm);
733
+ background: var(--ican-input-bg);
734
+ color: var(--ican-primary-text);
735
+ font-size: 12px;
736
+ font-family: var(--ican-font-mono);
737
+ width: 100%;
738
+ }
739
+
740
+ .ican-conn-input:focus {
741
+ border-color: var(--ican-border-focus);
742
+ outline: none;
743
+ box-shadow: 0 0 0 3px rgba(124, 106, 255, 0.10);
744
+ }
745
+
746
+ .ican-conn-input::placeholder { color: var(--ican-tertiary-text); }
747
+
748
+ .ican-conn-actions {
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 8px;
752
+ flex-shrink: 0;
753
+ }
754
+
755
+ .ican-conn-status {
756
+ display: inline-flex;
757
+ align-items: center;
758
+ gap: 5px;
759
+ font-size: 11px;
760
+ color: var(--ican-secondary-text);
761
+ font-family: var(--ican-font-mono);
762
+ white-space: nowrap;
763
+ }
764
+
765
+ .ican-conn-dot {
766
+ width: 6px;
767
+ height: 6px;
768
+ border-radius: 50%;
769
+ background: var(--ican-tertiary-text);
770
+ flex-shrink: 0;
771
+ }
772
+
773
+ .ican-conn-dot.connected {
774
+ background: var(--ican-success);
775
+ box-shadow: 0 0 6px rgba(74, 222, 128, 0.45);
776
+ }
777
+
778
+ /* ═══════════════════════════════════════════════════════
779
+ Main Content
780
+ ═══════════════════════════════════════════════════════ */
781
+ .ican-main {
782
+ margin-top: 104px;
783
+ flex: 1;
784
+ overflow-y: auto;
785
+ padding: 20px 18px;
786
+ }
787
+
788
+ /* ═══════════════════════════════════════════════════════
789
+ Widget Grid
790
+ ═══════════════════════════════════════════════════════ */
791
+ .ican-widget-grid { position: relative; }
792
+
793
+ .ican-edit-mode .react-grid-item.react-grid-placeholder {
794
+ background: var(--ican-accent);
795
+ opacity: 0.12;
796
+ border-radius: var(--ican-radius-md);
797
+ }
798
+
799
+ .ican-edit-mode .react-grid-item { cursor: move; }
800
+ .ican-edit-mode .react-resizable-handle { opacity: 1 !important; }
801
+ .react-resizable-handle {
802
+ opacity: 0;
803
+ transition: opacity 0.15s;
804
+ }
805
+
806
+ /* ═══════════════════════════════════════════════════════
807
+ Widget Card
808
+ ═══════════════════════════════════════════════════════ */
809
+ .ican-widget-card {
810
+ background: var(--ican-card-bg);
811
+ border: 1px solid var(--ican-border);
812
+ border-radius: var(--ican-radius-md);
813
+ box-shadow: var(--ican-card-shadow);
814
+ display: flex;
815
+ flex-direction: column;
816
+ overflow: hidden;
817
+ height: 100%;
818
+ }
819
+
820
+ .ican-widget-card:hover { box-shadow: var(--ican-card-hover-shadow); }
821
+
822
+ .ican-widget-card-header {
823
+ display: flex;
824
+ align-items: center;
825
+ padding: 0 10px;
826
+ height: 40px;
827
+ border-bottom: 1px solid var(--ican-border);
828
+ gap: 6px;
829
+ flex-shrink: 0;
830
+ }
831
+
832
+ .ican-drag-handle {
833
+ color: var(--ican-tertiary-text);
834
+ display: flex;
835
+ align-items: center;
836
+ padding: 0 2px;
837
+ }
838
+
839
+ .ican-widget-card-name {
840
+ font-family: var(--ican-font-brand);
841
+ font-size: 10px;
842
+ font-weight: 700;
843
+ letter-spacing: 0.7px;
844
+ text-transform: uppercase;
845
+ color: var(--ican-secondary-text);
846
+ flex: 1;
847
+ overflow: hidden;
848
+ text-overflow: ellipsis;
849
+ white-space: nowrap;
850
+ }
851
+
852
+ .ican-widget-card-close {
853
+ display: inline-flex;
854
+ align-items: center;
855
+ justify-content: center;
856
+ width: 22px;
857
+ height: 22px;
858
+ border: none;
859
+ border-radius: var(--ican-radius-xs);
860
+ background: transparent;
861
+ color: var(--ican-tertiary-text);
862
+ cursor: pointer;
863
+ flex-shrink: 0;
864
+ }
865
+
866
+ .ican-widget-card-close:hover {
867
+ background: var(--ican-error-dim);
868
+ color: var(--ican-error);
869
+ }
870
+
871
+ .ican-widget-card-body {
872
+ flex: 1;
873
+ overflow: auto;
874
+ position: relative;
875
+ }
876
+
877
+ /* ═══════════════════════════════════════════════════════
878
+ Widget Error
879
+ ═══════════════════════════════════════════════════════ */
880
+ .ican-widget-error {
881
+ display: flex;
882
+ flex-direction: column;
883
+ align-items: center;
884
+ justify-content: center;
885
+ height: 100%;
886
+ min-height: 180px;
887
+ padding: 24px;
888
+ gap: 8px;
889
+ text-align: center;
890
+ }
891
+
892
+ .ican-widget-error-icon { color: var(--ican-error); opacity: 0.7; }
893
+ .ican-widget-error-title { font-weight: 600; font-size: 13px; color: var(--ican-error); }
894
+ .ican-widget-error-msg {
895
+ font-size: 11px;
896
+ color: var(--ican-tertiary-text);
897
+ font-family: var(--ican-font-mono);
898
+ max-width: 100%;
899
+ overflow-wrap: break-word;
900
+ }
901
+
902
+ /* ═══════════════════════════════════════════════════════
903
+ Empty State — no widgets registered yet
904
+ ═══════════════════════════════════════════════════════ */
905
+ .ican-waiting {
906
+ display: flex;
907
+ flex-direction: column;
908
+ align-items: center;
909
+ justify-content: center;
910
+ min-height: 60vh;
911
+ gap: 16px;
912
+ text-align: center;
913
+ padding: 40px 24px;
914
+ }
915
+
916
+ .ican-waiting-icon {
917
+ width: 72px;
918
+ height: 72px;
919
+ border-radius: 20px;
920
+ background: var(--ican-accent-dim);
921
+ border: 1px solid var(--ican-border-bright);
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ color: var(--ican-accent);
926
+ }
927
+
928
+ .ican-waiting-title {
929
+ font-family: var(--ican-font-brand);
930
+ font-size: 20px;
931
+ font-weight: 600;
932
+ color: var(--ican-primary-text);
933
+ }
934
+
935
+ .ican-waiting-sub {
936
+ font-size: 13px;
937
+ color: var(--ican-secondary-text);
938
+ max-width: 320px;
939
+ line-height: 1.7;
940
+ }
941
+
942
+ .ican-waiting-cmd {
943
+ display: inline-flex;
944
+ align-items: center;
945
+ gap: 8px;
946
+ padding: 10px 18px;
947
+ background: var(--ican-secondary-bg);
948
+ border: 1px solid var(--ican-border-bright);
949
+ border-radius: var(--ican-radius-md);
950
+ font-size: 13px;
951
+ font-family: var(--ican-font-mono);
952
+ color: var(--ican-accent-text);
953
+ }
954
+
955
+ /* ═══════════════════════════════════════════════════════
956
+ Available Widgets — registry has items, canvas empty
957
+ ═══════════════════════════════════════════════════════ */
958
+ .ican-available {
959
+ display: flex;
960
+ flex-direction: column;
961
+ gap: 18px;
962
+ }
963
+
964
+ .ican-available-hd {
965
+ display: flex;
966
+ align-items: center;
967
+ gap: 10px;
968
+ }
969
+
970
+ .ican-available-hd-title {
971
+ font-family: var(--ican-font-brand);
972
+ font-size: 15px;
973
+ font-weight: 600;
974
+ color: var(--ican-primary-text);
975
+ }
976
+
977
+ .ican-available-hd-count {
978
+ font-size: 11px;
979
+ color: var(--ican-tertiary-text);
980
+ font-family: var(--ican-font-mono);
981
+ padding: 1px 7px;
982
+ background: var(--ican-border);
983
+ border-radius: 99px;
984
+ }
985
+
986
+ .ican-available-grid {
987
+ display: grid;
988
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
989
+ gap: 14px;
990
+ }
991
+
992
+ .ican-available-card {
993
+ background: var(--ican-card-bg);
994
+ border: 1px solid var(--ican-border);
995
+ border-radius: var(--ican-radius-lg);
996
+ padding: 24px;
997
+ display: flex;
998
+ flex-direction: column;
999
+ gap: 14px;
1000
+ box-shadow: var(--ican-card-shadow);
1001
+ transition: box-shadow 200ms, border-color 200ms;
1002
+ }
1003
+
1004
+ .ican-available-card:hover {
1005
+ box-shadow: var(--ican-card-hover-shadow);
1006
+ border-color: var(--ican-border-bright);
1007
+ }
1008
+
1009
+ [data-theme='glass-dark'] .ican-available-card,
1010
+ [data-theme='glass-light'] .ican-available-card {
1011
+ backdrop-filter: blur(32px) saturate(200%) brightness(1.06);
1012
+ -webkit-backdrop-filter: blur(32px) saturate(200%) brightness(1.06);
1013
+ border-color: var(--ican-glass-border);
1014
+ }
1015
+
1016
+ .ican-available-card-icon {
1017
+ width: 52px;
1018
+ height: 52px;
1019
+ border-radius: 14px;
1020
+ background: var(--ican-accent-dim);
1021
+ border: 1px solid var(--ican-border-bright);
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: center;
1025
+ color: var(--ican-accent);
1026
+ }
1027
+
1028
+ .ican-available-card-name {
1029
+ font-family: var(--ican-font-brand);
1030
+ font-size: 17px;
1031
+ font-weight: 600;
1032
+ color: var(--ican-primary-text);
1033
+ margin-bottom: 2px;
1034
+ }
1035
+
1036
+ .ican-available-card-id {
1037
+ font-size: 11px;
1038
+ color: var(--ican-tertiary-text);
1039
+ font-family: var(--ican-font-mono);
1040
+ }
1041
+
1042
+ .ican-available-card-desc {
1043
+ font-size: 12px;
1044
+ color: var(--ican-secondary-text);
1045
+ line-height: 1.55;
1046
+ }
1047
+
1048
+ .ican-available-card-add {
1049
+ margin-top: auto;
1050
+ }
1051
+
1052
+ /* ═══════════════════════════════════════════════════════
1053
+ Add Widget Panel / Modal
1054
+ ═══════════════════════════════════════════════════════ */
1055
+ .ican-overlay {
1056
+ position: fixed;
1057
+ inset: 0;
1058
+ z-index: 200;
1059
+ background: rgba(0, 0, 0, 0.50);
1060
+ opacity: 0;
1061
+ pointer-events: none;
1062
+ transition: opacity 200ms ease;
1063
+ display: flex;
1064
+ align-items: center;
1065
+ justify-content: center;
1066
+ }
1067
+
1068
+ .ican-overlay.open { opacity: 1; pointer-events: all; }
1069
+ [data-theme='glass-dark'] .ican-overlay { background: rgba(0,0,0,0.45); }
1070
+ [data-theme='glass-light'] .ican-overlay { background: rgba(100, 120, 180, 0.28); }
1071
+
1072
+ .ican-panel {
1073
+ position: relative;
1074
+ z-index: 201;
1075
+ width: 480px;
1076
+ max-width: calc(100vw - 40px);
1077
+ max-height: 80vh;
1078
+ background: var(--ican-modal-bg);
1079
+ border: 1px solid var(--ican-border-bright);
1080
+ border-radius: var(--ican-radius-lg);
1081
+ box-shadow: var(--ican-modal-shadow);
1082
+ display: flex;
1083
+ flex-direction: column;
1084
+ transform: scale(0.95) translateY(12px);
1085
+ transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
1086
+ }
1087
+
1088
+ .ican-overlay.open .ican-panel {
1089
+ transform: scale(1) translateY(0);
1090
+ }
1091
+
1092
+ .ican-panel-header {
1093
+ display: flex;
1094
+ align-items: center;
1095
+ padding: 0 18px;
1096
+ height: 52px;
1097
+ border-bottom: 1px solid var(--ican-border);
1098
+ gap: 10px;
1099
+ flex-shrink: 0;
1100
+ }
1101
+
1102
+ .ican-panel-title {
1103
+ font-family: var(--ican-font-brand);
1104
+ font-size: 15px;
1105
+ font-weight: 600;
1106
+ color: var(--ican-primary-text);
1107
+ flex: 1;
1108
+ }
1109
+
1110
+ .ican-panel-body {
1111
+ flex: 1;
1112
+ overflow-y: auto;
1113
+ padding: 10px;
1114
+ display: flex;
1115
+ flex-direction: column;
1116
+ gap: 6px;
1117
+ }
1118
+
1119
+ .ican-panel-empty {
1120
+ display: flex;
1121
+ flex-direction: column;
1122
+ align-items: center;
1123
+ justify-content: center;
1124
+ padding: 44px 20px;
1125
+ gap: 10px;
1126
+ text-align: center;
1127
+ }
1128
+
1129
+ .ican-panel-empty-text {
1130
+ font-size: 13px;
1131
+ color: var(--ican-secondary-text);
1132
+ line-height: 1.65;
1133
+ }
1134
+
1135
+ /* ── Registry Item ────────────────────────────────────── */
1136
+ .ican-registry-item {
1137
+ display: flex;
1138
+ align-items: center;
1139
+ gap: 12px;
1140
+ padding: 10px 12px;
1141
+ border: 1px solid var(--ican-border);
1142
+ border-radius: var(--ican-radius-md);
1143
+ background: var(--ican-secondary-bg);
1144
+ }
1145
+
1146
+ .ican-registry-item:hover { border-color: var(--ican-border-bright); }
1147
+
1148
+ .ican-registry-item-icon {
1149
+ width: 36px;
1150
+ height: 36px;
1151
+ border-radius: var(--ican-radius-sm);
1152
+ background: var(--ican-accent-dim);
1153
+ border: 1px solid var(--ican-border-bright);
1154
+ display: flex;
1155
+ align-items: center;
1156
+ justify-content: center;
1157
+ color: var(--ican-accent);
1158
+ flex-shrink: 0;
1159
+ }
1160
+
1161
+ .ican-registry-item-info { flex: 1; min-width: 0; }
1162
+
1163
+ .ican-registry-item-name {
1164
+ font-weight: 600;
1165
+ font-size: 13px;
1166
+ color: var(--ican-primary-text);
1167
+ overflow: hidden;
1168
+ text-overflow: ellipsis;
1169
+ white-space: nowrap;
1170
+ }
1171
+
1172
+ .ican-registry-item-id {
1173
+ font-size: 10px;
1174
+ color: var(--ican-tertiary-text);
1175
+ font-family: var(--ican-font-mono);
1176
+ overflow: hidden;
1177
+ text-overflow: ellipsis;
1178
+ white-space: nowrap;
1179
+ margin-top: 2px;
1180
+ }
1181
+
1182
+ /* ═══════════════════════════════════════════════════════
1183
+ Toast Notifications
1184
+ ═══════════════════════════════════════════════════════ */
1185
+ .ican-toast-root {
1186
+ position: fixed;
1187
+ bottom: 20px;
1188
+ right: 20px;
1189
+ z-index: 9999;
1190
+ display: flex;
1191
+ flex-direction: column;
1192
+ gap: 8px;
1193
+ pointer-events: none;
1194
+ }
1195
+
1196
+ .ican-toast {
1197
+ position: relative;
1198
+ padding: 10px 14px 10px 17px;
1199
+ border-radius: var(--ican-radius-md);
1200
+ background: var(--ican-modal-bg);
1201
+ border: 1px solid var(--ican-border-bright);
1202
+ box-shadow: 0 4px 20px rgba(0,0,0,0.2);
1203
+ font-size: 13px;
1204
+ color: var(--ican-primary-text);
1205
+ max-width: 300px;
1206
+ pointer-events: all;
1207
+ animation: toastIn 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
1208
+ }
1209
+
1210
+ .ican-toast::before {
1211
+ content: '';
1212
+ position: absolute;
1213
+ left: 0; top: 0; bottom: 0;
1214
+ width: 3px;
1215
+ border-radius: 8px 0 0 8px;
1216
+ }
1217
+
1218
+ .ican-toast.success::before { background: var(--ican-success); }
1219
+ .ican-toast.error::before { background: var(--ican-error); }
1220
+ .ican-toast.info::before { background: var(--ican-accent); }
1221
+
1222
+ @keyframes toastIn {
1223
+ from { opacity: 0; transform: translateY(10px) scale(0.95); }
1224
+ to { opacity: 1; transform: translateY(0) scale(1); }
1225
+ }
1226
+
1227
+ /* ═══════════════════════════════════════════════════════
1228
+ ICan Component CSS (used by ican-components.js)
1229
+ ═══════════════════════════════════════════════════════ */
1230
+ .ican-widget-wrapper { display: flex; flex-direction: column; width: 100%; height: 100%; }
1231
+
1232
+ .ican-titlebar {
1233
+ height: 42px;
1234
+ display: flex;
1235
+ align-items: center;
1236
+ padding: 0 14px;
1237
+ border-bottom: 1px solid var(--ican-border);
1238
+ gap: 8px;
1239
+ flex-shrink: 0;
1240
+ font-family: var(--ican-font-brand);
1241
+ }
1242
+
1243
+ .ican-titlebar-title {
1244
+ text-transform: uppercase;
1245
+ font-size: 10px;
1246
+ font-weight: 700;
1247
+ letter-spacing: 0.7px;
1248
+ color: var(--ican-secondary-text);
1249
+ }
1250
+
1251
+ .ican-titlebar-subtitle { font-size: 10px; color: var(--ican-tertiary-text); }
1252
+ .ican-titlebar-actions { display: flex; gap: 6px; margin-left: auto; }
1253
+ .ican-filter-panel { display: flex; gap: 6px; align-items: center; }
1254
+
1255
+ .ican-loading {
1256
+ display: flex;
1257
+ align-items: center;
1258
+ justify-content: center;
1259
+ gap: 10px;
1260
+ padding: 24px;
1261
+ color: var(--ican-secondary-text);
1262
+ font-size: 13px;
1263
+ }
1264
+
1265
+ @keyframes spin { to { transform: rotate(360deg); } }
1266
+
1267
+ .ican-spinner {
1268
+ width: 22px;
1269
+ height: 22px;
1270
+ border: 2px solid var(--ican-border-bright);
1271
+ border-top-color: var(--ican-accent);
1272
+ border-radius: 50%;
1273
+ animation: spin 0.7s linear infinite;
1274
+ flex-shrink: 0;
1275
+ }
1276
+
1277
+ .ican-error-display {
1278
+ display: flex;
1279
+ gap: 8px;
1280
+ padding: 12px 14px;
1281
+ background: var(--ican-error-dim);
1282
+ border: 1px solid rgba(248, 113, 113, 0.15);
1283
+ border-radius: var(--ican-radius-sm);
1284
+ color: var(--ican-error);
1285
+ font-size: 13px;
1286
+ }
1287
+
1288
+ .ican-card {
1289
+ background: var(--ican-card-bg);
1290
+ border: 1px solid var(--ican-border);
1291
+ border-radius: var(--ican-radius-md);
1292
+ padding: 14px;
1293
+ }
1294
+
1295
+ .ican-data-list { margin: 0; padding: 0; list-style: none; }
1296
+
1297
+ .ican-data-list-row {
1298
+ display: flex;
1299
+ justify-content: space-between;
1300
+ padding: 5px 0;
1301
+ border-bottom: 1px solid var(--ican-border);
1302
+ font-size: 12px;
1303
+ color: var(--ican-primary-text);
1304
+ }
1305
+
1306
+ .ican-data-list-row:last-child { border-bottom: none; }
1307
+
1308
+ .ican-badge {
1309
+ display: inline-flex;
1310
+ align-items: center;
1311
+ font-size: 10px;
1312
+ padding: 2px 7px;
1313
+ border-radius: 99px;
1314
+ font-weight: 600;
1315
+ }
1316
+
1317
+ .ican-badge--default { background: var(--ican-accent-dim); color: var(--ican-accent-text); }
1318
+ .ican-badge--success { background: var(--ican-success-dim); color: var(--ican-success); }
1319
+ .ican-badge--error { background: var(--ican-error-dim); color: var(--ican-error); }
1320
+
1321
+ /* ── Inline code snippets ─────────────────────────────── */
1322
+ code {
1323
+ font-family: var(--ican-font-mono);
1324
+ font-size: 11px;
1325
+ background: var(--ican-secondary-bg);
1326
+ color: var(--ican-accent-text);
1327
+ padding: 1px 5px;
1328
+ border-radius: 4px;
1329
+ border: 1px solid var(--ican-border-bright);
1330
+ }
1331
+ </style>
1332
+ </head>
1333
+ <body>
1334
+ <div id="app"></div>
1335
+ <div id="toast-root" class="ican-toast-root"></div>
1336
+
1337
+ <script>
1338
+ (function () {
1339
+ 'use strict';
1340
+
1341
+ /* ─── Constants ───────────────────────────────────────── */
1342
+ var LS_THEME = 'ican_theme';
1343
+ var LS_ADDED = 'ican_added_widgets';
1344
+ var LS_ORCH_URL = 'ican_orchUrl';
1345
+ var LS_API_KEY = 'ican_apiKey';
1346
+
1347
+ /* ─── Widget Registry ─────────────────────────────────── */
1348
+ window.__ican_registry = new Map();
1349
+
1350
+ window.registerWidget = function (config) {
1351
+ if (!config || !config.id) {
1352
+ console.warn('[ICan Dev] registerWidget called without a valid config.id');
1353
+ return;
1354
+ }
1355
+ window.__ican_registry.set(config.id, config);
1356
+ console.log('[ICan Dev] Widget registered:', config.id, config.name || '');
1357
+ window.dispatchEvent(new CustomEvent('ican:registry-updated'));
1358
+ };
1359
+
1360
+ /* ─── Theme registry ──────────────────────────────────── */
1361
+ var THEMES = [
1362
+ {
1363
+ key: 'dark',
1364
+ label: 'Dark',
1365
+ desc: 'Premium deep dark',
1366
+ type: 'Dark',
1367
+ bg: '#09090b',
1368
+ mid: '#141417',
1369
+ accent: '#7c6aff',
1370
+ glass: false,
1371
+ },
1372
+ {
1373
+ key: 'light',
1374
+ label: 'Light',
1375
+ desc: 'Crisp Apple-inspired',
1376
+ type: 'Light',
1377
+ bg: '#f5f5f7',
1378
+ mid: '#ffffff',
1379
+ accent: '#5b4cd9',
1380
+ glass: false,
1381
+ },
1382
+ {
1383
+ key: 'glass-dark',
1384
+ label: 'Glass Dark',
1385
+ desc: 'Frosted night sky',
1386
+ type: 'Glass-Dark',
1387
+ bg: '#08061a',
1388
+ mid: 'rgba(80,50,180,0.4)',
1389
+ accent: '#b4a8ff',
1390
+ glass: true,
1391
+ },
1392
+ {
1393
+ key: 'glass-light',
1394
+ label: 'Glass Light',
1395
+ desc: 'Frosted pastel day',
1396
+ type: 'Glass-Light',
1397
+ bg: '#d0e4ff',
1398
+ mid: 'rgba(160,130,255,0.5)',
1399
+ accent: '#5b4cd9',
1400
+ glass: true,
1401
+ },
1402
+ ];
1403
+
1404
+ function getThemeInfo(key) {
1405
+ for (var i = 0; i < THEMES.length; i++) {
1406
+ if (THEMES[i].key === key) return THEMES[i];
1407
+ }
1408
+ return THEMES[0];
1409
+ }
1410
+
1411
+ /* ─── Mock icanContext factory ─────────────────────────── */
1412
+ function makeICanContext(themeKey) {
1413
+ var info = getThemeInfo(themeKey);
1414
+ return {
1415
+ environment: 'dev',
1416
+ userKey: 'dev-user-001',
1417
+ language: 'en',
1418
+ root: 'root',
1419
+ orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1420
+ apiKey: localStorage.getItem(LS_API_KEY) || '',
1421
+ executeAction: function (model, action, params) {
1422
+ console.log('[ICan Dev] executeAction:', model, action, params);
1423
+ return Promise.resolve([]);
1424
+ },
1425
+ executeService: function (app, service, params) {
1426
+ console.log('[ICan Dev] executeService:', app, service, params);
1427
+ return Promise.resolve(null);
1428
+ },
1429
+ fireEvent: function (id) {
1430
+ console.log('[ICan Dev] fireEvent:', id);
1431
+ return Promise.resolve();
1432
+ },
1433
+ hasAppRole: function () { return true; },
1434
+ $L: function (code) { return code; },
1435
+ themeName: info.label,
1436
+ themeType: info.type,
1437
+ };
1438
+ }
1439
+
1440
+ /* ─── Theme helpers ───────────────────────────────────── */
1441
+ function getStoredTheme() {
1442
+ return localStorage.getItem(LS_THEME) || 'dark';
1443
+ }
1444
+
1445
+ function applyTheme(key) {
1446
+ var el = document.documentElement;
1447
+ if (key === 'dark') {
1448
+ el.removeAttribute('data-theme');
1449
+ } else {
1450
+ el.setAttribute('data-theme', key);
1451
+ }
1452
+ localStorage.setItem(LS_THEME, key);
1453
+ }
1454
+
1455
+ applyTheme(getStoredTheme());
1456
+
1457
+ /* ─── Widgets persistence ─────────────────────────────── */
1458
+ function loadAddedWidgets() {
1459
+ try { return JSON.parse(localStorage.getItem(LS_ADDED) || '[]'); }
1460
+ catch (_) { return []; }
1461
+ }
1462
+
1463
+ function saveAddedWidgets(ids) {
1464
+ localStorage.setItem(LS_ADDED, JSON.stringify(ids));
1465
+ }
1466
+
1467
+ /* ─── Toast ───────────────────────────────────────────── */
1468
+ function showToast(msg, type) {
1469
+ var root = document.getElementById('toast-root');
1470
+ if (!root) return;
1471
+ var el = document.createElement('div');
1472
+ el.className = 'ican-toast ' + (type || 'info');
1473
+ el.textContent = msg;
1474
+ root.appendChild(el);
1475
+ setTimeout(function () {
1476
+ if (el.parentNode) el.parentNode.removeChild(el);
1477
+ }, 3200);
1478
+ }
1479
+
1480
+ /* ─── React shorthand ─────────────────────────────────── */
1481
+ var h = React.createElement;
1482
+
1483
+ /* ─── Heroicons SVG helper ────────────────────────────── */
1484
+ function icon(name, size) {
1485
+ size = size || 16;
1486
+ var a = {
1487
+ xmlns: 'http://www.w3.org/2000/svg',
1488
+ width: size, height: size,
1489
+ viewBox: '0 0 24 24',
1490
+ fill: 'none',
1491
+ stroke: 'currentColor',
1492
+ strokeWidth: '1.75',
1493
+ strokeLinecap: 'round',
1494
+ strokeLinejoin: 'round',
1495
+ 'aria-hidden': 'true',
1496
+ style: { display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 },
1497
+ };
1498
+ switch (name) {
1499
+ /* Grip — custom 6-dot (standard UX pattern, no Heroicon equivalent) */
1500
+ case 'grip':
1501
+ return h('svg', Object.assign({}, a, { strokeWidth: '2' }),
1502
+ h('circle', { cx: '9', cy: '5', r: '1', fill: 'currentColor', stroke: 'none' }),
1503
+ h('circle', { cx: '15', cy: '5', r: '1', fill: 'currentColor', stroke: 'none' }),
1504
+ h('circle', { cx: '9', cy: '12', r: '1', fill: 'currentColor', stroke: 'none' }),
1505
+ h('circle', { cx: '15', cy: '12', r: '1', fill: 'currentColor', stroke: 'none' }),
1506
+ h('circle', { cx: '9', cy: '19', r: '1', fill: 'currentColor', stroke: 'none' }),
1507
+ h('circle', { cx: '15', cy: '19', r: '1', fill: 'currentColor', stroke: 'none' })
1508
+ );
1509
+ /* XMarkIcon */
1510
+ case 'x':
1511
+ return h('svg', a, h('path', { d: 'M6 18L18 6M6 6l12 12' }));
1512
+ /* ExclamationTriangleIcon */
1513
+ case 'alert':
1514
+ return h('svg', a,
1515
+ h('path', { 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.126Z' }),
1516
+ h('path', { d: 'M12 15.75h.007v.008H12v-.008Z' })
1517
+ );
1518
+ /* Cog6ToothIcon */
1519
+ case 'settings':
1520
+ return h('svg', a,
1521
+ h('path', { 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.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z' }),
1522
+ h('path', { d: 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z' })
1523
+ );
1524
+ /* LockClosedIcon */
1525
+ case 'lock':
1526
+ return h('svg', a,
1527
+ h('path', { d: 'M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z' })
1528
+ );
1529
+ /* PencilSquareIcon */
1530
+ case 'pencil':
1531
+ return h('svg', a,
1532
+ h('path', { d: 'm16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z' }),
1533
+ h('path', { d: 'M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10' })
1534
+ );
1535
+ /* CubeIcon */
1536
+ case 'cube':
1537
+ return h('svg', a,
1538
+ h('path', { d: 'm21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9' })
1539
+ );
1540
+ /* Squares2X2Icon */
1541
+ case 'squares':
1542
+ return h('svg', a,
1543
+ h('path', { d: 'M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z' })
1544
+ );
1545
+ /* ChartBarIcon */
1546
+ case 'chart-bar':
1547
+ return h('svg', a,
1548
+ h('path', { d: 'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z' })
1549
+ );
1550
+ /* PlusIcon */
1551
+ case 'plus':
1552
+ return h('svg', a, h('path', { d: 'M12 4.5v15m7.5-7.5h-15' }));
1553
+ /* SwatchIcon */
1554
+ case 'swatch':
1555
+ return h('svg', a,
1556
+ h('path', { d: 'M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 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 0 0 3.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.007v.008H6.75v-.008Z' })
1557
+ );
1558
+ /* CheckIcon */
1559
+ case 'check':
1560
+ return h('svg', a, h('path', { d: 'm4.5 12.75 6 6 9-13.5' }));
1561
+ /* SignalIcon */
1562
+ case 'signal':
1563
+ return h('svg', a,
1564
+ h('path', { d: 'M9.348 14.652a3.75 3.75 0 0 1 0-5.304m5.304 0a3.75 3.75 0 0 1 0 5.304m-7.425 2.121a6.75 6.75 0 0 1 0-9.546m9.546 0a6.75 6.75 0 0 1 0 9.546M5.106 18.394a9.75 9.75 0 0 1 0-12.788m13.576 0a9.75 9.75 0 0 1 0 12.788M12 12h.007v.008H12V12Z' })
1565
+ );
1566
+ /* ChevronDownIcon */
1567
+ case 'chevron-down':
1568
+ return h('svg', a, h('path', { d: 'm19.5 8.25-7.5 7.5-7.5-7.5' }));
1569
+ default:
1570
+ return null;
1571
+ }
1572
+ }
1573
+
1574
+ /* ─── react-grid-layout ───────────────────────────────── */
1575
+ var RGL = window.ReactGridLayout || null;
1576
+ if (!RGL) console.warn('[ICan Dev] react-grid-layout not loaded — CDN may be unavailable.');
1577
+
1578
+ /* ─── WidgetRenderer ──────────────────────────────────── */
1579
+ function WidgetRenderer(props) {
1580
+ var config = props.config;
1581
+ var icanContext = props.icanContext;
1582
+
1583
+ if (!config || !config.component) {
1584
+ return h('div', { className: 'ican-widget-error' },
1585
+ h('div', { className: 'ican-widget-error-icon' }, icon('alert', 24)),
1586
+ h('div', { className: 'ican-widget-error-title' }, 'No component defined'),
1587
+ h('div', { className: 'ican-widget-error-msg' }, config ? config.id : 'unknown')
1588
+ );
1589
+ }
1590
+
1591
+ try {
1592
+ return h(config.component, Object.assign({}, config.defaultProps || {}, { icanContext: icanContext }));
1593
+ } catch (err) {
1594
+ return h('div', { className: 'ican-widget-error' },
1595
+ h('div', { className: 'ican-widget-error-icon' }, icon('alert', 24)),
1596
+ h('div', { className: 'ican-widget-error-title' }, 'Render error'),
1597
+ h('div', { className: 'ican-widget-error-msg' }, String(err && err.message ? err.message : err))
1598
+ );
1599
+ }
1600
+ }
1601
+
1602
+ /* ─── ErrorBoundary ───────────────────────────────────── */
1603
+ var ErrorBoundary = (function () {
1604
+ function EB(props) {
1605
+ React.Component.call(this, props);
1606
+ this.state = { hasError: false, error: null };
1607
+ }
1608
+ EB.prototype = Object.create(React.Component.prototype);
1609
+ EB.prototype.constructor = EB;
1610
+ EB.getDerivedStateFromError = function (err) { return { hasError: true, error: err }; };
1611
+ EB.prototype.render = function () {
1612
+ if (this.state.hasError) {
1613
+ return h('div', { className: 'ican-widget-error' },
1614
+ h('div', { className: 'ican-widget-error-icon' }, icon('alert', 24)),
1615
+ h('div', { className: 'ican-widget-error-title' }, 'Widget crashed'),
1616
+ h('div', { className: 'ican-widget-error-msg' },
1617
+ this.state.error ? String(this.state.error.message || this.state.error) : 'Unknown error'
1618
+ )
1619
+ );
1620
+ }
1621
+ return this.props.children;
1622
+ };
1623
+ return EB;
1624
+ }());
1625
+
1626
+ /* ─── WidgetCard ──────────────────────────────────────── */
1627
+ function WidgetCard(props) {
1628
+ var config = props.config;
1629
+ var icanContext = props.icanContext;
1630
+ var onRemove = props.onRemove;
1631
+ var editMode = props.editMode;
1632
+
1633
+ return h('div', { className: 'ican-widget-card' },
1634
+ h('div', { className: 'ican-widget-card-header' },
1635
+ h('span', {
1636
+ className: 'ican-drag-handle',
1637
+ title: editMode ? 'Drag to move' : 'Enable edit mode to drag',
1638
+ style: { cursor: editMode ? 'grab' : 'default', opacity: editMode ? 1 : 0.30 },
1639
+ 'aria-hidden': 'true',
1640
+ }, icon('grip', 14)),
1641
+ h('span', { className: 'ican-widget-card-name' }, config.name || config.id),
1642
+ h('button', {
1643
+ className: 'ican-widget-card-close',
1644
+ type: 'button',
1645
+ title: 'Remove widget',
1646
+ 'aria-label': 'Remove ' + (config.name || config.id),
1647
+ onClick: function () { onRemove(config.id); },
1648
+ }, icon('x', 13))
1649
+ ),
1650
+ h('div', { className: 'ican-widget-card-body' },
1651
+ h(ErrorBoundary, null,
1652
+ h(WidgetRenderer, { config: config, icanContext: icanContext })
1653
+ )
1654
+ )
1655
+ );
1656
+ }
1657
+
1658
+ /* ─── ThemeSwitcher ───────────────────────────────────── */
1659
+ function ThemeSwitcher(props) {
1660
+ var currentKey = props.currentTheme;
1661
+ var open = props.open;
1662
+ var onToggle = props.onToggle;
1663
+ var onChange = props.onChange;
1664
+ var info = getThemeInfo(currentKey);
1665
+
1666
+ /* Swatch gradient for the trigger button */
1667
+ var swatchStyle = {
1668
+ width: 14, height: 14, borderRadius: 4, flexShrink: 0,
1669
+ background: info.glass
1670
+ ? 'linear-gradient(135deg, ' + info.bg + ' 0%, ' + info.mid + ' 50%, ' + info.accent + ' 100%)'
1671
+ : 'linear-gradient(135deg, ' + info.mid + ' 0%, ' + info.bg + ' 100%)',
1672
+ border: '1px solid rgba(255,255,255,0.18)',
1673
+ };
1674
+
1675
+ return h('div', { className: 'ican-theme-wrapper' },
1676
+
1677
+ /* ── Trigger button ── */
1678
+ h('button', {
1679
+ className: 'ican-theme-btn',
1680
+ type: 'button',
1681
+ onClick: onToggle,
1682
+ 'aria-label': 'Switch theme — current: ' + info.label,
1683
+ title: 'Switch theme',
1684
+ },
1685
+ h('div', { style: swatchStyle }),
1686
+ info.label,
1687
+ h('span', { className: 'ican-theme-chevron' + (open ? ' open' : '') },
1688
+ icon('chevron-down', 11)
1689
+ )
1690
+ ),
1691
+
1692
+ /* ── Dropdown ── */
1693
+ h('div', { className: 'ican-theme-dropdown' + (open ? ' open' : '') },
1694
+ h('div', { className: 'ican-theme-dd-label' }, 'Appearance'),
1695
+
1696
+ THEMES.map(function (t) {
1697
+ var isSelected = t.key === currentKey;
1698
+ var swatchBg = t.glass
1699
+ ? 'linear-gradient(135deg, ' + t.bg + ' 0%, ' + t.mid + ' 50%, ' + t.accent + ' 100%)'
1700
+ : 'linear-gradient(135deg, ' + t.mid + ' 0%, ' + t.bg + ' 100%)';
1701
+
1702
+ return h('button', {
1703
+ key: t.key,
1704
+ className: 'ican-theme-option' + (isSelected ? ' selected' : ''),
1705
+ type: 'button',
1706
+ onClick: function () { onChange(t.key); },
1707
+ 'aria-label': 'Switch to ' + t.label,
1708
+ },
1709
+ h('div', { className: 'ican-theme-option-swatch', style: { background: swatchBg } },
1710
+ h('div', { className: 'ican-theme-option-dot', style: { background: t.accent } })
1711
+ ),
1712
+ h('div', { className: 'ican-theme-option-info' },
1713
+ h('div', { className: 'ican-theme-option-name' }, t.label),
1714
+ h('div', { className: 'ican-theme-option-desc' }, t.desc)
1715
+ ),
1716
+ isSelected
1717
+ ? h('span', { className: 'ican-theme-option-check' }, icon('check', 14))
1718
+ : null
1719
+ );
1720
+ })
1721
+ )
1722
+ );
1723
+ }
1724
+
1725
+ /* ─── AddWidgetPanel ──────────────────────────────────── */
1726
+ function AddWidgetPanel(props) {
1727
+ var open = props.open;
1728
+ var onClose = props.onClose;
1729
+ var registry = props.registry;
1730
+ var addedIds = props.addedIds;
1731
+ var onAdd = props.onAdd;
1732
+
1733
+ var entries = [];
1734
+ registry.forEach(function (config, id) { entries.push({ id: id, config: config }); });
1735
+
1736
+ return h('div', {
1737
+ className: 'ican-overlay' + (open ? ' open' : ''),
1738
+ onClick: onClose,
1739
+ 'aria-hidden': !open,
1740
+ role: 'dialog',
1741
+ 'aria-modal': 'true',
1742
+ 'aria-label': 'Add Widget',
1743
+ },
1744
+ h('aside', {
1745
+ className: 'ican-panel',
1746
+ onClick: function (e) { e.stopPropagation(); },
1747
+ },
1748
+ h('div', { className: 'ican-panel-header' },
1749
+ h('span', { className: 'ican-panel-title' }, 'Add Widget'),
1750
+ h('button', {
1751
+ className: 'ican-icon-btn',
1752
+ type: 'button',
1753
+ onClick: onClose,
1754
+ 'aria-label': 'Close panel',
1755
+ title: 'Close',
1756
+ }, icon('x', 15))
1757
+ ),
1758
+ h('div', { className: 'ican-panel-body' },
1759
+ entries.length === 0
1760
+ ? h('div', { className: 'ican-panel-empty' },
1761
+ h('div', { style: { color: 'var(--ican-tertiary-text)', marginBottom: 6 } }, icon('cube', 36)),
1762
+ h('div', {
1763
+ style: {
1764
+ fontFamily: 'var(--ican-font-brand)',
1765
+ fontSize: 14,
1766
+ fontWeight: 600,
1767
+ color: 'var(--ican-secondary-text)',
1768
+ marginBottom: 6,
1769
+ },
1770
+ }, 'No widgets registered'),
1771
+ h('div', { className: 'ican-panel-empty-text' },
1772
+ 'Build your widget and run ',
1773
+ h('code', null, 'npm run build'),
1774
+ ' to register it here.'
1775
+ )
1776
+ )
1777
+ : entries.map(function (entry) {
1778
+ var alreadyAdded = addedIds.indexOf(entry.id) !== -1;
1779
+ return h('div', { className: 'ican-registry-item', key: entry.id },
1780
+ h('div', { className: 'ican-registry-item-icon' },
1781
+ entry.config.icon ? entry.config.icon : icon('squares', 18)
1782
+ ),
1783
+ h('div', { className: 'ican-registry-item-info' },
1784
+ h('div', { className: 'ican-registry-item-name' }, entry.config.name || entry.id),
1785
+ h('div', { className: 'ican-registry-item-id' }, entry.id)
1786
+ ),
1787
+ h('button', {
1788
+ className: 'ican-btn ' + (alreadyAdded ? 'ican-btn-secondary' : 'ican-btn-primary'),
1789
+ type: 'button',
1790
+ disabled: alreadyAdded,
1791
+ style: { fontSize: 12, height: 28, padding: '0 10px' },
1792
+ onClick: function () { onAdd(entry.id); },
1793
+ 'aria-label': (alreadyAdded ? 'Already added: ' : 'Add ') + (entry.config.name || entry.id),
1794
+ }, alreadyAdded ? 'Added' : [icon('plus', 12), ' Add'])
1795
+ );
1796
+ })
1797
+ )
1798
+ )
1799
+ );
1800
+ }
1801
+
1802
+ /* ─── ConnectionPanel ─────────────────────────────────── */
1803
+ function ConnectionPanel(props) {
1804
+ var orchUrl = props.orchUrl;
1805
+ var apiKey = props.apiKey;
1806
+ var onSave = props.onSave;
1807
+ var ref = { orchUrl: orchUrl, apiKey: apiKey };
1808
+
1809
+ return h('div', { className: 'ican-conn-panel' },
1810
+ h('div', { className: 'ican-conn-field' },
1811
+ h('label', { className: 'ican-conn-label', htmlFor: 'conn-orch' }, 'Orch URL'),
1812
+ h('input', {
1813
+ id: 'conn-orch',
1814
+ className: 'ican-conn-input',
1815
+ type: 'text',
1816
+ defaultValue: orchUrl,
1817
+ placeholder: 'https://your-orch.example.com',
1818
+ 'aria-label': 'Orchestration URL',
1819
+ onChange: function (e) { ref.orchUrl = e.target.value; },
1820
+ })
1821
+ ),
1822
+ h('div', { className: 'ican-conn-field' },
1823
+ h('label', { className: 'ican-conn-label', htmlFor: 'conn-key' }, 'API Key'),
1824
+ h('input', {
1825
+ id: 'conn-key',
1826
+ className: 'ican-conn-input',
1827
+ type: 'password',
1828
+ defaultValue: apiKey,
1829
+ placeholder: '••••••••••••••••',
1830
+ 'aria-label': 'API Key',
1831
+ onChange: function (e) { ref.apiKey = e.target.value; },
1832
+ })
1833
+ ),
1834
+ h('div', { className: 'ican-conn-actions' },
1835
+ h('button', {
1836
+ className: 'ican-btn ican-btn-primary',
1837
+ type: 'button',
1838
+ style: { height: 30, fontSize: 12 },
1839
+ onClick: function () { onSave(ref.orchUrl, ref.apiKey); },
1840
+ }, 'Save'),
1841
+ h('div', { className: 'ican-conn-status' },
1842
+ h('div', { className: 'ican-conn-dot' + (orchUrl ? ' connected' : '') }),
1843
+ orchUrl ? 'Connected' : 'Not configured'
1844
+ )
1845
+ )
1846
+ );
1847
+ }
1848
+
1849
+ /* ─── Main App ────────────────────────────────────────── */
1850
+ var App = (function () {
1851
+ /* Connection panel height (fixed, no wrapping) */
1852
+ var CONN_H = 64;
1853
+
1854
+ function App(props) {
1855
+ React.Component.call(this, props);
1856
+ var storedTheme = getStoredTheme();
1857
+ this.state = {
1858
+ theme: storedTheme,
1859
+ themeDdOpen: false,
1860
+ panelOpen: false,
1861
+ registry: new Map(window.__ican_registry),
1862
+ addedIds: loadAddedWidgets(),
1863
+ orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1864
+ apiKey: localStorage.getItem(LS_API_KEY) || '',
1865
+ showConn: false,
1866
+ editMode: false,
1867
+ layouts: JSON.parse(localStorage.getItem('ican_layouts') || '{}'),
1868
+ gridWidth: window.innerWidth - 40,
1869
+ };
1870
+
1871
+ this._onRegistryUpdate = this._onRegistryUpdate.bind(this);
1872
+ this._onWindowResize = this._onWindowResize.bind(this);
1873
+ this._setTheme = this._setTheme.bind(this);
1874
+ this._toggleThemeDd = this._toggleThemeDd.bind(this);
1875
+ this._openPanel = this._openPanel.bind(this);
1876
+ this._closePanel = this._closePanel.bind(this);
1877
+ this._addWidget = this._addWidget.bind(this);
1878
+ this._removeWidget = this._removeWidget.bind(this);
1879
+ this._saveConnection = this._saveConnection.bind(this);
1880
+ this._toggleEditMode = this._toggleEditMode.bind(this);
1881
+ this._onLayoutChange = this._onLayoutChange.bind(this);
1882
+ }
1883
+
1884
+ App.prototype = Object.create(React.Component.prototype);
1885
+ App.prototype.constructor = App;
1886
+
1887
+ App.prototype.componentDidMount = function () {
1888
+ window.addEventListener('ican:registry-updated', this._onRegistryUpdate);
1889
+ window.addEventListener('resize', this._onWindowResize);
1890
+ };
1891
+
1892
+ App.prototype.componentWillUnmount = function () {
1893
+ window.removeEventListener('ican:registry-updated', this._onRegistryUpdate);
1894
+ window.removeEventListener('resize', this._onWindowResize);
1895
+ };
1896
+
1897
+ App.prototype._onWindowResize = function () {
1898
+ this.setState({ gridWidth: window.innerWidth - 40 });
1899
+ };
1900
+
1901
+ App.prototype._onRegistryUpdate = function () {
1902
+ this.setState({ registry: new Map(window.__ican_registry) });
1903
+ };
1904
+
1905
+ App.prototype._toggleThemeDd = function () {
1906
+ this.setState(function (s) { return { themeDdOpen: !s.themeDdOpen }; });
1907
+ };
1908
+
1909
+ App.prototype._setTheme = function (key) {
1910
+ applyTheme(key);
1911
+ this.setState({ theme: key, themeDdOpen: false });
1912
+ };
1913
+
1914
+ App.prototype._openPanel = function () { this.setState({ panelOpen: true }); };
1915
+ App.prototype._closePanel = function () { this.setState({ panelOpen: false }); };
1916
+
1917
+ App.prototype._addWidget = function (id) {
1918
+ var ids = this.state.addedIds.slice();
1919
+ if (ids.indexOf(id) === -1) {
1920
+ ids.push(id);
1921
+ saveAddedWidgets(ids);
1922
+ this.setState({ addedIds: ids });
1923
+ showToast('Widget added to canvas', 'success');
1924
+ }
1925
+ };
1926
+
1927
+ App.prototype._removeWidget = function (id) {
1928
+ var ids = this.state.addedIds.filter(function (x) { return x !== id; });
1929
+ saveAddedWidgets(ids);
1930
+ this.setState({ addedIds: ids });
1931
+ showToast('Widget removed', 'info');
1932
+ };
1933
+
1934
+ App.prototype._saveConnection = function (orchUrl, apiKey) {
1935
+ localStorage.setItem(LS_ORCH_URL, orchUrl);
1936
+ localStorage.setItem(LS_API_KEY, apiKey);
1937
+ this.setState({ orchUrl: orchUrl, apiKey: apiKey });
1938
+ showToast('Connection settings saved', 'success');
1939
+ };
1940
+
1941
+ App.prototype._toggleEditMode = function () {
1942
+ this.setState(function (s) { return { editMode: !s.editMode }; });
1943
+ };
1944
+
1945
+ App.prototype._onLayoutChange = function (layout) {
1946
+ var layouts = Object.assign({}, this.state.layouts);
1947
+ layout.forEach(function (item) { layouts[item.i] = item; });
1948
+ localStorage.setItem('ican_layouts', JSON.stringify(layouts));
1949
+ this.setState({ layouts: layouts });
1950
+ };
1951
+
1952
+ App.prototype.render = function () {
1953
+ var self = this;
1954
+ var s = this.state;
1955
+ var ctx = makeICanContext(s.theme);
1956
+
1957
+ /* Layout offsets */
1958
+ var connH = s.showConn ? CONN_H : 0;
1959
+ var toolbarTop = 56 + connH;
1960
+ var mainMT = 56 + connH + 48;
1961
+
1962
+ var addedConfigs = s.addedIds
1963
+ .map(function (id) { return s.registry.get(id); })
1964
+ .filter(Boolean);
1965
+
1966
+ /* Registry items not yet on canvas */
1967
+ var unadded = [];
1968
+ s.registry.forEach(function (cfg, id) {
1969
+ if (s.addedIds.indexOf(id) === -1) unadded.push({ id: id, config: cfg });
1970
+ });
1971
+
1972
+ return h(React.Fragment, null,
1973
+
1974
+ /* ── Theme dropdown backdrop ── */
1975
+ s.themeDdOpen
1976
+ ? h('div', {
1977
+ style: { position: 'fixed', inset: 0, zIndex: 499 },
1978
+ onClick: function () { self.setState({ themeDdOpen: false }); },
1979
+ })
1980
+ : null,
1981
+
1982
+ /* ── Header ── */
1983
+ h('header', { className: 'ican-header' },
1984
+ h('a', { className: 'ican-logo', href: '#', 'aria-label': 'InfAIra ICan' },
1985
+ h('img', { src: './resources/infaira-icon.png', alt: 'InfAIra', height: 28 }),
1986
+ h('span', { className: 'ican-logo-name' },
1987
+ 'Inf', h('em', null, 'AI'), 'ra'
1988
+ )
1989
+ ),
1990
+ h('div', { className: 'ican-sep' }),
1991
+ h('span', {
1992
+ className: 'ican-dev-badge',
1993
+ title: 'Development environment',
1994
+ 'aria-label': 'Development environment',
1995
+ }, 'DEV'),
1996
+ h('div', { className: 'ican-header-spacer' }),
1997
+
1998
+ /* Theme switcher */
1999
+ h(ThemeSwitcher, {
2000
+ currentTheme: s.theme,
2001
+ open: s.themeDdOpen,
2002
+ onToggle: self._toggleThemeDd,
2003
+ onChange: self._setTheme,
2004
+ }),
2005
+
2006
+ /* Connection settings toggle */
2007
+ h('button', {
2008
+ className: 'ican-icon-btn' + (s.showConn ? ' active' : ''),
2009
+ type: 'button',
2010
+ title: 'Connection settings',
2011
+ 'aria-label': 'Connection settings',
2012
+ onClick: function () {
2013
+ self.setState(function (st) { return { showConn: !st.showConn }; });
2014
+ },
2015
+ }, icon('signal', 15)),
2016
+
2017
+ h('div', { className: 'ican-avatar', title: 'Dev user', 'aria-label': 'Dev user' }, 'D')
2018
+ ),
2019
+
2020
+ /* ── Connection Panel ── */
2021
+ s.showConn
2022
+ ? h('div', { style: { position: 'fixed', top: 56, left: 0, right: 0, zIndex: 98 } },
2023
+ h(ConnectionPanel, {
2024
+ orchUrl: s.orchUrl,
2025
+ apiKey: s.apiKey,
2026
+ onSave: self._saveConnection,
2027
+ })
2028
+ )
2029
+ : null,
2030
+
2031
+ /* ── Toolbar ── */
2032
+ h('div', {
2033
+ className: 'ican-toolbar',
2034
+ style: { top: toolbarTop },
2035
+ },
2036
+ h('span', { className: 'ican-toolbar-title' }, 'Widget Canvas'),
2037
+ addedConfigs.length > 0
2038
+ ? h('span', { className: 'ican-toolbar-count' },
2039
+ addedConfigs.length + (addedConfigs.length === 1 ? ' widget' : ' widgets')
2040
+ )
2041
+ : null,
2042
+ h('div', { className: 'ican-toolbar-spacer' }),
2043
+
2044
+ /* Edit layout toggle — only when canvas has widgets */
2045
+ addedConfigs.length > 0
2046
+ ? h('button', {
2047
+ className: 'ican-btn ican-btn-secondary' + (s.editMode ? ' active' : ''),
2048
+ type: 'button',
2049
+ title: s.editMode ? 'Lock layout' : 'Edit layout',
2050
+ 'aria-label': s.editMode ? 'Lock layout' : 'Edit layout',
2051
+ onClick: self._toggleEditMode,
2052
+ },
2053
+ s.editMode ? icon('lock', 13) : icon('pencil', 13),
2054
+ ' ',
2055
+ s.editMode ? 'Lock Layout' : 'Edit Layout'
2056
+ )
2057
+ : null,
2058
+
2059
+ /* Add Widget — only when canvas has widgets (empty state handles first-add inline) */
2060
+ addedConfigs.length > 0 && unadded.length > 0
2061
+ ? h('button', {
2062
+ className: 'ican-btn ican-btn-primary',
2063
+ type: 'button',
2064
+ onClick: self._openPanel,
2065
+ 'aria-label': 'Add widget',
2066
+ },
2067
+ icon('plus', 14),
2068
+ ' Add Widget'
2069
+ )
2070
+ : null
2071
+ ),
2072
+
2073
+ /* ── Main content ── */
2074
+ h('main', {
2075
+ className: 'ican-main',
2076
+ style: { marginTop: mainMT },
2077
+ },
2078
+ addedConfigs.length === 0
2079
+
2080
+ /* ── State A: widget not registered yet ── */
2081
+ ? (s.registry.size === 0
2082
+ ? h('div', { className: 'ican-waiting' },
2083
+ h('div', { className: 'ican-waiting-icon' }, icon('cube', 30)),
2084
+ h('div', { className: 'ican-waiting-title' }, 'Waiting for your widget…'),
2085
+ h('div', { className: 'ican-waiting-sub' },
2086
+ 'Your widget hasn\'t registered yet. Build it to see it here.'
2087
+ ),
2088
+ h('div', { className: 'ican-waiting-cmd' },
2089
+ icon('squares', 14),
2090
+ h('span', null, 'npm run build')
2091
+ )
2092
+ )
2093
+
2094
+ /* ── State B: registry has items — show inline cards ── */
2095
+ : h('div', { className: 'ican-available' },
2096
+ h('div', { className: 'ican-available-hd' },
2097
+ h('span', { className: 'ican-available-hd-title' }, 'Available Widgets'),
2098
+ h('span', { className: 'ican-available-hd-count' }, unadded.length)
2099
+ ),
2100
+ h('div', { className: 'ican-available-grid' },
2101
+ unadded.map(function (entry) {
2102
+ return h('div', { key: entry.id, className: 'ican-available-card' },
2103
+ h('div', { className: 'ican-available-card-icon' },
2104
+ entry.config.icon ? entry.config.icon : icon('squares', 22)
2105
+ ),
2106
+ h('div', null,
2107
+ h('div', { className: 'ican-available-card-name' },
2108
+ entry.config.name || entry.id
2109
+ ),
2110
+ h('div', { className: 'ican-available-card-id' }, entry.id),
2111
+ entry.config.description
2112
+ ? h('div', { className: 'ican-available-card-desc' }, entry.config.description)
2113
+ : null
2114
+ ),
2115
+ h('div', { className: 'ican-available-card-add' },
2116
+ h('button', {
2117
+ className: 'ican-btn ican-btn-primary',
2118
+ type: 'button',
2119
+ style: { width: '100%', justifyContent: 'center' },
2120
+ onClick: function () { self._addWidget(entry.id); },
2121
+ 'aria-label': 'Add ' + (entry.config.name || entry.id) + ' to canvas',
2122
+ },
2123
+ icon('plus', 15),
2124
+ ' Add to Canvas'
2125
+ )
2126
+ )
2127
+ );
2128
+ })
2129
+ )
2130
+ )
2131
+ )
2132
+
2133
+ /* Widget grid (react-grid-layout) */
2134
+ : RGL
2135
+ ? h(RGL, {
2136
+ className: 'ican-widget-grid' + (s.editMode ? ' ican-edit-mode' : ''),
2137
+ layout: addedConfigs.map(function (cfg) {
2138
+ var sv = s.layouts[cfg.id] || {};
2139
+ var dl = (cfg.configs && cfg.configs.layout) || {};
2140
+ return {
2141
+ i: cfg.id,
2142
+ x: sv.x !== undefined ? sv.x : 0,
2143
+ y: sv.y !== undefined ? sv.y : Infinity,
2144
+ w: sv.w || dl.w || dl.defaultW || 10,
2145
+ h: sv.h || dl.h || dl.defaultH || 8,
2146
+ minW: dl.minW || 4,
2147
+ minH: dl.minH || 4,
2148
+ };
2149
+ }),
2150
+ cols: 30,
2151
+ rowHeight: 30,
2152
+ width: s.gridWidth,
2153
+ isDraggable: s.editMode,
2154
+ isResizable: s.editMode,
2155
+ draggableHandle: '.ican-drag-handle',
2156
+ margin: [12, 12],
2157
+ containerPadding: [0, 0],
2158
+ onLayoutChange: self._onLayoutChange,
2159
+ },
2160
+ addedConfigs.map(function (cfg) {
2161
+ return h('div', { key: cfg.id },
2162
+ h(WidgetCard, {
2163
+ config: cfg,
2164
+ icanContext: ctx,
2165
+ onRemove: self._removeWidget,
2166
+ editMode: s.editMode,
2167
+ })
2168
+ );
2169
+ })
2170
+ )
2171
+
2172
+ /* Fallback CSS grid (when RGL CDN fails) */
2173
+ : h('div', {
2174
+ className: 'ican-widget-grid',
2175
+ style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(380px,1fr))', gap: 16 },
2176
+ },
2177
+ addedConfigs.map(function (cfg) {
2178
+ return h('div', { key: cfg.id, style: { minHeight: 280 } },
2179
+ h(WidgetCard, {
2180
+ config: cfg,
2181
+ icanContext: ctx,
2182
+ onRemove: self._removeWidget,
2183
+ editMode: false,
2184
+ })
2185
+ );
2186
+ })
2187
+ )
2188
+ ),
2189
+
2190
+ /* ── Add Widget Panel ── */
2191
+ h(AddWidgetPanel, {
2192
+ open: s.panelOpen,
2193
+ onClose: self._closePanel,
2194
+ registry: s.registry,
2195
+ addedIds: s.addedIds,
2196
+ onAdd: self._addWidget,
2197
+ })
2198
+ );
2199
+ };
2200
+
2201
+ return App;
2202
+ }());
2203
+
2204
+ /* ─── Bootstrap ───────────────────────────────────────── */
2205
+ function mountApp() {
2206
+ ReactDOM.render(h(App, null), document.getElementById('app'));
2207
+ }
2208
+
2209
+ function loadWidgetScript(cb) {
2210
+ var s = document.createElement('script');
2211
+ s.src = './dist/main.js?t=' + Date.now();
2212
+ s.onload = function () { console.log('[ICan Dev] dist/main.js loaded'); cb(); };
2213
+ s.onerror = function () {
2214
+ console.warn('[ICan Dev] dist/main.js not found — run `npm run build` in your widget folder.');
2215
+ cb();
2216
+ };
2217
+ document.head.appendChild(s);
2218
+ }
2219
+
2220
+ loadWidgetScript(mountApp);
2221
+
2222
+ }());
2223
+ </script>
2224
+ </body>
2225
+ </html>