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,633 @@
1
+ # ICan Widget & Display Theming Guide
2
+
3
+ > **Read this first.** Every widget and display you build must work across all four ICan themes.
4
+ > This guide shows exactly what CSS variables to use, what values they resolve to per theme, and how to handle edge cases like charts and glass effects.
5
+ > All rules apply equally to **widgets** (dashboard tiles) and **displays** (full-screen / kiosk views) — they share the same component library, the same CSS variables, and the same theming architecture.
6
+
7
+ ---
8
+
9
+ ## 30-Second Quick Start
10
+
11
+ These 10 variables cover 90% of every widget:
12
+
13
+ ```scss
14
+ .my-widget {
15
+ background: var(--ican-card-bg); /* widget surface */
16
+ color: var(--ican-primary-text); /* main text */
17
+
18
+ .label { color: var(--ican-secondary-text); }
19
+ .hint { color: var(--ican-tertiary-text); }
20
+
21
+ border: 1px solid var(--ican-border);
22
+
23
+ .badge { color: var(--ican-accent); background: var(--ican-accent-dim); }
24
+
25
+ .ok { color: var(--ican-success); background: var(--ican-success-dim); }
26
+ .fail { color: var(--ican-error); background: var(--ican-error-dim); }
27
+ .warn { color: var(--ican-warning); background: var(--ican-warning-dim); }
28
+
29
+ tr:hover, li:hover { background: var(--ican-hover); }
30
+
31
+ /* Single line — frosts on glass themes, no-op on Dark */
32
+ backdrop-filter: var(--ican-backdrop-filter);
33
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
34
+ }
35
+ ```
36
+
37
+ ---
38
+
39
+ ## The Golden Rule
40
+
41
+ **Never hardcode a color.** Every color in every widget and display must come from a `--ican-*` CSS variable.
42
+
43
+ ```scss
44
+ /* WRONG — breaks on 3 out of 4 themes */
45
+ .card { background: #141417; color: #fafafa; }
46
+
47
+ /* CORRECT — works across all 4 themes automatically */
48
+ .card { background: var(--ican-card-bg); color: var(--ican-primary-text); }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## The Four Themes
54
+
55
+ | Theme | `data-theme` value | Background | When active |
56
+ |-------|--------------------|------------|-------------|
57
+ | **Dark** *(default)* | *(attribute absent)* | Deep near-black `#09090b` | Default — deep near-black surfaces |
58
+ | **Light** | `light` | Crisp white `#f5f5f7` | White surfaces, Apple-inspired |
59
+ | **Glass Dark** | `glass-dark` | Vivid deep-purple/indigo radial gradient on `#08061a` | Frosted glass over a vivid dark gradient |
60
+ | **Glass Light** | `glass-light` | Bright pastel sky gradient (lavender/pink/mint/teal) on `#d0e4ff` | Frosted glass over a soft light gradient |
61
+
62
+ The portal sets `data-theme` on the `[data-module="ican"]` container element — **not on `<html>`**. Your CSS variables remap instantly — your widget repaints with zero JavaScript.
63
+
64
+ ---
65
+
66
+ ## How Your Widget Gets These Variables
67
+
68
+ Your widget's DOM is rendered **inside** the portal's `[data-module="ican"]` container. CSS custom properties cascade down the entire tree:
69
+
70
+ ```
71
+ <div data-module="ican" data-theme="light"> ← portal sets this
72
+ └── globals.scss injects:
73
+ [data-module="ican"][data-theme='light'] { --ican-card-bg: #ffffff; ... }
74
+ └── .widget-cell (portal)
75
+ └── .my-card (your widget)
76
+ uses var(--ican-card-bg) → resolves to #ffffff ✓
77
+ ```
78
+
79
+ You never import or fetch variables — they are always there.
80
+
81
+ > **Note on isolation.** As of the latest globals refactor, every selector — including the `--ican-*` variable definitions — is scoped under `[data-module="ican"]`. The variables exist *only* inside the ICan subtree, so they cannot collide with variables of the same name in other microservices. Your widget's variable lookups still resolve normally because your widget is rendered *inside* `[data-module="ican"]`.
82
+
83
+ ---
84
+
85
+ ## Style Inheritance from the Portal (No Shadow DOM)
86
+
87
+ There is **no Shadow DOM boundary** between the portal and your widget — your widget renders directly inside `[data-module="ican"]`. That's intentional: it lets the `--ican-*` variables cascade in without ceremony. But it also means a small set of generic-element rules from the portal's `globals.scss` reach your widget's elements. Knowing what they are makes overriding deterministic.
88
+
89
+ | Selector applied by portal | What it does | How to override |
90
+ |---|---|---|
91
+ | `button` | Resets `border`, `background`, `cursor`; sets `font-family`; inherits `color` and `font-size` | Just style your own button class — your rules win on specificity |
92
+ | `input, select, textarea, option` | Forces `background-color` and `color` via `!important`; sets default border + padding + radius | Use `!important` on `background`, `color`, `border`, `padding` — or scope under a higher-specificity wrapper class |
93
+ | `h1`–`h6` | Sets `font-family` + tightened margins | Set your own `margin` / `font-family` in your widget CSS |
94
+ | `a` | Sets accent color + underline on hover | Override `color` / `text-decoration` in your `.my-widget a {}` rule |
95
+ | `*, *::before, *::after` | `box-sizing: border-box` | Leave it — this is the convention you want |
96
+
97
+ **The most common gotcha is the `input` rule** because it uses `!important`. If your widget needs a transparent input or a custom palette:
98
+
99
+ ```scss
100
+ .my-widget input {
101
+ background: transparent !important;
102
+ color: var(--ican-primary-text) !important;
103
+ border: 1px solid var(--ican-border) !important;
104
+ padding: 8px 12px !important;
105
+ }
106
+ ```
107
+
108
+ **Class-name collisions are not a concern.** The portal's rules target element types (`button`, `input`, `h1`–`h6`, `a`), not class names. Any class name you invent — `.my-fancy-card`, `.revenue-value`, anything — is yours. ICan has zero rules targeting widget-internal class names.
109
+
110
+ ---
111
+
112
+ ## Complete Variable Reference
113
+
114
+ ### Backgrounds
115
+
116
+ | Variable | Dark | Light | Glass Dark | Glass Light |
117
+ |----------|------|-------|------------|-------------|
118
+ | `--ican-card-bg` | `#141417` | `#ffffff` | `rgba(255,255,255,0.08)` | `rgba(255,255,255,0.48)` |
119
+ | `--ican-primary-bg` | `#141417` | `#ffffff` | `rgba(255,255,255,0.06)` | `rgba(255,255,255,0.42)` |
120
+ | `--ican-secondary-bg` | `#1a1a1f` | `#f0f0f4` | `rgba(255,255,255,0.05)` | `rgba(255,255,255,0.35)` |
121
+ | `--ican-input-bg` | `#1a1a1f` | `#f5f5f7` | `rgba(255,255,255,0.08)` | `rgba(255,255,255,0.50)` |
122
+ | `--ican-hover` | `rgba(255,255,255,0.05)` | `rgba(0,0,0,0.035)` | `rgba(255,255,255,0.08)` | `rgba(255,255,255,0.30)` |
123
+ | `--ican-modal-bg` | `#111114` | `#ffffff` | `rgba(15,10,40,0.70)` | `rgba(255,255,255,0.72)` |
124
+
125
+ ### Text
126
+
127
+ | Variable | What it is | Dark | Light | Glass Dark | Glass Light |
128
+ |----------|------------|------|-------|------------|-------------|
129
+ | `--ican-primary-text` | Headings, values, body | `#fafafa` | `#1d1d1f` | `rgba(255,255,255,0.95)` | `rgba(20,20,40,0.92)` |
130
+ | `--ican-secondary-text` | Labels, captions | `#a1a1aa` | `#6e6e80` | `rgba(220,220,245,0.65)` | `rgba(60,60,100,0.62)` |
131
+ | `--ican-tertiary-text` | Timestamps, hints | `#52525b` | `#aeaeb8` | `rgba(180,180,220,0.40)` | `rgba(100,100,140,0.45)` |
132
+
133
+ ### Borders
134
+
135
+ | Variable | What it is | Dark | Light | Glass Dark | Glass Light |
136
+ |----------|------------|------|-------|------------|-------------|
137
+ | `--ican-border` | Default border | `rgba(255,255,255,0.06)` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.13)` | `rgba(255,255,255,0.55)` |
138
+ | `--ican-border-bright` | Active/hover border | `rgba(255,255,255,0.12)` | `rgba(0,0,0,0.11)` | `rgba(255,255,255,0.22)` | `rgba(255,255,255,0.70)` |
139
+ | `--ican-glass-border` | Glass-optimised border | `rgba(255,255,255,0.10)` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.18)` | `rgba(255,255,255,0.50)` |
140
+ | `--ican-border-focus` | Focus ring | `#7c6aff` | `#5b4cd9` | `rgba(200,190,255,0.90)` | `rgba(90,70,220,0.80)` |
141
+
142
+ ### Accent (purple family)
143
+
144
+ | Variable | What it is | Dark | Light | Glass Dark | Glass Light |
145
+ |----------|------------|------|-------|------------|-------------|
146
+ | `--ican-accent` | Icons, links, highlights | `#7c6aff` | `#5b4cd9` | `#b4a8ff` | `#5b4cd9` |
147
+ | `--ican-accent-hover` | Accent on hover | `#6b59e8` | `#4a3cb5` | `#a099ff` | `#4a3cb5` |
148
+ | `--ican-accent-text` | Accent-coloured text | `#a99aff` | `#4a3cb5` | `rgba(200,190,255,0.95)` | `#4a3cb5` |
149
+ | `--ican-accent-dim` | Badge/icon tint background | `rgba(124,106,255,0.08)` | `rgba(91,76,217,0.07)` | `rgba(180,168,255,0.14)` | `rgba(91,76,217,0.10)` |
150
+
151
+ ### Semantic Colors
152
+
153
+ | Variable | Dark | Light | Glass Dark | Glass Light |
154
+ |----------|------|-------|------------|-------------|
155
+ | `--ican-success` | `#4ade80` | `#16a34a` | `#86efac` | `#16a34a` |
156
+ | `--ican-success-dim` | `rgba(74,222,128,0.08)` | `rgba(22,163,74,0.07)` | `rgba(134,239,172,0.12)` | `rgba(22,163,74,0.10)` |
157
+ | `--ican-error` | `#f87171` | `#dc2626` | `#fca5a5` | `#dc2626` |
158
+ | `--ican-error-dim` | `rgba(248,113,113,0.08)` | `rgba(220,38,38,0.07)` | `rgba(252,165,165,0.12)` | `rgba(220,38,38,0.10)` |
159
+ | `--ican-warning` | `#fbbf24` | `#d97706` | `#fcd34d` | `#d97706` |
160
+ | `--ican-warning-dim` | `rgba(251,191,36,0.08)` | `rgba(217,119,6,0.07)` | `rgba(252,211,77,0.12)` | `rgba(217,119,6,0.10)` |
161
+ | `--ican-info` | `#60a5fa` | `#2563eb` | `#7dd3fc` | `#2563eb` |
162
+
163
+ ### Buttons
164
+
165
+ | Variable | Dark | Light | Glass Dark | Glass Light |
166
+ |----------|------|-------|------------|-------------|
167
+ | `--ican-btn-primary-bg` | `#7c6aff` | `#5b4cd9` | `rgba(140,120,255,0.75)` | `rgba(91,76,217,0.85)` |
168
+ | `--ican-btn-primary-text` | `#ffffff` | `#ffffff` | `#ffffff` | `#ffffff` |
169
+ | `--ican-btn-secondary-bg` | `#1a1a1f` | `#f0f0f5` | `rgba(255,255,255,0.10)` | `rgba(255,255,255,0.55)` |
170
+ | `--ican-btn-secondary-text` | `#fafafa` | `#1d1d1f` | `rgba(255,255,255,0.90)` | `rgba(30,30,60,0.85)` |
171
+
172
+ ### Shadows
173
+
174
+ | Variable | What it is |
175
+ |----------|------------|
176
+ | `--ican-card-shadow` | Default card drop shadow — use on every card |
177
+ | `--ican-card-hover-shadow` | Card shadow on hover |
178
+ | `--ican-modal-shadow` | Modal/overlay shadow |
179
+
180
+ These are multi-layer values set per theme. Always use the variable — never copy/paste the raw box-shadow value.
181
+
182
+ ### Glass / Blur
183
+
184
+ | Variable | Dark | Light | Glass Dark | Glass Light |
185
+ |----------|------|-------|------------|-------------|
186
+ | `--ican-backdrop-filter` | `none` | `blur(20px) saturate(180%)` | `blur(24px) saturate(200%) brightness(1.12)` | `blur(22px) saturate(190%) brightness(1.08)` |
187
+ | `--ican-glass-border` | `rgba(255,255,255,0.10)` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.18)` | `rgba(255,255,255,0.50)` |
188
+
189
+ > **Key insight:** Apply `backdrop-filter: var(--ican-backdrop-filter)` to every card. On Dark it resolves to `none` — zero performance cost. On glass themes it frosts automatically. No JS, no theme checks needed.
190
+
191
+ ### Typography (same across all themes)
192
+
193
+ | Variable | Value | Use for |
194
+ |----------|-------|---------|
195
+ | `--ican-font-brand` | `'Comfortaa', cursive` | Widget title bar, dashboard headings |
196
+ | `--ican-font-body` | `'Inter', system-ui, sans-serif` | Body text, labels, numbers, buttons |
197
+ | `--ican-font-mono` | `'JetBrains Mono', monospace` | IDs, slugs, code, timestamps |
198
+
199
+ ### Border Radius (same across all themes)
200
+
201
+ | Variable | Value | Use for |
202
+ |----------|-------|---------|
203
+ | `--ican-radius-xs` | `4px` | Tiny chips, inner elements |
204
+ | `--ican-radius-sm` | `6px` | Small buttons, tags |
205
+ | `--ican-radius-md` | `10px` | Buttons, inputs, badges |
206
+ | `--ican-radius-lg` | `16px` | Cards, panels |
207
+ | `--ican-radius-xl` | `24px` | Modals, large containers |
208
+
209
+ ### Transitions (same across all themes)
210
+
211
+ | Variable | Value | Use for |
212
+ |----------|-------|---------|
213
+ | `--ican-transition-fast` | `120ms cubic-bezier(0.4, 0, 0.2, 1)` | Hover states, button presses |
214
+ | `--ican-transition-normal` | `220ms cubic-bezier(0.4, 0, 0.2, 1)` | Panel slides, dropdowns |
215
+ | `--ican-transition-slow` | `350ms cubic-bezier(0.4, 0, 0.2, 1)` | Modal open/close |
216
+
217
+ ---
218
+
219
+ ## Common Patterns
220
+
221
+ ### Standard Card
222
+
223
+ ```scss
224
+ .my-card {
225
+ background: var(--ican-card-bg);
226
+ border: 1px solid var(--ican-glass-border);
227
+ border-radius: var(--ican-radius-lg);
228
+ box-shadow: var(--ican-card-shadow);
229
+
230
+ /* Single line handles all 4 themes */
231
+ backdrop-filter: var(--ican-backdrop-filter);
232
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
233
+
234
+ transition: box-shadow var(--ican-transition-fast),
235
+ border-color var(--ican-transition-fast);
236
+
237
+ &:hover {
238
+ box-shadow: var(--ican-card-hover-shadow);
239
+ border-color: var(--ican-border-bright);
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### Accent Badge / Icon Chip
245
+
246
+ ```tsx
247
+ <span style={{
248
+ display: 'inline-flex', alignItems: 'center', gap: 4,
249
+ color: 'var(--ican-accent)',
250
+ background: 'var(--ican-accent-dim)',
251
+ padding: '2px 8px', borderRadius: 99,
252
+ fontSize: 11, fontWeight: 600,
253
+ }}>
254
+ Active
255
+ </span>
256
+ ```
257
+
258
+ ### Status Badge (ok / error)
259
+
260
+ ```tsx
261
+ const StatusBadge = ({ ok }: { ok: boolean }) => (
262
+ <span style={{
263
+ display: 'inline-flex', alignItems: 'center', gap: 4,
264
+ color: ok ? 'var(--ican-success)' : 'var(--ican-error)',
265
+ background: ok ? 'var(--ican-success-dim)' : 'var(--ican-error-dim)',
266
+ padding: '2px 8px', borderRadius: 99,
267
+ fontSize: 11, fontWeight: 600,
268
+ }}>
269
+ {ok ? 'Operational' : 'Degraded'}
270
+ </span>
271
+ );
272
+ ```
273
+
274
+ ### Interactive Row / List Item
275
+
276
+ ```scss
277
+ .row {
278
+ display: flex;
279
+ align-items: center;
280
+ padding: 8px 10px;
281
+ border-radius: var(--ican-radius-sm);
282
+ transition: background var(--ican-transition-fast);
283
+
284
+ &:hover { background: var(--ican-hover); }
285
+ }
286
+ ```
287
+
288
+ ### Shimmer Skeleton Loader (Google/Instagram style)
289
+
290
+ ```scss
291
+ @keyframes sk-shimmer {
292
+ 0% { background-position: -600px 0; }
293
+ 100% { background-position: 600px 0; }
294
+ }
295
+
296
+ .sk-block {
297
+ border-radius: var(--ican-radius-sm);
298
+ background: linear-gradient(
299
+ 90deg,
300
+ var(--ican-secondary-bg) 25%, /* dark base */
301
+ var(--ican-hover) 37%, /* lighter sweep */
302
+ var(--ican-secondary-bg) 63%
303
+ );
304
+ background-size: 600px 100%;
305
+ animation: sk-shimmer 1.6s ease-in-out infinite;
306
+ }
307
+
308
+ /* Size variants — adjust to match your real content */
309
+ .sk-block--title { height: 13px; width: 45%; }
310
+ .sk-block--value { height: 28px; width: 70%; }
311
+ .sk-block--label { height: 11px; width: 55%; }
312
+ .sk-block--badge { height: 18px; width: 80px; border-radius: 20px; }
313
+ .sk-block--avatar { width: 34px; height: 34px; border-radius: 50%; }
314
+
315
+ /* Wave effect across multiple bars */
316
+ @for $i from 1 through 12 {
317
+ .sk-bar-col:nth-child(#{$i}) .sk-bar {
318
+ animation-delay: #{($i - 1) * 0.06}s;
319
+ }
320
+ }
321
+ ```
322
+
323
+ > **Tip:** Mirror your real widget layout exactly in skeleton form — same grid, same sections, same relative sizes. This prevents layout shift when the real content loads.
324
+
325
+ ### Extra Glass Depth (optional)
326
+
327
+ For richer frosting on glass themes beyond what `var(--ican-backdrop-filter)` provides:
328
+
329
+ ```scss
330
+ /* Base — all 4 themes */
331
+ .my-card {
332
+ background: var(--ican-card-bg);
333
+ backdrop-filter: var(--ican-backdrop-filter);
334
+ -webkit-backdrop-filter: var(--ican-backdrop-filter);
335
+ }
336
+
337
+ /* Override — extra blur on glass themes only */
338
+ [data-theme='glass-dark'] .my-card {
339
+ background: rgba(255, 255, 255, 0.06) !important;
340
+ backdrop-filter: blur(40px) saturate(200%) !important;
341
+ -webkit-backdrop-filter: blur(40px) saturate(200%) !important;
342
+ border-color: rgba(255, 255, 255, 0.14);
343
+ }
344
+
345
+ [data-theme='glass-light'] .my-card {
346
+ background: rgba(255, 255, 255, 0.22) !important;
347
+ backdrop-filter: blur(40px) saturate(240%) brightness(1.15) !important;
348
+ -webkit-backdrop-filter: blur(40px) saturate(240%) brightness(1.15) !important;
349
+ border-color: rgba(255, 255, 255, 0.70);
350
+ }
351
+ ```
352
+
353
+ > **Why `!important`?** The portal's `.widget-cell` sets a background. `!important` ensures your inner card overrides it cleanly on glass themes.
354
+
355
+ ---
356
+
357
+ ## Glass Background Colors (What Your Cards Frost Over)
358
+
359
+ Understanding the exact backgrounds is critical — they define how your frosted cards look.
360
+
361
+ ### Glass Dark — deep purple/indigo gradient
362
+ ```
363
+ Body background:
364
+ Soft violet blob top-left rgba(100, 70, 200, 0.40)
365
+ Muted blue blob top-right rgba(50, 100, 200, 0.30)
366
+ Deep indigo blob btm-left rgba(80, 50, 180, 0.35)
367
+ Slate blue blob btm-right rgba(40, 80, 160, 0.25)
368
+ Base color: #08061a (very dark near-black blue)
369
+ ```
370
+ Your frosted dark cards will have a luminous deep-purple/blue color cast.
371
+
372
+ ### Glass Light — pastel sky gradient
373
+ ```
374
+ Body background:
375
+ Lavender blob behind-sidebar rgba(160, 130, 255, 0.65)
376
+ Sky blue blob top-center rgba(100, 180, 255, 0.75)
377
+ Rose pink blob sidebar-mid rgba(255, 120, 180, 0.55)
378
+ Mint teal blob center-right rgba(80, 220, 220, 0.40)
379
+ Peach blob btm-left rgba(255, 170, 120, 0.50)
380
+ Soft pink blob btm-right rgba(255, 140, 200, 0.45)
381
+ Warm gold blob center rgba(255, 200, 120, 0.20)
382
+ Base color: #d0e4ff (light sky blue)
383
+ ```
384
+ Your frosted light cards will have a multi-hued pastel/warm colour cast.
385
+
386
+ > **Design implication:** On glass themes your cards are never neutral grey — they take on the colour of whatever gradient is behind them. Design for this. A card's background colour is driven by its position on screen, not a fixed hex value.
387
+
388
+ ---
389
+
390
+ ## Glass Frosting Architecture — The Two-Layer Rule
391
+
392
+ > **This is the #1 glass-theme bug.** Read this before writing any glass-specific CSS.
393
+
394
+ ### How glass rendering works
395
+
396
+ The portal (and the dev harness) uses a two-layer stack:
397
+
398
+ ```
399
+ Body gradient ← vivid radial blobs live here
400
+ └── Outer container ← .widget-cell (portal) / .ican-widget-card (harness)
401
+ └── Your widget's cards ← .my-card, .my-inner-panel, etc.
402
+ ```
403
+
404
+ **Only ONE layer should do heavy frosting.** That layer is your widget's inner cards.
405
+
406
+ ### The correct glass hierarchy
407
+
408
+ | Layer | Element | Background | Blur | Role |
409
+ |-------|---------|-----------|------|------|
410
+ | 1 — Body | `<body>` | Vivid radial gradient | — | Light source, colour |
411
+ | 2 — Outer cell | `.widget-cell` / `.ican-widget-card` | Nearly transparent (`rgba(255,255,255,0.03–0.06)`) | `blur(2px)` | Structural wrapper only — let the gradient show through |
412
+ | 3 — Inner cards | `.my-card` etc. | `var(--ican-card-bg)` → `rgba(255,255,255,0.08–0.48)` | `blur(22–40px)` | **This is where the frosted glass effect happens** |
413
+
414
+ ### The anti-pattern: double-frosting
415
+
416
+ ```
417
+ Body gradient
418
+ └── .ican-widget-card backdrop-filter: blur(32px) ← ❌ first frost
419
+ └── .my-card backdrop-filter: blur(40px) ← ❌ second frost on top
420
+ ```
421
+
422
+ Result: both layers pull colour from the gradient and blur it. By the time light reaches the inner card, the gradient underneath is already a washed-out grey smear. The inner card then blurs that smear — producing an opaque, uniform, muddy surface with no colour depth.
423
+
424
+ ### The correct pattern: single-layer frosting
425
+
426
+ ```
427
+ Body gradient
428
+ └── .ican-widget-card background: rgba(255,255,255,0.03); blur(2px) ← ✅ nearly transparent
429
+ └── .my-card backdrop-filter: blur(40px) ← ✅ all frosting here
430
+ ```
431
+
432
+ Result: the gradient colours shine through the transparent outer shell. The inner card frosts them into a rich, luminous, coloured panel.
433
+
434
+ ### What the harness and portal already do for you
435
+
436
+ The portal (`globals.scss`) and every harness `index.html` already apply the correct outer-container rules:
437
+
438
+ ```css
439
+ /* Portal — globals.scss */
440
+ [data-theme='glass-light'] .widget-cell {
441
+ background: rgba(255, 255, 255, 0.06) !important;
442
+ backdrop-filter: blur(2px);
443
+ }
444
+ [data-theme='glass-dark'] .widget-cell {
445
+ background: rgba(255, 255, 255, 0.03) !important;
446
+ backdrop-filter: blur(2px);
447
+ }
448
+
449
+ /* Harness — index.html */
450
+ [data-theme='glass-light'] .ican-widget-card {
451
+ background: rgba(255, 255, 255, 0.06) !important;
452
+ backdrop-filter: blur(2px);
453
+ }
454
+ [data-theme='glass-dark'] .ican-widget-card {
455
+ background: rgba(255, 255, 255, 0.03) !important;
456
+ backdrop-filter: blur(2px);
457
+ }
458
+ ```
459
+
460
+ **You don't touch these.** Your only job is to apply frosting on your own inner cards — via `backdrop-filter: var(--ican-backdrop-filter)` (or the "Extra Glass Depth" override pattern above).
461
+
462
+ ### Quick diagnostic
463
+
464
+ If your widget looks opaque / grey / washed-out on glass themes:
465
+
466
+ 1. Open DevTools → inspect `.ican-widget-card` or `.widget-cell`
467
+ 2. Check `backdrop-filter` — it should be `blur(2px)`, not `blur(32px)` or higher
468
+ 3. If it's high, you or another stylesheet has re-added frosting to the outer container — remove it
469
+ 4. Inspect your inner `.my-card` — it should have `blur(22–40px)`
470
+ 5. Confirm the body gradient is visible through the nearly-transparent outer shell
471
+
472
+ ---
473
+
474
+ ## Chart Colors — Special Case
475
+
476
+ SVG elements (Recharts, D3) **cannot read CSS variables**. Use `icanContext.themeType` to derive chart colors in JavaScript:
477
+
478
+ ```tsx
479
+ function getChartColors(themeType?: string) {
480
+ const isLight = themeType === 'Light' || themeType === 'Glass-Light';
481
+ return {
482
+ grid: isLight ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)',
483
+ axis: isLight ? '#6e6e80' : '#a1a1aa',
484
+ cursor: isLight ? 'rgba(0,0,0,0.035)' : 'rgba(255,255,255,0.05)',
485
+ barPrimary: isLight ? '#5b4cd9' : '#7c6aff',
486
+ barActive: isLight ? '#4a3cb5' : '#a99aff',
487
+ line: isLight ? '#5b4cd9' : '#a99aff',
488
+ area: isLight ? 'rgba(91,76,217,0.12)' : 'rgba(124,106,255,0.12)',
489
+ tooltip: {
490
+ bg: isLight ? '#ffffff' : '#141417',
491
+ border: isLight ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.08)',
492
+ text: isLight ? '#1d1d1f' : '#fafafa',
493
+ },
494
+ };
495
+ }
496
+
497
+ const MyWidget: React.FC<IWidgetProps> = ({ icanContext }) => {
498
+ const c = getChartColors(icanContext?.themeType);
499
+
500
+ return (
501
+ <ComposedChart data={data}>
502
+ <CartesianGrid stroke={c.grid} vertical={false} />
503
+ <XAxis tick={{ fill: c.axis, fontSize: 11 }} />
504
+ <YAxis tick={{ fill: c.axis, fontSize: 11 }} />
505
+ <Tooltip
506
+ cursor={{ fill: c.cursor }}
507
+ contentStyle={{
508
+ background: c.tooltip.bg,
509
+ border: `1px solid ${c.tooltip.border}`,
510
+ color: c.tooltip.text,
511
+ borderRadius: 10,
512
+ }}
513
+ />
514
+ <Bar dataKey="value" fill={c.barPrimary} radius={[4, 4, 0, 0]} />
515
+ </ComposedChart>
516
+ );
517
+ };
518
+ ```
519
+
520
+ > **Why?** CSS variables are resolved by the browser's style engine. SVG `fill` and `stroke` attributes are set via React props (JavaScript strings) — the browser never runs CSS variable resolution on them.
521
+
522
+ ---
523
+
524
+ ## Reading the Current Theme in JavaScript
525
+
526
+ The portal passes `themeType` and `themeName` directly on `icanContext`. Use these — do not read the DOM yourself.
527
+
528
+ ```tsx
529
+ const MyWidget: React.FC<IWidgetProps> = ({ icanContext }) => {
530
+ // themeType: 'Dark' | 'Light' | 'Glass-Dark' | 'Glass-Light'
531
+ const themeType = icanContext?.themeType ?? 'Dark';
532
+ const themeName = icanContext?.themeName; // e.g. 'Dark', 'My Custom Theme'
533
+
534
+ const isLight = themeType === 'Light' || themeType === 'Glass-Light';
535
+ const isGlass = themeType === 'Glass-Dark' || themeType === 'Glass-Light';
536
+ };
537
+ ```
538
+
539
+ **Use `icanContext.themeType` for chart colors and conditional JS logic. Use CSS variables for everything else.**
540
+
541
+ > If `icanContext` is unavailable (e.g. dev harness without a portal), fall back to the DOM:
542
+ > ```ts
543
+ > const container = document.querySelector('[data-module="ican"]');
544
+ > const fallback = container?.getAttribute('data-theme') ?? 'dark';
545
+ > ```
546
+ > Note: `data-theme` is on the `[data-module="ican"]` container, not on `<html>` or `<body>`.
547
+
548
+ ---
549
+
550
+ ## Debugging Variables in DevTools
551
+
552
+ To see what a variable resolves to on the current theme:
553
+
554
+ 1. Open browser **DevTools → Elements**
555
+ 2. Select `<html>` in the element tree
556
+ 3. Go to **Computed** tab → scroll to **CSS Variables** (or search `--ican-`)
557
+ 4. Switch theme in the portal header — values update live
558
+
559
+ ---
560
+
561
+ ## Pre-Publish Checklist
562
+
563
+ Applies to both **widgets** and **displays**:
564
+
565
+ - [ ] Search SCSS/CSS for `#`, `rgb(`, `hsl(` — replace every hit with `var(--ican-*)`
566
+ - [ ] All cards use `backdrop-filter: var(--ican-backdrop-filter)` — disables on Dark automatically
567
+ - [ ] **No double-frosting** — inner cards frost, outer `.ican-widget-card` / `.widget-cell` stays at `blur(2px)` — see Glass Frosting Architecture above
568
+ - [ ] Chart colors derived from `icanContext.themeType`, not CSS variables
569
+ - [ ] Skeleton loader uses `var(--ican-secondary-bg)` + `var(--ican-hover)` for shimmer colors
570
+ - [ ] Loading, error, and empty states all use semantic variables
571
+ - [ ] Tested on all 4 themes — switch in the portal header to verify
572
+
573
+ Widget-specific:
574
+ - [ ] `bundle.json` has `defaultW`, `defaultH`, `minW`, `minH` set
575
+
576
+ Display-specific *(coming soon)*:
577
+ - [ ] Display fills `100vw × 100vh` — background is `var(--ican-bg)`
578
+ - [ ] No hardcoded pixel dimensions that assume a particular screen size
579
+ - [ ] `display.json` has `id`, `label`, `entry` set
580
+
581
+ Both:
582
+ - [ ] No `console.log` in production code
583
+
584
+ ---
585
+
586
+ ## `IContextProvider` — Full Interface
587
+
588
+ ```typescript
589
+ interface IContextProvider {
590
+ environment: 'dev' | 'prod'; // 'dev' in local harness, 'prod' in portal
591
+ userKey: string;
592
+ language: string; // e.g. 'en'
593
+ root: string;
594
+ orchUrl?: string;
595
+ apiKey?: string;
596
+
597
+ // Fetch data from Orch
598
+ executeAction(
599
+ model: string,
600
+ action: string,
601
+ params?: unknown,
602
+ options?: IActionOptions
603
+ ): Promise<unknown>;
604
+
605
+ executeService(
606
+ app: string,
607
+ service: string,
608
+ params: unknown,
609
+ options?: IActionOptions
610
+ ): Promise<unknown>;
611
+
612
+ fireEvent(eventId: string): Promise<void>;
613
+ hasAppRole(app: string, role: string): boolean;
614
+ $L(code: string, params?: Record<string, string>): string;
615
+
616
+ // Theme — use for chart colors and conditional logic
617
+ themeName?: string; // e.g. 'Dark', 'Light', 'Glass Dark', 'Glass Light'
618
+ themeType?: 'Dark' | 'Light' | 'Glass-Dark' | 'Glass-Light';
619
+ }
620
+
621
+ interface IActionOptions {
622
+ json?: boolean; // parse response as JSON
623
+ key?: string; // dedup key for batching
624
+ cancelPrevious?: boolean; // cancel in-flight call with same key
625
+ executeImmediately?: boolean; // skip the 100ms batch window
626
+ }
627
+ ```
628
+
629
+ ---
630
+
631
+ *This file is placed in your project root by `infaira-canvas init` and `infaira-canvas init-display`, and is also available in the `infaira-canvas` npm package under `templates/`.*
632
+ *Applies to widgets now. Display support is coming soon — theming rules are identical.*
633
+ *Applies to `infaira-canvas` v0.1.3+*