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.
- package/README.md +264 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.js +647 -0
- package/dist/commands/upload.d.ts +8 -0
- package/dist/commands/upload.js +164 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -0
- package/package.json +44 -0
- package/templates/ICan-Customizing-Components.md +195 -0
- package/templates/ICan-Widget-Development-Guide.md +500 -0
- package/templates/ICan-Widget-Styling-Patterns.md +890 -0
- package/templates/ICan-Widget-Theming-Guide.md +633 -0
- package/templates/README.md +127 -0
- package/templates/designer.d.ts +468 -0
- package/templates/ican.d.ts +763 -0
- package/templates/index.html +2225 -0
- package/templates/resources/favicon.ico +2 -0
- package/templates/resources/ican-components.js +1734 -0
- package/templates/resources/infaira-icon.png +0 -0
- package/templates/resources/infaira-logo.png +0 -0
- package/templates/site.webmanifest +17 -0
- package/templates/ui.html +1670 -0
|
@@ -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+*
|