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,1734 @@
1
+ /**
2
+ * ICan Components — InfAIra Canvas v2.0
3
+ * Complete local-dev mirror of window.ICanComponents (matches widget-bridge.ts production export).
4
+ * Pure vanilla JS IIFE — no imports, no CDN fetches (except MapComponent's lazy Leaflet).
5
+ * All colours use CSS custom properties.
6
+ *
7
+ * IMPORTANT: Prop signatures MUST match widget-bridge.ts (the canonical runtime).
8
+ * - DataTable: `rows` (NOT `data`), columns use `key`/`label`/`render` (NOT `title`/`renderColumn`)
9
+ * - Select: `value` (NOT `selected`)
10
+ * - Tabs: `tabs[].key` + `activeKey` + `onChange` (controlled, no internal selection)
11
+ * - Modal: `open` + `onClose` + `title` + `footer` + `width`
12
+ * - Badge: `label` + `variant` + `dot` (no children)
13
+ * - IconButton: `type` (limited set), `active`, `tooltip`
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var R = window.React;
19
+ if (!R) { console.error('[ICan] React must be loaded before ican-components.js'); return; }
20
+
21
+ var useState = R.useState;
22
+ var useEffect = R.useEffect;
23
+ var useRef = R.useRef;
24
+
25
+ function el() { return R.createElement.apply(R, arguments); }
26
+
27
+ /* ─────────────────────────────────────────────────────
28
+ * LAYOUT / SHELL
29
+ * ───────────────────────────────────────────────────── */
30
+
31
+ function WidgetWrapper(props) {
32
+ return el('div', {
33
+ className: props.className,
34
+ style: Object.assign({ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }, props.style || {})
35
+ }, props.children);
36
+ }
37
+
38
+ function Loading(props) {
39
+ return el('div', {
40
+ className: props.className,
41
+ style: Object.assign({ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', color: 'var(--ican-secondary-text)', gap: '8px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
42
+ },
43
+ el('span', { style: { fontSize: '18px' } }, '⟳'),
44
+ el('span', null, props.label || 'Loading...')
45
+ );
46
+ }
47
+
48
+ function TitleBar(props) {
49
+ return el('div', {
50
+ className: props.className,
51
+ style: Object.assign({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', borderBottom: '1px solid var(--ican-border)', minHeight: '44px', flexShrink: 0 }, props.style || {})
52
+ },
53
+ el('div', null,
54
+ el('div', { style: { fontFamily: 'var(--ican-font-brand)', fontWeight: 600, fontSize: '12px', color: 'var(--ican-primary-text)', textTransform: 'uppercase', letterSpacing: '0.06em' } }, props.title),
55
+ props.subtitle ? el('div', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', marginTop: '2px' } }, props.subtitle) : null
56
+ ),
57
+ props.children ? el('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, props.children) : null
58
+ );
59
+ }
60
+
61
+ function FilterPanel(props) {
62
+ return el('div', {
63
+ className: props.className,
64
+ style: Object.assign({ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }, props.style || {})
65
+ }, props.children);
66
+ }
67
+
68
+ function ErrorDisplay(props) {
69
+ return el('div', {
70
+ className: props.className,
71
+ style: Object.assign({ padding: '16px', color: 'var(--ican-error)', background: 'rgba(237,48,131,0.1)', borderRadius: 'var(--ican-radius-md)', fontSize: '13px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
72
+ },
73
+ '⚠ ',
74
+ props.message
75
+ );
76
+ }
77
+
78
+ function Card(props) {
79
+ var hasSlots = props.header != null || props.footer != null;
80
+ if (!hasSlots) {
81
+ return el('div', {
82
+ className: props.className,
83
+ style: Object.assign({ background: 'var(--ican-card-bg)', borderRadius: 'var(--ican-radius-md)', border: '1px solid var(--ican-border)', padding: '12px' }, props.style || {})
84
+ }, props.children);
85
+ }
86
+ return el('div', {
87
+ className: props.className,
88
+ style: Object.assign({ background: 'var(--ican-card-bg)', borderRadius: 'var(--ican-radius-md)', border: '1px solid var(--ican-border)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }, props.style || {})
89
+ },
90
+ props.header ? el('div', { style: { padding: '10px 14px', borderBottom: '1px solid var(--ican-border)', fontWeight: 600, fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)' } }, props.header) : null,
91
+ el('div', { style: { padding: '12px', flex: 1 } }, props.children),
92
+ props.footer ? el('div', { style: { padding: '10px 14px', borderTop: '1px solid var(--ican-border)', fontSize: '12px', color: 'var(--ican-secondary-text)', fontFamily: 'var(--ican-font-body)' } }, props.footer) : null
93
+ );
94
+ }
95
+
96
+ function DataList(props) {
97
+ var items = props.items || [];
98
+ return el('dl', { className: props.className, style: Object.assign({ margin: 0, padding: 0 }, props.style || {}) },
99
+ items.map(function (item) {
100
+ return el('div', {
101
+ key: item.label,
102
+ style: { display: 'flex', justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px solid var(--ican-border)', fontSize: '13px' }
103
+ },
104
+ el('dt', { style: { color: 'var(--ican-secondary-text)' } }, item.label),
105
+ el('dd', { style: { color: 'var(--ican-primary-text)', fontWeight: 500 } }, String(item.value))
106
+ );
107
+ })
108
+ );
109
+ }
110
+
111
+ /* ─────────────────────────────────────────────────────
112
+ * BUTTONS
113
+ * ───────────────────────────────────────────────────── */
114
+
115
+ function Button(props) {
116
+ var variant = props.variant || 'primary';
117
+ var size = props.size || 'md';
118
+ var disabled = !!props.disabled;
119
+ var loading = !!props.loading;
120
+ var bgMap = { primary: 'var(--ican-accent)', secondary: 'transparent', danger: 'var(--ican-error)', ghost: 'transparent' };
121
+ var colorMap = { primary: '#fff', secondary: 'var(--ican-primary-text)', danger: '#fff', ghost: 'var(--ican-secondary-text)' };
122
+ var borderMap = { primary: 'none', secondary: '1px solid var(--ican-border)', danger: 'none', ghost: 'none' };
123
+ var paddingMap = { sm: '4px 10px', md: '7px 16px', lg: '10px 22px' };
124
+ var fontSizeMap = { sm: '11px', md: '13px', lg: '15px' };
125
+ return el('button', {
126
+ className: props.className,
127
+ onClick: disabled || loading ? undefined : props.onClick,
128
+ disabled: disabled || loading,
129
+ style: Object.assign({
130
+ background: bgMap[variant], color: colorMap[variant], border: borderMap[variant],
131
+ borderRadius: 'var(--ican-radius-md)', padding: paddingMap[size], fontSize: fontSizeMap[size],
132
+ fontFamily: 'var(--ican-font-body)', cursor: disabled || loading ? 'not-allowed' : 'pointer',
133
+ opacity: disabled ? 0.5 : 1, display: 'inline-flex', alignItems: 'center', gap: '6px',
134
+ lineHeight: 1.4, transition: 'opacity 0.15s, background var(--ican-transition-fast)'
135
+ }, props.style || {})
136
+ },
137
+ loading ? el('span', { style: { display: 'inline-block', animation: 'ican-spin 0.8s linear infinite', fontSize: '14px' } }, '↻')
138
+ : (props.icon ? el('span', { style: { display: 'inline-flex', alignItems: 'center', fontSize: '14px' } }, props.icon) : null),
139
+ props.label
140
+ );
141
+ }
142
+
143
+ function AsyncButton(props) {
144
+ var _s = useState(false);
145
+ var loading = _s[0]; var setLoading = _s[1];
146
+ var variant = props.variant || 'primary';
147
+ var bgMap = { primary: 'var(--ican-btn-primary-bg, var(--ican-accent))', secondary: 'var(--ican-btn-secondary-bg, transparent)', danger: 'var(--ican-error)' };
148
+ var colorMap = { primary: 'var(--ican-btn-primary-text, #fff)', secondary: 'var(--ican-btn-secondary-text, var(--ican-primary-text))', danger: '#fff' };
149
+ var borderMap = { primary: 'none', secondary: '1px solid var(--ican-border)', danger: 'none' };
150
+
151
+ function handleClick() {
152
+ if (loading || props.disabled) return;
153
+ setLoading(true);
154
+ Promise.resolve().then(function () { return props.onClick(); }).finally(function () { setLoading(false); });
155
+ }
156
+
157
+ return el('button', {
158
+ className: props.className,
159
+ onClick: handleClick,
160
+ disabled: props.disabled || loading,
161
+ style: Object.assign({ background: bgMap[variant], color: colorMap[variant], border: borderMap[variant], borderRadius: 'var(--ican-radius-md)', padding: '7px 16px', fontSize: '13px', fontFamily: 'var(--ican-font-body)', cursor: props.disabled || loading ? 'not-allowed' : 'pointer', opacity: props.disabled ? 0.5 : 1, display: 'inline-flex', alignItems: 'center', gap: '6px', transition: 'opacity var(--ican-transition-fast)' }, props.style || {})
162
+ },
163
+ loading ? el('span', { style: { display: 'inline-block', animation: 'ican-spin 0.8s linear infinite', fontSize: '14px' } }, '↻')
164
+ : (props.icon ? el('span', null, props.icon) : null),
165
+ loading ? (props.loadingLabel || 'Loading…') : props.label
166
+ );
167
+ }
168
+
169
+ function ConfirmButton(props) {
170
+ var _s = useState('idle');
171
+ var stage = _s[0]; var setStage = _s[1];
172
+ var timeoutRef = useRef(null);
173
+ var variant = props.variant || 'primary';
174
+
175
+ function handleClick() {
176
+ if (props.disabled || stage === 'loading') return;
177
+ if (stage === 'idle') {
178
+ setStage('confirm');
179
+ timeoutRef.current = setTimeout(function () { setStage('idle'); }, 3000);
180
+ } else if (stage === 'confirm') {
181
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
182
+ setStage('loading');
183
+ Promise.resolve().then(function () { return props.onConfirm(); }).finally(function () { setStage('idle'); });
184
+ }
185
+ }
186
+
187
+ useEffect(function () { return function () { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []);
188
+
189
+ var bg = stage === 'confirm' ? 'var(--ican-warning)' : variant === 'danger' ? 'var(--ican-error)' : 'var(--ican-btn-primary-bg, var(--ican-accent))';
190
+ return el('button', {
191
+ className: props.className,
192
+ onClick: handleClick,
193
+ onBlur: function () { if (stage === 'confirm') { if (timeoutRef.current) clearTimeout(timeoutRef.current); setStage('idle'); } },
194
+ disabled: props.disabled || stage === 'loading',
195
+ style: Object.assign({ background: bg, color: '#fff', border: 'none', borderRadius: 'var(--ican-radius-md)', padding: '7px 16px', fontSize: '13px', fontFamily: 'var(--ican-font-body)', cursor: props.disabled ? 'not-allowed' : 'pointer', opacity: props.disabled ? 0.5 : 1, display: 'inline-flex', alignItems: 'center', gap: '6px', transition: 'background var(--ican-transition-fast)' }, props.style || {})
196
+ },
197
+ stage === 'loading' ? el('span', { style: { fontSize: '14px' } }, '↻') : null,
198
+ stage === 'idle' ? props.label : stage === 'confirm' ? (props.confirmLabel || 'Are you sure?') : 'Loading…'
199
+ );
200
+ }
201
+
202
+ var ICON_MAP = {
203
+ search: '⌕', close: '✕', done: '✓', edit: '✎', delete: '✕', add: '+', filter: '≡',
204
+ 'arrow-up': '↑', 'arrow-down': '↓', 'arrow-left': '←', 'arrow-right': '→',
205
+ pin: '⊕', copy: '⧉', refresh: '↻'
206
+ };
207
+
208
+ function IconButton(props) {
209
+ var size = props.size || 'md';
210
+ var sizeMap = { sm: '22px', md: '28px', lg: '34px' };
211
+ var fontSizeMap = { sm: '12px', md: '14px', lg: '18px' };
212
+ var disabled = !!props.disabled;
213
+ var active = !!props.active;
214
+ return el('button', {
215
+ className: props.className,
216
+ onClick: disabled ? undefined : props.onClick,
217
+ disabled: disabled,
218
+ title: props.tooltip,
219
+ style: Object.assign({
220
+ width: sizeMap[size], height: sizeMap[size], display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
221
+ background: active ? 'var(--ican-accent)' : 'transparent',
222
+ color: active ? 'var(--ican-btn-primary-text, #fff)' : 'var(--ican-secondary-text)',
223
+ border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-sm)',
224
+ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.4 : 1,
225
+ fontSize: fontSizeMap[size],
226
+ transition: 'background var(--ican-transition-fast), color var(--ican-transition-fast)',
227
+ fontFamily: 'var(--ican-font-body)'
228
+ }, props.style || {})
229
+ }, ICON_MAP[props.type] || props.type);
230
+ }
231
+
232
+ /* ─────────────────────────────────────────────────────
233
+ * FORM INPUTS
234
+ * ───────────────────────────────────────────────────── */
235
+
236
+ function Input(props) {
237
+ var type = props.type || 'text';
238
+ var disabled = !!props.disabled;
239
+ return el('div', {
240
+ className: props.className,
241
+ style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
242
+ },
243
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
244
+ el('input', {
245
+ type: type, value: props.value, placeholder: props.placeholder, disabled: disabled,
246
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
247
+ style: {
248
+ background: 'var(--ican-card-bg)',
249
+ border: props.error ? '1px solid var(--ican-error)' : '1px solid var(--ican-border)',
250
+ borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px',
251
+ color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)',
252
+ outline: 'none', opacity: disabled ? 0.5 : 1, width: '100%', boxSizing: 'border-box'
253
+ }
254
+ }),
255
+ props.error ? el('span', { style: { fontSize: '11px', color: 'var(--ican-error)' } }, props.error) : null
256
+ );
257
+ }
258
+
259
+ function TextArea(props) {
260
+ var rows = props.rows || 4;
261
+ var disabled = !!props.disabled;
262
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
263
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
264
+ el('textarea', {
265
+ value: props.value, placeholder: props.placeholder, rows: rows, disabled: disabled, maxLength: props.maxLength,
266
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
267
+ style: { background: 'var(--ican-input-bg, var(--ican-card-bg))', border: props.error ? '1px solid var(--ican-error)' : '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', opacity: disabled ? 0.5 : 1, width: '100%', boxSizing: 'border-box', resize: 'both', lineHeight: 1.5 }
268
+ }),
269
+ el('div', { style: { display: 'flex', justifyContent: 'space-between' } },
270
+ props.error ? el('span', { style: { fontSize: '11px', color: 'var(--ican-error)' } }, props.error) : el('span', null),
271
+ props.maxLength ? el('span', { style: { fontSize: '11px', color: 'var(--ican-tertiary-text, var(--ican-secondary-text))' } }, (props.value || '').length + '/' + props.maxLength) : null
272
+ )
273
+ );
274
+ }
275
+
276
+ function Select(props) {
277
+ var disabled = !!props.disabled;
278
+ var value = props.value != null ? props.value : '';
279
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
280
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
281
+ el('select', {
282
+ value: value, disabled: disabled,
283
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
284
+ style: {
285
+ background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)',
286
+ borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px',
287
+ color: value ? 'var(--ican-primary-text)' : 'var(--ican-secondary-text)',
288
+ fontFamily: 'var(--ican-font-body)', outline: 'none', opacity: disabled ? 0.5 : 1,
289
+ width: '100%', cursor: disabled ? 'not-allowed' : 'pointer'
290
+ }
291
+ },
292
+ props.placeholder ? el('option', { value: '', disabled: true }, props.placeholder) : null,
293
+ (props.options || []).map(function (opt) {
294
+ return el('option', { key: opt.value, value: opt.value }, opt.label);
295
+ })
296
+ )
297
+ );
298
+ }
299
+
300
+ function Checkbox(props) {
301
+ var type = props.type || 'default';
302
+ var disabled = !!props.disabled;
303
+ var checked = !!props.checked;
304
+ var baseStyle = { display: 'inline-flex', alignItems: 'center', gap: '8px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1, fontFamily: 'var(--ican-font-body)', fontSize: '13px', color: 'var(--ican-primary-text)' };
305
+ function click() { if (!disabled && props.onChange) props.onChange(!checked); }
306
+
307
+ if (type === 'switch') {
308
+ return el('div', { className: props.className, style: Object.assign({}, baseStyle, props.style || {}), onClick: click },
309
+ el('div', { style: { width: '36px', height: '20px', borderRadius: '10px', background: checked ? 'var(--ican-accent)' : 'var(--ican-border)', position: 'relative', transition: 'background var(--ican-transition-fast)', flexShrink: 0 } },
310
+ el('div', { style: { position: 'absolute', top: '3px', left: checked ? '19px' : '3px', width: '14px', height: '14px', borderRadius: '50%', background: '#fff', transition: 'left var(--ican-transition-fast)', boxShadow: '0 1px 3px rgba(0,0,0,0.3)' } })
311
+ ),
312
+ props.label ? el('span', null, props.label) : null
313
+ );
314
+ }
315
+ if (type === 'toggle') {
316
+ return el('button', {
317
+ className: props.className,
318
+ onClick: click,
319
+ style: Object.assign({}, baseStyle, { gap: 0, background: checked ? 'var(--ican-accent)' : 'var(--ican-secondary-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-sm)', padding: '4px 12px', color: checked ? 'var(--ican-btn-primary-text, #fff)' : 'var(--ican-primary-text)', transition: 'background var(--ican-transition-fast)', fontSize: '13px' }, props.style || {})
320
+ }, props.label != null ? props.label : (checked ? 'On' : 'Off'));
321
+ }
322
+ if (type === 'indicator') {
323
+ return el('div', { className: props.className, style: Object.assign({}, baseStyle, props.style || {}), onClick: click },
324
+ el('div', { style: { width: '10px', height: '10px', borderRadius: '50%', background: checked ? 'var(--ican-success)' : 'var(--ican-border)', flexShrink: 0, transition: 'background var(--ican-transition-fast)' } }),
325
+ props.label ? el('span', null, props.label) : null
326
+ );
327
+ }
328
+ // default + bordered
329
+ var boxStyle = { width: '16px', height: '16px', borderRadius: 'var(--ican-radius-xs)', border: checked ? (type === 'bordered' ? '2px solid var(--ican-accent)' : '1px solid var(--ican-accent)') : '1px solid var(--ican-border)', background: checked ? 'var(--ican-accent)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'background var(--ican-transition-fast)', boxShadow: type === 'bordered' && checked ? '0 0 0 2px var(--ican-accent-dim)' : 'none' };
330
+ return el('div', { className: props.className, style: Object.assign({}, baseStyle, props.style || {}), onClick: click },
331
+ el('div', { style: boxStyle }, checked ? el('span', { style: { color: '#fff', fontSize: '11px', lineHeight: 1 } }, '✓') : null),
332
+ props.label ? el('span', null, props.label) : null
333
+ );
334
+ }
335
+
336
+ function MultiSelect(props) {
337
+ var _o = useState(false); var open = _o[0]; var setOpen = _o[1];
338
+ var _s = useState(''); var search = _s[0]; var setSearch = _s[1];
339
+ var ref = useRef(null);
340
+ var options = props.options || [];
341
+ var selected = props.selected || [];
342
+ var searchable = !!props.searchable;
343
+ var maxHeight = props.maxHeight || '200px';
344
+ var placeholder = props.placeholder || 'Select…';
345
+
346
+ useEffect(function () {
347
+ if (!open) return;
348
+ function handler(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
349
+ document.addEventListener('mousedown', handler);
350
+ return function () { document.removeEventListener('mousedown', handler); };
351
+ }, [open]);
352
+
353
+ var filtered = searchable && search ? options.filter(function (o) { return o.label.toLowerCase().includes(search.toLowerCase()); }) : options;
354
+ function toggle(value) { if (props.onChange) props.onChange(selected.includes(value) ? selected.filter(function (v) { return v !== value; }) : selected.concat([value])); }
355
+ function removeChip(value, e) { e.stopPropagation(); if (props.onChange) props.onChange(selected.filter(function (v) { return v !== value; })); }
356
+ var selectedOpts = selected.map(function (v) { return options.find(function (o) { return o.value === v; }); }).filter(Boolean);
357
+
358
+ return el('div', { className: props.className, ref: ref, style: Object.assign({ position: 'relative', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
359
+ props.label ? el('label', { style: { display: 'block', fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500, marginBottom: '4px' } }, props.label) : null,
360
+ el('div', {
361
+ onClick: function () { setOpen(!open); },
362
+ style: { width: '100%', background: 'var(--ican-input-bg, var(--ican-card-bg))', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: selected.length === 0 ? '6px 10px' : '4px 8px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', textAlign: 'left', minHeight: '32px', boxSizing: 'border-box' }
363
+ },
364
+ selected.length === 0
365
+ ? el('span', { style: { color: 'var(--ican-secondary-text)' } }, placeholder)
366
+ : el('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px', flex: 1 } },
367
+ selectedOpts.map(function (opt) {
368
+ return el('span', {
369
+ key: opt.value,
370
+ style: { display: 'inline-flex', alignItems: 'center', gap: '4px', background: 'var(--ican-accent-dim)', color: 'var(--ican-accent)', borderRadius: '999px', padding: '2px 8px', fontSize: '11px', fontWeight: 500 }
371
+ },
372
+ opt.label,
373
+ el('button', {
374
+ onClick: function (e) { removeChip(opt.value, e); },
375
+ style: { background: 'none', border: 'none', cursor: 'pointer', color: 'var(--ican-accent)', fontSize: '12px', padding: 0, lineHeight: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }
376
+ }, '×')
377
+ );
378
+ })
379
+ ),
380
+ el('span', { style: { fontSize: '10px', color: 'var(--ican-secondary-text)', marginLeft: '6px', flexShrink: 0 } }, open ? '▲' : '▼')
381
+ ),
382
+ open ? el('div', {
383
+ style: { position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', boxShadow: 'var(--ican-card-shadow)', zIndex: 100, overflow: 'hidden' }
384
+ },
385
+ searchable ? el('div', { style: { padding: '6px 8px', borderBottom: '1px solid var(--ican-border)' } },
386
+ el('input', {
387
+ value: search, placeholder: 'Search…',
388
+ onChange: function (e) { setSearch(e.target.value); },
389
+ style: { width: '100%', background: 'transparent', border: 'none', outline: 'none', fontSize: '12px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', boxSizing: 'border-box' }
390
+ })
391
+ ) : null,
392
+ el('div', { style: { maxHeight: maxHeight, overflowY: 'auto' } },
393
+ filtered.map(function (opt) {
394
+ return el('div', {
395
+ key: opt.value, onClick: function () { toggle(opt.value); },
396
+ style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '7px 10px', cursor: 'pointer', fontSize: '13px', color: 'var(--ican-primary-text)', background: selected.includes(opt.value) ? 'var(--ican-accent-dim)' : 'transparent' }
397
+ },
398
+ el('div', { style: { width: '14px', height: '14px', borderRadius: 'var(--ican-radius-xs)', border: selected.includes(opt.value) ? '1px solid var(--ican-accent)' : '1px solid var(--ican-border)', background: selected.includes(opt.value) ? 'var(--ican-accent)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 } },
399
+ selected.includes(opt.value) ? el('span', { style: { color: '#fff', fontSize: '10px' } }, '✓') : null
400
+ ),
401
+ opt.label
402
+ );
403
+ })
404
+ ),
405
+ selected.length > 0 ? el('div', { style: { borderTop: '1px solid var(--ican-border)', padding: '6px 10px' } },
406
+ el('button', { onClick: function () { if (props.onChange) props.onChange([]); }, style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '12px', color: 'var(--ican-secondary-text)', fontFamily: 'var(--ican-font-body)' } }, 'Clear all')
407
+ ) : null
408
+ ) : null
409
+ );
410
+ }
411
+
412
+ function ToggleFilter(props) {
413
+ var size = props.size || 'md';
414
+ return el('div', { className: props.className, style: Object.assign({ display: 'inline-flex', gap: '4px', flexWrap: 'wrap' }, props.style || {}) },
415
+ (props.options || []).map(function (opt) {
416
+ var active = opt.value === props.value;
417
+ return el('button', {
418
+ key: opt.value,
419
+ onClick: function () { if (props.onChange) props.onChange(opt.value); },
420
+ style: { padding: size === 'sm' ? '3px 10px' : '5px 14px', fontSize: size === 'sm' ? '11px' : '13px', borderRadius: '999px', border: '1px solid var(--ican-border)', background: active ? 'var(--ican-accent)' : 'var(--ican-secondary-bg)', color: active ? 'var(--ican-btn-primary-text, #fff)' : 'var(--ican-primary-text)', cursor: 'pointer', fontFamily: 'var(--ican-font-body)', transition: 'background var(--ican-transition-fast), color var(--ican-transition-fast)', fontWeight: active ? 600 : 400 }
421
+ }, opt.label);
422
+ })
423
+ );
424
+ }
425
+
426
+ function ColorPicker(props) {
427
+ var _o = useState(false); var open = _o[0]; var setOpen = _o[1];
428
+ var ref = useRef(null);
429
+ var presets = props.presets || ['var(--ican-accent)', 'var(--ican-success)', 'var(--ican-warning)', 'var(--ican-error)', 'var(--ican-info)', '#6366f1'];
430
+ useEffect(function () {
431
+ if (!open) return;
432
+ function handler(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
433
+ document.addEventListener('mousedown', handler);
434
+ return function () { document.removeEventListener('mousedown', handler); };
435
+ }, [open]);
436
+ return el('div', { className: props.className, ref: ref, style: Object.assign({ display: 'inline-flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
437
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
438
+ el('div', { style: { position: 'relative' } },
439
+ el('div', {
440
+ onClick: function () { setOpen(!open); },
441
+ style: { width: '32px', height: '32px', borderRadius: 'var(--ican-radius-sm)', background: props.color, border: '2px solid var(--ican-border)', cursor: 'pointer', boxSizing: 'border-box' }
442
+ }),
443
+ open ? el('div', { style: { position: 'absolute', top: '38px', left: 0, background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '12px', boxShadow: 'var(--ican-card-shadow)', zIndex: 200, display: 'flex', flexDirection: 'column', gap: '10px', minWidth: '180px' } },
444
+ el('div', { style: { display: 'flex', gap: '6px', flexWrap: 'wrap' } },
445
+ presets.map(function (p, i) {
446
+ return el('div', {
447
+ key: i,
448
+ onClick: function () { if (props.onChange) props.onChange(p); setOpen(false); },
449
+ style: { width: '22px', height: '22px', borderRadius: 'var(--ican-radius-xs)', background: p, border: p === props.color ? '2px solid var(--ican-border-focus)' : '1px solid var(--ican-border)', cursor: 'pointer', boxSizing: 'border-box' }
450
+ });
451
+ })
452
+ ),
453
+ el('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
454
+ el('input', { type: 'color', value: (props.color || '').startsWith('#') ? props.color : '#6366f1', onChange: function (e) { if (props.onChange) props.onChange(e.target.value); }, style: { width: '32px', height: '28px', border: 'none', padding: 0, cursor: 'pointer', background: 'transparent' } }),
455
+ el('input', { type: 'text', value: props.color, onChange: function (e) { if (props.onChange) props.onChange(e.target.value); }, style: { flex: 1, background: 'var(--ican-input-bg, var(--ican-card-bg))', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-sm)', padding: '4px 6px', fontSize: '12px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-mono)', outline: 'none' } })
456
+ )
457
+ ) : null
458
+ )
459
+ );
460
+ }
461
+
462
+ function SearchInput(props) {
463
+ return el('div', {
464
+ className: props.className,
465
+ style: Object.assign({ position: 'relative', display: 'inline-flex', alignItems: 'center', width: '100%', fontFamily: 'var(--ican-font-body)' }, props.style || {})
466
+ },
467
+ el('input', {
468
+ type: 'text', value: props.value, placeholder: props.placeholder || 'Search…',
469
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
470
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 32px 6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', width: '100%', boxSizing: 'border-box' }
471
+ }),
472
+ props.value ? el('button', {
473
+ onClick: props.onClear ? props.onClear : function () { if (props.onChange) props.onChange(''); },
474
+ style: { position: 'absolute', right: '8px', background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: 'var(--ican-secondary-text)', lineHeight: 1, padding: 0 }
475
+ }, '×') : null
476
+ );
477
+ }
478
+
479
+ /* ─────────────────────────────────────────────────────
480
+ * DATE / TIME PICKERS
481
+ * ───────────────────────────────────────────────────── */
482
+
483
+ function DatePicker(props) {
484
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
485
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
486
+ el('input', {
487
+ type: 'date',
488
+ value: props.value || '',
489
+ min: props.minDate, max: props.maxDate,
490
+ disabled: !!props.disabled, placeholder: props.placeholder,
491
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
492
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', opacity: props.disabled ? 0.5 : 1, width: '100%', boxSizing: 'border-box' }
493
+ })
494
+ );
495
+ }
496
+
497
+ function TimePicker(props) {
498
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
499
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
500
+ el('input', {
501
+ type: 'time',
502
+ value: props.value || '',
503
+ disabled: !!props.disabled, placeholder: props.placeholder,
504
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
505
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', opacity: props.disabled ? 0.5 : 1, width: '100%', boxSizing: 'border-box' }
506
+ })
507
+ );
508
+ }
509
+
510
+ function DateRangePicker(props) {
511
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
512
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
513
+ el('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
514
+ el('input', {
515
+ type: 'date', value: props.startDate || '', disabled: !!props.disabled,
516
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value, props.endDate || ''); },
517
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', flex: 1 }
518
+ }),
519
+ el('span', { style: { color: 'var(--ican-secondary-text)', fontSize: '12px' } }, '→'),
520
+ el('input', {
521
+ type: 'date', value: props.endDate || '', disabled: !!props.disabled,
522
+ onChange: function (e) { if (props.onChange) props.onChange(props.startDate || '', e.target.value); },
523
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', flex: 1 }
524
+ })
525
+ )
526
+ );
527
+ }
528
+
529
+ function DateTimePicker(props) {
530
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
531
+ props.label ? el('label', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500 } }, props.label) : null,
532
+ el('input', {
533
+ type: 'datetime-local',
534
+ value: props.value || '',
535
+ disabled: !!props.disabled,
536
+ onChange: function (e) { if (props.onChange) props.onChange(e.target.value); },
537
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '13px', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', opacity: props.disabled ? 0.5 : 1, width: '100%', boxSizing: 'border-box' }
538
+ })
539
+ );
540
+ }
541
+
542
+ function CalendarView(props) {
543
+ var today = new Date();
544
+ var _cur = useState(new Date(today.getFullYear(), today.getMonth(), 1));
545
+ var cur = _cur[0]; var setCur = _cur[1];
546
+ var _sel = useState(null);
547
+ var selected = _sel[0]; var setSelected = _sel[1];
548
+
549
+ var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
550
+ var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
551
+ var yr = cur.getFullYear(); var mo = cur.getMonth();
552
+ var firstDay = new Date(yr, mo, 1).getDay();
553
+ var daysInMonth = new Date(yr, mo + 1, 0).getDate();
554
+ var cells = [];
555
+ for (var i = 0; i < firstDay; i++) cells.push(null);
556
+ for (var d = 1; d <= daysInMonth; d++) cells.push(d);
557
+
558
+ var highlighted = (props.dates || []).reduce(function (acc, s) {
559
+ var dt = new Date(s);
560
+ if (!isNaN(dt)) acc[dt.getFullYear() + '-' + (dt.getMonth()+1) + '-' + dt.getDate()] = true;
561
+ return acc;
562
+ }, {});
563
+
564
+ function isoStr(day) { return yr + '-' + String(mo+1).padStart(2,'0') + '-' + String(day).padStart(2,'0'); }
565
+
566
+ return el('div', { className: props.className, style: Object.assign({ fontFamily: 'var(--ican-font-body)', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '16px', display: 'inline-block', minWidth: '240px' }, props.style || {}) },
567
+ el('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' } },
568
+ el('button', { onClick: function () { setCur(new Date(yr, mo - 1, 1)); }, style: { background: 'none', border: 'none', cursor: 'pointer', color: 'var(--ican-secondary-text)', fontSize: '14px' } }, '←'),
569
+ el('span', { style: { fontSize: '13px', fontWeight: 600, color: 'var(--ican-primary-text)' } }, months[mo] + ' ' + yr),
570
+ el('button', { onClick: function () { setCur(new Date(yr, mo + 1, 1)); }, style: { background: 'none', border: 'none', cursor: 'pointer', color: 'var(--ican-secondary-text)', fontSize: '14px' } }, '→')
571
+ ),
572
+ el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: '1px', marginBottom: '4px' } },
573
+ days.map(function (d) {
574
+ return el('div', { key: d, style: { textAlign: 'center', fontSize: '10px', color: 'var(--ican-secondary-text)', fontWeight: 600, padding: '2px' } }, d);
575
+ })
576
+ ),
577
+ el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: '2px' } },
578
+ cells.map(function (day, i) {
579
+ if (!day) return el('div', { key: 'e' + i });
580
+ var key = yr + '-' + (mo+1) + '-' + day;
581
+ var isHl = highlighted[key];
582
+ var isSel = selected && selected.getTime && selected.getFullYear() === yr && selected.getMonth() === mo && selected.getDate() === day;
583
+ var isToday = today.getFullYear() === yr && today.getMonth() === mo && today.getDate() === day;
584
+ var bg = 'transparent'; var color = 'var(--ican-primary-text)'; var fw = 400;
585
+ if (isSel) { bg = 'var(--ican-accent)'; color = 'var(--ican-btn-primary-text, #fff)'; fw = 700; }
586
+ else if (isHl) { bg = 'var(--ican-accent-dim)'; }
587
+ else if (isToday) { bg = 'rgba(99,102,241,0.15)'; fw = 600; }
588
+ return el('div', {
589
+ key: i,
590
+ onClick: function () { setSelected(new Date(yr, mo, day)); if (props.onSelectDate) props.onSelectDate(isoStr(day)); },
591
+ style: { textAlign: 'center', padding: '4px 2px', borderRadius: 'var(--ican-radius-sm)', background: bg, color: color, fontWeight: fw, cursor: 'pointer', fontSize: '12px', transition: 'background var(--ican-transition-fast)' }
592
+ }, day);
593
+ })
594
+ )
595
+ );
596
+ }
597
+
598
+ /* ─────────────────────────────────────────────────────
599
+ * BADGE / STAT CARD / EMPTY STATE
600
+ * ───────────────────────────────────────────────────── */
601
+
602
+ function Badge(props) {
603
+ var variant = props.variant || 'default';
604
+ var dot = !!props.dot;
605
+ var colorMap = { default: 'var(--ican-secondary-text)', success: 'var(--ican-success, #22c55e)', warning: 'var(--ican-warning, #f59e0b)', error: 'var(--ican-error)', info: 'var(--ican-accent)' };
606
+ var bgMap = { default: 'rgba(128,128,128,0.12)', success: 'rgba(34,197,94,0.12)', warning: 'rgba(245,158,11,0.12)', error: 'rgba(237,48,131,0.12)', info: 'rgba(var(--ican-accent-rgb,99,102,241),0.12)' };
607
+ return el('span', {
608
+ className: props.className,
609
+ style: Object.assign({ display: 'inline-flex', alignItems: 'center', gap: '4px', padding: '2px 8px', borderRadius: '999px', fontSize: '11px', fontWeight: 500, fontFamily: 'var(--ican-font-body)', color: colorMap[variant], background: bgMap[variant], whiteSpace: 'nowrap' }, props.style || {})
610
+ },
611
+ dot ? el('span', { style: { width: '6px', height: '6px', borderRadius: '50%', background: colorMap[variant], flexShrink: 0 } }) : null,
612
+ props.label
613
+ );
614
+ }
615
+
616
+ function StatCard(props) {
617
+ var trend = props.trend;
618
+ var trendColor = trend === undefined ? undefined : (trend >= 0 ? 'var(--ican-success, #22c55e)' : 'var(--ican-error)');
619
+ var trendPrefix = trend !== undefined && trend >= 0 ? '▲ ' : '▼ ';
620
+ return el('div', {
621
+ className: props.className,
622
+ style: Object.assign({ background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: '6px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
623
+ },
624
+ el('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
625
+ el('span', { style: { fontSize: '11px', color: 'var(--ican-secondary-text)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.05em' } }, props.label),
626
+ props.icon ? el('span', { style: { fontSize: '18px' } }, props.icon) : null
627
+ ),
628
+ el('div', { style: { display: 'flex', alignItems: 'baseline', gap: '4px' } },
629
+ el('span', { style: { fontSize: '28px', fontWeight: 700, color: 'var(--ican-primary-text)', lineHeight: 1 } }, String(props.value)),
630
+ props.unit ? el('span', { style: { fontSize: '13px', color: 'var(--ican-secondary-text)' } }, props.unit) : null
631
+ ),
632
+ el('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
633
+ trend !== undefined ? el('span', { style: { fontSize: '12px', color: trendColor, fontWeight: 500 } }, trendPrefix + Math.abs(trend) + '%') : null,
634
+ props.description ? el('span', { style: { fontSize: '11px', color: 'var(--ican-tertiary-text, var(--ican-secondary-text))' } }, props.description) : null
635
+ )
636
+ );
637
+ }
638
+
639
+ function EmptyState(props) {
640
+ return el('div', {
641
+ className: props.className,
642
+ style: Object.assign({ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 24px', gap: '12px', textAlign: 'center', fontFamily: 'var(--ican-font-body)' }, props.style || {})
643
+ },
644
+ props.icon ? el('span', { style: { fontSize: '40px', lineHeight: 1 } }, props.icon) : null,
645
+ el('div', { style: { fontWeight: 600, fontSize: '14px', color: 'var(--ican-primary-text)' } }, props.title),
646
+ props.description ? el('div', { style: { fontSize: '13px', color: 'var(--ican-secondary-text)', maxWidth: '320px' } }, props.description) : null,
647
+ props.action ? el('div', { style: { marginTop: '4px' } }, props.action) : null
648
+ );
649
+ }
650
+
651
+ /* ─────────────────────────────────────────────────────
652
+ * DATA TABLE (rows / columns[].key + label + render)
653
+ * ───────────────────────────────────────────────────── */
654
+
655
+ function DataTable(props) {
656
+ var _sort = useState(null);
657
+ var sort = _sort[0]; var setSort = _sort[1];
658
+
659
+ var columns = props.columns || [];
660
+ var rawRows = props.rows || [];
661
+ var loading = !!props.loading;
662
+ var emptyMessage = props.emptyMessage || 'No data';
663
+ var stickyHeader = !!props.stickyHeader;
664
+
665
+ var rows = rawRows;
666
+ if (sort && sort.key) {
667
+ var col = columns.find(function (c) { return c.key === sort.key; });
668
+ if (col) {
669
+ rows = rawRows.slice().sort(function (a, b) {
670
+ var av = a[sort.key]; var bv = b[sort.key];
671
+ if (av == null) return 1; if (bv == null) return -1;
672
+ if (typeof av === 'number' && typeof bv === 'number') return sort.dir === 'asc' ? av - bv : bv - av;
673
+ return sort.dir === 'asc' ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
674
+ });
675
+ }
676
+ }
677
+
678
+ if (loading) return el(Loading, { label: 'Loading table...' });
679
+
680
+ function toggleSort(key) {
681
+ if (!sort || sort.key !== key) setSort({ key: key, dir: 'asc' });
682
+ else if (sort.dir === 'asc') setSort({ key: key, dir: 'desc' });
683
+ else setSort(null);
684
+ }
685
+
686
+ var headerCells = columns.map(function (col) {
687
+ var isSorted = sort && sort.key === col.key;
688
+ var arrow = isSorted ? (sort.dir === 'asc' ? ' ▲' : ' ▼') : '';
689
+ return el('th', {
690
+ key: col.key,
691
+ onClick: col.sortable ? function () { toggleSort(col.key); } : undefined,
692
+ style: Object.assign({
693
+ padding: '8px 12px', textAlign: 'left', fontSize: '11px', fontWeight: 600,
694
+ color: isSorted ? 'var(--ican-accent)' : 'var(--ican-secondary-text)', textTransform: 'uppercase', letterSpacing: '0.05em',
695
+ borderBottom: '1px solid var(--ican-border)', background: 'var(--ican-card-bg)',
696
+ whiteSpace: 'nowrap', width: col.width,
697
+ cursor: col.sortable ? 'pointer' : 'default',
698
+ userSelect: 'none'
699
+ }, stickyHeader ? { position: 'sticky', top: 0, zIndex: 1 } : {})
700
+ }, col.label + arrow);
701
+ });
702
+
703
+ var bodyContent = rows.length === 0
704
+ ? el('tr', null,
705
+ el('td', {
706
+ colSpan: columns.length,
707
+ style: { padding: '32px', textAlign: 'center', color: 'var(--ican-secondary-text)', fontSize: '13px', fontFamily: 'var(--ican-font-body)' }
708
+ }, emptyMessage)
709
+ )
710
+ : rows.map(function (row, rowIdx) {
711
+ return el('tr', {
712
+ key: rowIdx,
713
+ onClick: props.onRowClick ? function () { props.onRowClick(row); } : undefined,
714
+ onMouseEnter: function (e) { e.currentTarget.style.background = 'var(--ican-hover, var(--ican-secondary-bg))'; },
715
+ onMouseLeave: function (e) { e.currentTarget.style.background = 'transparent'; },
716
+ style: { cursor: props.onRowClick ? 'pointer' : 'default', transition: 'background 0.1s' }
717
+ },
718
+ columns.map(function (col) {
719
+ return el('td', {
720
+ key: col.key,
721
+ style: { padding: '8px 12px', fontSize: '13px', color: 'var(--ican-primary-text)', borderBottom: '1px solid var(--ican-border)', fontFamily: 'var(--ican-font-body)' }
722
+ }, col.render ? col.render(row[col.key], row) : String(row[col.key] != null ? row[col.key] : ''));
723
+ })
724
+ );
725
+ });
726
+
727
+ return el('div', { className: props.className, style: Object.assign({ overflowX: 'auto', width: '100%' }, props.style || {}) },
728
+ el('table', { style: { width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--ican-font-body)' } },
729
+ el('thead', null, el('tr', null, headerCells)),
730
+ el('tbody', null, bodyContent)
731
+ )
732
+ );
733
+ }
734
+
735
+ /* ─────────────────────────────────────────────────────
736
+ * DATA GRID
737
+ * ───────────────────────────────────────────────────── */
738
+
739
+ function DataGrid(props) {
740
+ var items = props.items || [];
741
+ var columns = props.columns || 3;
742
+ var gap = props.gap || '12px';
743
+ var loading = !!props.loading;
744
+ var emptyMessage = props.emptyMessage || 'No items';
745
+ if (loading) return el(Loading, { label: 'Loading…' });
746
+ if (items.length === 0) return el('div', { className: props.className, style: Object.assign({ padding: '32px', textAlign: 'center', color: 'var(--ican-secondary-text)', fontSize: '13px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) }, emptyMessage);
747
+ return el('div', {
748
+ className: props.className,
749
+ style: Object.assign({ display: 'grid', gridTemplateColumns: 'repeat(' + columns + ', 1fr)', gap: gap }, props.style || {})
750
+ },
751
+ items.map(function (item, i) { return props.renderItem(item, i); })
752
+ );
753
+ }
754
+
755
+ /* ─────────────────────────────────────────────────────
756
+ * ITEM CARD / ITEM LIST CARD / PROFILE IMAGE
757
+ * ───────────────────────────────────────────────────── */
758
+
759
+ function ProfileImage(props) {
760
+ var size = props.size || 'md';
761
+ var sizeMap = { xs: '20px', sm: '28px', md: '36px', lg: '48px' };
762
+ var fontMap = { xs: '9px', sm: '11px', md: '13px', lg: '17px' };
763
+ var initials = (props.name || '?').split(' ').map(function (w) { return w[0] || ''; }).join('').slice(0, 2).toUpperCase();
764
+ var hash = Array.from(props.name || '').reduce(function (a, c) { return a + c.charCodeAt(0); }, 0);
765
+ var colors = ['var(--ican-accent)', 'var(--ican-success)', 'var(--ican-info)', 'var(--ican-warning)'];
766
+ var bg = props.bgColor || colors[hash % colors.length];
767
+ var dim = sizeMap[size];
768
+ if (props.image) {
769
+ return el('img', { className: props.className, src: props.image, alt: props.name, style: Object.assign({ width: dim, height: dim, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }, props.style || {}) });
770
+ }
771
+ return el('div', {
772
+ className: props.className,
773
+ style: Object.assign({ width: dim, height: dim, borderRadius: '50%', background: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: fontMap[size], fontWeight: 700, flexShrink: 0, fontFamily: 'var(--ican-font-body)' }, props.style || {})
774
+ }, initials);
775
+ }
776
+
777
+ function ItemCard(props) {
778
+ var name = props.name || props.title;
779
+ var initials = (name || '').split(' ').map(function (w) { return w[0] || ''; }).join('').slice(0, 2).toUpperCase();
780
+ var hash = Array.from(name || '').reduce(function (a, c) { return a + c.charCodeAt(0); }, 0);
781
+ var colors = ['var(--ican-accent)', 'var(--ican-success)', 'var(--ican-info)', 'var(--ican-warning)'];
782
+ var bg = colors[hash % colors.length];
783
+ return el('div', {
784
+ className: props.className,
785
+ onClick: props.onClick,
786
+ onMouseEnter: props.onClick ? function (e) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = 'var(--ican-card-hover-shadow, 0 6px 18px rgba(0,0,0,0.18))'; } : undefined,
787
+ onMouseLeave: props.onClick ? function (e) { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; } : undefined,
788
+ style: Object.assign({ background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '14px', display: 'flex', flexDirection: 'column', gap: '10px', cursor: props.onClick ? 'pointer' : 'default', transition: 'transform var(--ican-transition-fast), box-shadow var(--ican-transition-fast)', fontFamily: 'var(--ican-font-body)' }, props.style || {})
789
+ },
790
+ el('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
791
+ props.image
792
+ ? el('img', { src: props.image, alt: name, style: { width: '36px', height: '36px', borderRadius: '50%', objectFit: 'cover', flexShrink: 0 } })
793
+ : el('div', { style: { width: '36px', height: '36px', borderRadius: '50%', background: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: '13px', fontWeight: 700, flexShrink: 0 } }, initials),
794
+ el('div', { style: { flex: 1, minWidth: 0 } },
795
+ el('div', { style: { fontWeight: 600, fontSize: '13px', color: 'var(--ican-primary-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, props.title),
796
+ props.subtitle ? el('div', { style: { fontSize: '12px', color: 'var(--ican-secondary-text)', marginTop: '2px' } }, props.subtitle) : null
797
+ )
798
+ ),
799
+ props.meta ? el('div', { style: { fontSize: '12px', color: 'var(--ican-tertiary-text, var(--ican-secondary-text))' } }, props.meta) : null,
800
+ props.actions ? el('div', { style: { display: 'flex', gap: '6px', marginTop: '2px' } }, props.actions) : null
801
+ );
802
+ }
803
+
804
+ function ItemListCard(props) {
805
+ return el('div', {
806
+ className: props.className,
807
+ onClick: props.onClick,
808
+ style: Object.assign({ background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '14px', display: 'flex', flexDirection: 'column', gap: '10px', cursor: props.onClick ? 'pointer' : 'default', fontFamily: 'var(--ican-font-body)' }, props.style || {})
809
+ },
810
+ el('div', null,
811
+ el('div', { style: { fontWeight: 600, fontSize: '13px', color: 'var(--ican-primary-text)' } }, props.title),
812
+ props.subtitle ? el('div', { style: { fontSize: '12px', color: 'var(--ican-secondary-text)', marginTop: '2px' } }, props.subtitle) : null
813
+ ),
814
+ el('dl', { style: { margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '4px' } },
815
+ (props.fields || []).map(function (f, i) {
816
+ return el('div', { key: i, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', fontSize: '13px', gap: '8px' } },
817
+ el('dt', { style: { color: 'var(--ican-secondary-text)', flexShrink: 0 } }, f.label),
818
+ el('dd', { style: { color: 'var(--ican-primary-text)', fontWeight: 500, textAlign: 'right', margin: 0 } }, f.value)
819
+ );
820
+ })
821
+ ),
822
+ props.actions ? el('div', { style: { display: 'flex', gap: '6px', paddingTop: '6px', borderTop: '1px solid var(--ican-border)' } }, props.actions) : null
823
+ );
824
+ }
825
+
826
+ function HorizontalScrollList(props) {
827
+ var items = props.items || [];
828
+ var itemWidth = props.itemWidth || '200px';
829
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', gap: '12px', overflowX: 'auto', paddingBottom: '4px', scrollbarWidth: 'thin' }, props.style || {}) },
830
+ items.map(function (item, i) {
831
+ return el('div', { key: i, style: { flexShrink: 0, width: itemWidth } }, props.renderItem(item, i));
832
+ })
833
+ );
834
+ }
835
+
836
+ /* ─────────────────────────────────────────────────────
837
+ * NOTIFICATION BLOCK
838
+ * ───────────────────────────────────────────────────── */
839
+
840
+ function NotificationBlock(props) {
841
+ var type = props.type || 'info';
842
+ var colorMap = { info: 'var(--ican-info)', success: 'var(--ican-success)', warning: 'var(--ican-warning)', error: 'var(--ican-error)' };
843
+ var bgMap = { info: 'rgba(99,102,241,0.08)', success: 'var(--ican-success-dim)', warning: 'var(--ican-warning-dim)', error: 'var(--ican-error-dim)' };
844
+ var defaultIcons = { info: 'ℹ', success: '✓', warning: '⚠', error: '✕' };
845
+ return el('div', {
846
+ className: props.className,
847
+ style: Object.assign({ display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '12px 14px', borderRadius: 'var(--ican-radius-md)', background: bgMap[type], border: '1px solid ' + colorMap[type], borderLeft: '4px solid ' + colorMap[type], fontFamily: 'var(--ican-font-body)', fontSize: '13px' }, props.style || {})
848
+ },
849
+ el('span', { style: { color: colorMap[type], fontWeight: 700, flexShrink: 0, marginTop: '1px' } }, props.icon || defaultIcons[type]),
850
+ el('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: '2px', lineHeight: 1.5 } },
851
+ props.title ? el('div', { style: { color: 'var(--ican-primary-text)', fontWeight: 600, fontSize: '13px' } }, props.title) : null,
852
+ el('div', { style: { color: props.title ? 'var(--ican-secondary-text)' : 'var(--ican-primary-text)', fontSize: '13px' } }, props.message)
853
+ ),
854
+ props.onClose ? el('button', { onClick: props.onClose, style: { background: 'none', border: 'none', cursor: 'pointer', color: 'var(--ican-secondary-text)', fontSize: '14px', padding: 0, flexShrink: 0 } }, '✕') : null
855
+ );
856
+ }
857
+
858
+ function SampleDataLabel(props) {
859
+ if (props.show === false) return null;
860
+ return el('div', {
861
+ className: props.className,
862
+ style: Object.assign({ position: 'absolute', top: '8px', right: '8px', background: 'var(--ican-warning-dim)', color: 'var(--ican-warning)', border: '1px solid var(--ican-warning)', borderRadius: 'var(--ican-radius-xs)', padding: '2px 6px', fontSize: '10px', fontWeight: 600, fontFamily: 'var(--ican-font-body)', pointerEvents: 'none', zIndex: 10 }, props.style || {})
863
+ }, 'Sample Data');
864
+ }
865
+
866
+ /* ─────────────────────────────────────────────────────
867
+ * TOOLTIP / POPOVER / DROPDOWN BUTTON
868
+ * ───────────────────────────────────────────────────── */
869
+
870
+ function Tooltip(props) {
871
+ var _v = useState(false); var visible = _v[0]; var setVisible = _v[1];
872
+ var timer = useRef(null);
873
+ var position = props.position || 'top';
874
+ var delay = props.delay != null ? props.delay : 200;
875
+ var posStyles = {
876
+ top: { bottom: 'calc(100% + 6px)', left: '50%', transform: 'translateX(-50%)' },
877
+ bottom: { top: 'calc(100% + 6px)', left: '50%', transform: 'translateX(-50%)' },
878
+ left: { right: 'calc(100% + 6px)', top: '50%', transform: 'translateY(-50%)' },
879
+ right: { left: 'calc(100% + 6px)', top: '50%', transform: 'translateY(-50%)' }
880
+ };
881
+ function show() {
882
+ if (timer.current) clearTimeout(timer.current);
883
+ timer.current = setTimeout(function () { setVisible(true); }, delay);
884
+ }
885
+ function hide() {
886
+ if (timer.current) clearTimeout(timer.current);
887
+ setVisible(false);
888
+ }
889
+ useEffect(function () { return function () { if (timer.current) clearTimeout(timer.current); }; }, []);
890
+ return el('div', {
891
+ className: props.className,
892
+ style: Object.assign({ position: 'relative', display: 'inline-flex' }, props.style || {}),
893
+ onMouseEnter: show,
894
+ onMouseLeave: hide
895
+ },
896
+ props.children,
897
+ visible ? el('div', {
898
+ style: Object.assign({ position: 'absolute', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-sm)', padding: '5px 9px', fontSize: '12px', color: 'var(--ican-primary-text)', whiteSpace: 'nowrap', zIndex: 9000, boxShadow: 'var(--ican-card-shadow)', pointerEvents: 'none', fontFamily: 'var(--ican-font-body)' }, posStyles[position])
899
+ }, props.content) : null
900
+ );
901
+ }
902
+
903
+ function Popover(props) {
904
+ var _o = useState(false); var open = _o[0]; var setOpen = _o[1];
905
+ var ref = useRef(null);
906
+ var position = props.position || 'bottom';
907
+ function toggle() {
908
+ var next = !open;
909
+ setOpen(next);
910
+ if (next) { if (props.onOpen) props.onOpen(); } else { if (props.onClose) props.onClose(); }
911
+ }
912
+ useEffect(function () {
913
+ if (!open) return;
914
+ function handler(e) { if (ref.current && !ref.current.contains(e.target)) { setOpen(false); if (props.onClose) props.onClose(); } }
915
+ document.addEventListener('mousedown', handler);
916
+ return function () { document.removeEventListener('mousedown', handler); };
917
+ }, [open]);
918
+ var posStyles = {
919
+ bottom: { top: 'calc(100% + 6px)', left: 0 },
920
+ top: { bottom: 'calc(100% + 6px)', left: 0 },
921
+ right: { left: 'calc(100% + 6px)', top: 0 },
922
+ left: { right: 'calc(100% + 6px)', top: 0 }
923
+ };
924
+ return el('div', { className: props.className, ref: ref, style: Object.assign({ position: 'relative', display: 'inline-flex' }, props.style || {}) },
925
+ el('div', { onClick: toggle }, props.trigger),
926
+ open ? el('div', {
927
+ style: Object.assign({ position: 'absolute', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', boxShadow: 'var(--ican-card-shadow)', zIndex: 500, minWidth: '180px' }, posStyles[position])
928
+ }, props.content) : null
929
+ );
930
+ }
931
+
932
+ function DropDownButton(props) {
933
+ var _o = useState(false); var open = _o[0]; var setOpen = _o[1];
934
+ var ref = useRef(null);
935
+ var position = props.position || 'bottom-left';
936
+ useEffect(function () {
937
+ if (!open) return;
938
+ function handler(e) { if (ref.current && !ref.current.contains(e.target)) { setOpen(false); if (props.onClose) props.onClose(); } }
939
+ document.addEventListener('mousedown', handler);
940
+ return function () { document.removeEventListener('mousedown', handler); };
941
+ }, [open]);
942
+ var posMap = {
943
+ 'bottom-left': { top: 'calc(100% + 4px)', left: 0 },
944
+ 'bottom-right': { top: 'calc(100% + 4px)', right: 0 },
945
+ 'top-left': { bottom: 'calc(100% + 4px)', left: 0 },
946
+ 'top-right': { bottom: 'calc(100% + 4px)', right: 0 }
947
+ };
948
+ return el('div', { className: props.className, ref: ref, style: Object.assign({ position: 'relative', display: 'inline-flex' }, props.style || {}) },
949
+ el('button', {
950
+ onClick: function () { var n = !open; setOpen(n); if (n) { if (props.onOpen) props.onOpen(); } else { if (props.onClose) props.onClose(); } },
951
+ style: { display: 'inline-flex', alignItems: 'center', gap: '6px', padding: '7px 12px', fontSize: '13px', background: 'var(--ican-btn-secondary-bg, transparent)', color: 'var(--ican-primary-text)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', cursor: 'pointer', fontFamily: 'var(--ican-font-body)' }
952
+ },
953
+ props.icon ? el('span', null, props.icon) : null,
954
+ props.label,
955
+ el('span', { style: { fontSize: '9px', color: 'var(--ican-secondary-text)', marginLeft: '2px' } }, open ? '▲' : '▼')
956
+ ),
957
+ open ? el('div', {
958
+ style: Object.assign({ position: 'absolute', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', boxShadow: 'var(--ican-card-shadow)', zIndex: 500, minWidth: '160px', overflow: 'hidden' }, posMap[position])
959
+ }, props.children) : null
960
+ );
961
+ }
962
+
963
+ /* ─────────────────────────────────────────────────────
964
+ * TABS / MODAL / PAGINATION (controlled — activeKey + onChange)
965
+ * ───────────────────────────────────────────────────── */
966
+
967
+ function Tabs(props) {
968
+ var tabs = props.tabs || [];
969
+ function handleKey(e) {
970
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
971
+ var idx = tabs.findIndex(function (t) { return t.key === props.activeKey; });
972
+ if (idx < 0) return;
973
+ var next = e.key === 'ArrowRight' ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length;
974
+ if (props.onChange) props.onChange(tabs[next].key);
975
+ e.preventDefault();
976
+ }
977
+ return el('div', {
978
+ className: props.className,
979
+ role: 'tablist',
980
+ onKeyDown: handleKey,
981
+ style: Object.assign({ display: 'flex', borderBottom: '1px solid var(--ican-border)', gap: 0, fontFamily: 'var(--ican-font-body)' }, props.style || {})
982
+ },
983
+ tabs.map(function (tab) {
984
+ var isActive = tab.key === props.activeKey;
985
+ return el('button', {
986
+ key: tab.key,
987
+ role: 'tab',
988
+ 'aria-selected': isActive,
989
+ tabIndex: isActive ? 0 : -1,
990
+ onClick: function () { if (props.onChange) props.onChange(tab.key); },
991
+ style: { padding: '8px 16px', fontSize: '13px', fontWeight: isActive ? 600 : 400, color: isActive ? 'var(--ican-accent)' : 'var(--ican-secondary-text)', background: 'transparent', border: 'none', borderBottom: isActive ? '2px solid var(--ican-accent)' : '2px solid transparent', cursor: 'pointer', fontFamily: 'var(--ican-font-body)', transition: 'color 0.15s', marginBottom: '-1px', outline: 'none' }
992
+ }, tab.label);
993
+ })
994
+ );
995
+ }
996
+
997
+ function Modal(props) {
998
+ var closable = props.closable !== false;
999
+ useEffect(function () {
1000
+ if (!props.open || !closable) return;
1001
+ function onKey(e) { if (e.key === 'Escape' && props.onClose) props.onClose(); }
1002
+ document.addEventListener('keydown', onKey);
1003
+ return function () { document.removeEventListener('keydown', onKey); };
1004
+ }, [props.open, props.onClose, closable]);
1005
+ if (!props.open) return null;
1006
+ var width = props.width || '480px';
1007
+ var ReactDOM = window.ReactDOM;
1008
+ var inner = el('div', {
1009
+ className: props.className,
1010
+ onClick: function (e) { if (closable && e.target === e.currentTarget && props.onClose) props.onClose(); },
1011
+ style: Object.assign({ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, fontFamily: 'var(--ican-font-body)' }, props.style || {})
1012
+ },
1013
+ el('div', {
1014
+ style: { background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', width: width, maxWidth: '96vw', maxHeight: '90vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }
1015
+ },
1016
+ props.title ? el('div', {
1017
+ style: { padding: '14px 16px', borderBottom: '1px solid var(--ican-border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }
1018
+ },
1019
+ el('span', { style: { fontWeight: 600, fontSize: '14px', color: 'var(--ican-primary-text)' } }, props.title),
1020
+ closable ? el('button', { onClick: props.onClose, style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '18px', color: 'var(--ican-secondary-text)', lineHeight: 1, padding: '0 2px' } }, '×') : null
1021
+ ) : null,
1022
+ el('div', { style: { padding: '16px', overflowY: 'auto', flex: 1 } }, props.children),
1023
+ props.footer ? el('div', {
1024
+ style: { padding: '12px 16px', borderTop: '1px solid var(--ican-border)', display: 'flex', justifyContent: 'flex-end', gap: '8px', flexShrink: 0 }
1025
+ }, props.footer) : null
1026
+ )
1027
+ );
1028
+ if (ReactDOM && ReactDOM.createPortal) return ReactDOM.createPortal(inner, document.body);
1029
+ return inner;
1030
+ }
1031
+
1032
+ function Pagination(props) {
1033
+ var _j = useState('');
1034
+ var jump = _j[0]; var setJump = _j[1];
1035
+ var page = props.page;
1036
+ var pageSize = props.pageSize;
1037
+ var total = props.total;
1038
+ var totalPages = Math.max(1, Math.ceil(total / pageSize));
1039
+ var start = Math.min((page - 1) * pageSize + 1, total);
1040
+ var end = Math.min(page * pageSize, total);
1041
+ function applyJump() {
1042
+ var n = parseInt(jump, 10);
1043
+ if (!isNaN(n) && n >= 1 && n <= totalPages) props.onChange(n);
1044
+ setJump('');
1045
+ }
1046
+ return el('div', {
1047
+ className: props.className,
1048
+ style: Object.assign({ display: 'flex', alignItems: 'center', gap: '12px', fontFamily: 'var(--ican-font-body)', fontSize: '13px', color: 'var(--ican-secondary-text)', flexWrap: 'wrap' }, props.style || {})
1049
+ },
1050
+ el('span', null, 'Showing ' + start + '–' + end + ' of ' + total),
1051
+ el('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } },
1052
+ el('button', {
1053
+ onClick: function () { props.onChange(page - 1); },
1054
+ disabled: page <= 1,
1055
+ style: { padding: '4px 10px', fontSize: '12px', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', color: 'var(--ican-primary-text)', cursor: page <= 1 ? 'not-allowed' : 'pointer', opacity: page <= 1 ? 0.4 : 1, fontFamily: 'var(--ican-font-body)' }
1056
+ }, 'Prev'),
1057
+ el('span', { style: { fontSize: '12px', color: 'var(--ican-secondary-text)' } }, 'Page ' + page + ' / ' + totalPages),
1058
+ el('button', {
1059
+ onClick: function () { props.onChange(page + 1); },
1060
+ disabled: page >= totalPages,
1061
+ style: { padding: '4px 10px', fontSize: '12px', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', color: 'var(--ican-primary-text)', cursor: page >= totalPages ? 'not-allowed' : 'pointer', opacity: page >= totalPages ? 0.4 : 1, fontFamily: 'var(--ican-font-body)' }
1062
+ }, 'Next')
1063
+ ),
1064
+ totalPages > 5 ? el('div', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } },
1065
+ el('span', null, 'Go to'),
1066
+ el('input', {
1067
+ type: 'number', min: 1, max: totalPages, value: jump,
1068
+ onChange: function (e) { setJump(e.target.value); },
1069
+ onKeyDown: function (e) { if (e.key === 'Enter') applyJump(); },
1070
+ onBlur: applyJump,
1071
+ style: { width: '52px', padding: '3px 6px', fontSize: '12px', background: 'var(--ican-card-bg)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-sm)', color: 'var(--ican-primary-text)', fontFamily: 'var(--ican-font-body)', outline: 'none', textAlign: 'center' }
1072
+ })
1073
+ ) : null
1074
+ );
1075
+ }
1076
+
1077
+ /* ─────────────────────────────────────────────────────
1078
+ * WIZARD / MODAL WIZARD
1079
+ * ───────────────────────────────────────────────────── */
1080
+
1081
+ function Wizard(props) {
1082
+ var steps = props.steps || [];
1083
+ var _i = useState(0); var activeIdx = _i[0]; var setActiveIdx = _i[1];
1084
+ var _l = useState(false); var loading = _l[0]; var setLoading = _l[1];
1085
+ var _d = useState(false); var done = _d[0]; var setDone = _d[1];
1086
+ var isFirst = activeIdx === 0;
1087
+ var isLast = activeIdx === steps.length - 1;
1088
+
1089
+ function goNext() {
1090
+ var step = steps[activeIdx];
1091
+ function proceed() {
1092
+ if (isLast) { setDone(true); if (props.onComplete) props.onComplete(); }
1093
+ else setActiveIdx(activeIdx + 1);
1094
+ }
1095
+ if (step && step.onNext) {
1096
+ setLoading(true);
1097
+ Promise.resolve(step.onNext()).then(function (ok) {
1098
+ setLoading(false);
1099
+ if (ok !== false) proceed();
1100
+ }).catch(function () { setLoading(false); });
1101
+ } else { proceed(); }
1102
+ }
1103
+
1104
+ if (done) return el('div', { className: props.className, style: Object.assign({ textAlign: 'center', padding: '32px', fontFamily: 'var(--ican-font-body)', color: 'var(--ican-primary-text)', fontWeight: 600, fontSize: '15px' }, props.style || {}) }, props.completionTitle || 'Done!');
1105
+
1106
+ var step = steps[activeIdx] || {};
1107
+ return el('div', { className: props.className, style: Object.assign({ display: 'flex', flexDirection: 'column', gap: '16px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) },
1108
+ el('div', { style: { display: 'flex', alignItems: 'center', gap: 0 } },
1109
+ steps.reduce(function (out, s, i) {
1110
+ var completed = i < activeIdx; var active = i === activeIdx;
1111
+ out.push(el('div', { key: s.id, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' } },
1112
+ el('div', { style: { width: '28px', height: '28px', borderRadius: '50%', background: completed ? 'var(--ican-success)' : active ? 'var(--ican-accent)' : 'var(--ican-secondary-bg)', border: '2px solid ' + (completed ? 'var(--ican-success)' : active ? 'var(--ican-accent)' : 'var(--ican-border)'), display: 'flex', alignItems: 'center', justifyContent: 'center', color: completed || active ? '#fff' : 'var(--ican-secondary-text)', fontSize: '12px', fontWeight: 700 } }, completed ? '✓' : String(i + 1)),
1113
+ el('span', { style: { fontSize: '10px', color: active ? 'var(--ican-accent)' : 'var(--ican-secondary-text)', fontWeight: active ? 600 : 400, whiteSpace: 'nowrap' } }, s.title)
1114
+ ));
1115
+ if (i < steps.length - 1) {
1116
+ out.push(el('div', { key: 'l' + i, style: { flex: 1, height: '2px', background: i < activeIdx ? 'var(--ican-success)' : 'var(--ican-border)', margin: '0 4px', marginBottom: '18px' } }));
1117
+ }
1118
+ return out;
1119
+ }, [])
1120
+ ),
1121
+ el('div', { style: { flex: 1 } }, step.render ? step.render({ next: goNext, prev: function () { setActiveIdx(Math.max(0, activeIdx - 1)); }, isFirst: isFirst, isLast: isLast }) : null),
1122
+ el('div', { style: { display: 'flex', justifyContent: 'space-between', paddingTop: '12px', borderTop: '1px solid var(--ican-border)' } },
1123
+ el('button', { onClick: function () { setActiveIdx(Math.max(0, activeIdx - 1)); }, disabled: isFirst || loading, style: { padding: '7px 16px', fontSize: '13px', background: 'transparent', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', color: 'var(--ican-primary-text)', cursor: isFirst ? 'not-allowed' : 'pointer', opacity: isFirst ? 0.4 : 1, fontFamily: 'var(--ican-font-body)' } }, '← Prev'),
1124
+ el('button', { onClick: goNext, disabled: loading, style: { padding: '7px 16px', fontSize: '13px', background: 'var(--ican-accent)', border: 'none', borderRadius: 'var(--ican-radius-md)', color: 'var(--ican-btn-primary-text, #fff)', cursor: loading ? 'wait' : 'pointer', fontFamily: 'var(--ican-font-body)', display: 'inline-flex', alignItems: 'center', gap: '6px' } },
1125
+ loading ? el('span', null, '↻') : null,
1126
+ isLast ? 'Finish' : 'Next →'
1127
+ )
1128
+ )
1129
+ );
1130
+ }
1131
+
1132
+ function ModalWizard(props) {
1133
+ return el(Modal, { open: props.open, onClose: props.onClose, title: props.title || 'Wizard', width: props.width || '640px' },
1134
+ el('div', { className: props.className, style: Object.assign({ display: 'flex', gap: 0, minHeight: '300px' }, props.style || {}) },
1135
+ el('div', { style: { width: '160px', flexShrink: 0, borderRight: '1px solid var(--ican-border)', paddingRight: '12px', display: 'flex', flexDirection: 'column', gap: '4px' } },
1136
+ (props.steps || []).map(function (s, i) {
1137
+ return el('div', { key: s.id, style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: 'var(--ican-radius-sm)', fontSize: '12px', color: 'var(--ican-secondary-text)' } },
1138
+ el('div', { style: { width: '18px', height: '18px', borderRadius: '50%', background: 'var(--ican-secondary-bg)', border: '1px solid var(--ican-border)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px', flexShrink: 0 } }, String(i + 1)),
1139
+ s.title
1140
+ );
1141
+ })
1142
+ ),
1143
+ el('div', { style: { flex: 1, paddingLeft: '16px' } },
1144
+ el(Wizard, {
1145
+ steps: props.steps,
1146
+ onComplete: function () { if (props.onComplete) props.onComplete(); if (props.onClose) props.onClose(); },
1147
+ completionTitle: props.completionTitle
1148
+ })
1149
+ )
1150
+ )
1151
+ );
1152
+ }
1153
+
1154
+ /* ─────────────────────────────────────────────────────
1155
+ * FORM FIELD / LABEL / FORM FEEDBACK
1156
+ * ───────────────────────────────────────────────────── */
1157
+
1158
+ function Label(props) {
1159
+ return el('label', {
1160
+ className: props.className,
1161
+ style: Object.assign({ display: 'block', fontSize: '12px', fontWeight: 500, color: 'var(--ican-secondary-text)', marginBottom: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
1162
+ },
1163
+ props.children,
1164
+ props.required ? el('span', { style: { color: 'var(--ican-error)', marginLeft: '2px' } }, '*') : null
1165
+ );
1166
+ }
1167
+
1168
+ function FormField(props) {
1169
+ return el('div', {
1170
+ className: props.className,
1171
+ style: Object.assign({ display: props.inline ? 'flex' : 'block', alignItems: props.inline ? 'center' : undefined, gap: props.inline ? '12px' : undefined, marginBottom: '12px' }, props.style || {})
1172
+ },
1173
+ props.label ? el('label', {
1174
+ style: { display: 'block', fontSize: '12px', fontWeight: 500, color: 'var(--ican-secondary-text)', marginBottom: '4px', fontFamily: 'var(--ican-font-body)' }
1175
+ },
1176
+ props.label,
1177
+ props.required ? el('span', { style: { color: 'var(--ican-error)', marginLeft: '2px' } }, '*') : null
1178
+ ) : null,
1179
+ el('div', { style: { flex: props.inline ? 1 : undefined } }, props.children),
1180
+ props.hint && !props.error ? el('div', { style: { fontSize: '11px', color: 'var(--ican-tertiary-text)', marginTop: '4px' } }, props.hint) : null,
1181
+ props.error ? el('div', { style: { fontSize: '11px', color: 'var(--ican-error)', marginTop: '4px' } }, props.error) : null
1182
+ );
1183
+ }
1184
+
1185
+ function FormFeedback(props) {
1186
+ var valid = props.validInput !== false;
1187
+ return el('div', {
1188
+ className: props.className,
1189
+ style: Object.assign({ padding: '8px 12px', borderRadius: 'var(--ican-radius-sm)', fontSize: '12px', background: valid ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)', color: valid ? 'var(--ican-success)' : 'var(--ican-error)', border: '1px solid ' + (valid ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'), marginTop: '4px', fontFamily: 'var(--ican-font-body)' }, props.style || {})
1190
+ }, props.children);
1191
+ }
1192
+
1193
+ /* ─────────────────────────────────────────────────────
1194
+ * SVG CHARTS
1195
+ * ───────────────────────────────────────────────────── */
1196
+
1197
+ var PIE_COLORS = ['var(--ican-accent)', 'var(--ican-success)', 'var(--ican-warning)', 'var(--ican-info)', 'var(--ican-error)', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#06b6d4'];
1198
+
1199
+ function PieChart(props) {
1200
+ var R = window.React;
1201
+ var hoverState = R.useState(null); var hovered = hoverState[0]; var setHovered = hoverState[1];
1202
+ var posState = R.useState({ x: 0, y: 0 }); var pos = posState[0]; var setPos = posState[1];
1203
+
1204
+ var data = props.data || [];
1205
+ var size = props.size || 200;
1206
+ var donut = !!props.donut;
1207
+ var showLegend = props.showLegend !== false;
1208
+ var total = data.reduce(function (s, d) { return s + d.value; }, 0);
1209
+ if (total === 0) return el('div', { className: props.className, style: Object.assign({ color: 'var(--ican-secondary-text)', fontSize: '13px', textAlign: 'center', padding: '24px' }, props.style || {}) }, 'No data');
1210
+ var cx = size / 2; var cy = size / 2; var r = size * 0.4; var innerR = donut ? r * 0.55 : 0;
1211
+ var angle = -Math.PI / 2;
1212
+ var slices = data.map(function (d, i) {
1213
+ var sweep = (d.value / total) * 2 * Math.PI;
1214
+ var x1 = cx + r * Math.cos(angle); var y1 = cy + r * Math.sin(angle);
1215
+ angle += sweep;
1216
+ var x2 = cx + r * Math.cos(angle); var y2 = cy + r * Math.sin(angle);
1217
+ var largeArc = sweep > Math.PI ? 1 : 0;
1218
+ var color = d.color || PIE_COLORS[i % PIE_COLORS.length];
1219
+ var path;
1220
+ if (donut) {
1221
+ var xi1 = cx + innerR * Math.cos(angle - sweep); var yi1 = cy + innerR * Math.sin(angle - sweep);
1222
+ var xi2 = cx + innerR * Math.cos(angle); var yi2 = cy + innerR * Math.sin(angle);
1223
+ path = 'M' + x1 + ',' + y1 + ' A' + r + ',' + r + ' 0 ' + largeArc + ',1 ' + x2 + ',' + y2 + ' L' + xi2 + ',' + yi2 + ' A' + innerR + ',' + innerR + ' 0 ' + largeArc + ',0 ' + xi1 + ',' + yi1 + ' Z';
1224
+ } else {
1225
+ path = 'M' + cx + ',' + cy + ' L' + x1 + ',' + y1 + ' A' + r + ',' + r + ' 0 ' + largeArc + ',1 ' + x2 + ',' + y2 + ' Z';
1226
+ }
1227
+ return { path: path, color: color, d: d, i: i, pct: ((d.value / total) * 100).toFixed(1) };
1228
+ });
1229
+ function trackPos(e) {
1230
+ var rect = e.currentTarget.getBoundingClientRect();
1231
+ setPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1232
+ }
1233
+ return el('div', {
1234
+ className: props.className,
1235
+ style: Object.assign({ display: 'flex', flexDirection: showLegend ? 'column' : 'row', alignItems: 'center', gap: '16px', fontFamily: 'var(--ican-font-body)', position: 'relative' }, props.style || {}),
1236
+ onMouseMove: trackPos,
1237
+ onMouseLeave: function () { setHovered(null); }
1238
+ },
1239
+ el('svg', { width: size, height: size, viewBox: '0 0 ' + size + ' ' + size, style: { flexShrink: 0 } },
1240
+ slices.map(function (s) {
1241
+ return el('path', {
1242
+ key: s.i, d: s.path, fill: s.color,
1243
+ opacity: hovered == null || hovered.i === s.i ? 1 : 0.5,
1244
+ style: { transition: 'opacity 0.15s, transform 0.15s', cursor: 'pointer', transformOrigin: cx + 'px ' + cy + 'px', transform: hovered && hovered.i === s.i ? 'scale(1.03)' : 'scale(1)' },
1245
+ onMouseEnter: function () { setHovered(s); }
1246
+ });
1247
+ }),
1248
+ donut && props.centerLabel ? el('text', { x: cx, y: cy, textAnchor: 'middle', dominantBaseline: 'middle', fill: 'var(--ican-primary-text)', fontSize: '14', fontWeight: '600', fontFamily: 'var(--ican-font-body)' }, props.centerLabel) : null
1249
+ ),
1250
+ showLegend ? el('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px 16px' } },
1251
+ slices.map(function (s) {
1252
+ return el('div', {
1253
+ key: s.i,
1254
+ style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: 'var(--ican-primary-text)', cursor: 'pointer', opacity: hovered == null || hovered.i === s.i ? 1 : 0.5, transition: 'opacity 0.15s' },
1255
+ onMouseEnter: function () { setHovered(s); }
1256
+ },
1257
+ el('div', { style: { width: '10px', height: '10px', borderRadius: '2px', background: s.color, flexShrink: 0 } }),
1258
+ el('span', null, s.d.label + ' (' + s.pct + '%)')
1259
+ );
1260
+ })
1261
+ ) : null,
1262
+ hovered ? el('div', {
1263
+ style: { position: 'absolute', left: pos.x + 12, top: pos.y + 12, pointerEvents: 'none', background: 'var(--ican-card-bg)', color: 'var(--ican-primary-text)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '8px 12px', fontSize: '12px', fontFamily: 'var(--ican-font-body)', boxShadow: 'var(--ican-card-shadow, 0 4px 12px rgba(0,0,0,0.25))', zIndex: 10, whiteSpace: 'nowrap', backdropFilter: 'var(--ican-backdrop-filter)', WebkitBackdropFilter: 'var(--ican-backdrop-filter)' }
1264
+ },
1265
+ el('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '2px' } },
1266
+ el('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: hovered.color, display: 'inline-block' } }),
1267
+ el('strong', null, hovered.d.label)
1268
+ ),
1269
+ el('div', { style: { color: 'var(--ican-secondary-text)', fontSize: '11px' } }, hovered.d.value + ' • ' + hovered.pct + '%')
1270
+ ) : null
1271
+ );
1272
+ }
1273
+
1274
+ function LineChart(props) {
1275
+ var R = window.React;
1276
+ var hoverState = R.useState(null); var hovered = hoverState[0]; var setHovered = hoverState[1];
1277
+
1278
+ var series = props.series || [];
1279
+ if (series.length === 0 || !series.some(function (s) { return s.data && s.data.length; })) {
1280
+ return el('div', { className: props.className, style: Object.assign({ color: 'var(--ican-secondary-text)', fontSize: '13px', textAlign: 'center', padding: '24px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) }, 'No data');
1281
+ }
1282
+ var height = props.height || 200;
1283
+ var showGrid = props.showGrid !== false;
1284
+ var showDots = props.showDots !== false;
1285
+ var W = 400; var H = height;
1286
+ var PAD = { top: 16, right: 16, bottom: props.xLabel ? 36 : 24, left: props.yLabel ? 48 : 36 };
1287
+ var allY = series.reduce(function (a, s) { return a.concat(s.data.map(function (p) { return p.y; })); }, []);
1288
+ var yMin = Math.min.apply(null, allY); var yMax = Math.max.apply(null, allY);
1289
+ var yRange = yMax - yMin || 1;
1290
+ var allX = (series[0] && series[0].data ? series[0].data.map(function (p) { return p.x; }) : []);
1291
+ var n = allX.length;
1292
+ function toSvgX(i) { return PAD.left + (i / Math.max(n - 1, 1)) * (W - PAD.left - PAD.right); }
1293
+ function toSvgY(v) { return PAD.top + (1 - (v - yMin) / yRange) * (H - PAD.top - PAD.bottom); }
1294
+ var LINE_COLORS = ['var(--ican-accent)', 'var(--ican-success)', 'var(--ican-warning)', 'var(--ican-error)', 'var(--ican-info)'];
1295
+
1296
+ function trackHover(e) {
1297
+ var svg = e.currentTarget;
1298
+ var rect = svg.getBoundingClientRect();
1299
+ var localX = ((e.clientX - rect.left) / rect.width) * W;
1300
+ // Snap to nearest x-index
1301
+ var idx = Math.round((localX - PAD.left) / Math.max((W - PAD.left - PAD.right) / Math.max(n - 1, 1), 1));
1302
+ idx = Math.max(0, Math.min(n - 1, idx));
1303
+ setHovered({ idx: idx, label: allX[idx] });
1304
+ }
1305
+
1306
+ return el('div', { className: props.className, style: Object.assign({ fontFamily: 'var(--ican-font-body)', position: 'relative' }, props.style || {}) },
1307
+ el('svg', {
1308
+ viewBox: '0 0 ' + W + ' ' + H, width: '100%', style: { display: 'block' },
1309
+ onMouseMove: trackHover,
1310
+ onMouseLeave: function () { setHovered(null); }
1311
+ },
1312
+ showGrid ? el('g', null,
1313
+ [0,1,2,3,4].map(function (i) {
1314
+ var v = yMin + (yRange / 4) * i; var sy = toSvgY(v);
1315
+ return el('g', { key: i },
1316
+ el('line', { x1: PAD.left, y1: sy, x2: W - PAD.right, y2: sy, stroke: 'var(--ican-border)', strokeWidth: 0.5 }),
1317
+ el('text', { x: PAD.left - 4, y: sy, textAnchor: 'end', dominantBaseline: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)', fontFamily: 'var(--ican-font-body)' }, Math.round(v))
1318
+ );
1319
+ })
1320
+ ) : null,
1321
+ allX.filter(function (_, i) { return n <= 8 || i % Math.ceil(n / 8) === 0; }).map(function (x, i) {
1322
+ var idx = allX.indexOf(x);
1323
+ return el('text', { key: i, x: toSvgX(idx), y: H - PAD.bottom + 14, textAnchor: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)', fontFamily: 'var(--ican-font-body)' }, String(x));
1324
+ }),
1325
+ series.map(function (s, si) {
1326
+ var color = s.color || LINE_COLORS[si % LINE_COLORS.length];
1327
+ var pts = s.data.map(function (p, i) { return toSvgX(i) + ',' + toSvgY(p.y); }).join(' ');
1328
+ var fillPath = s.data.length > 0 ? 'M' + toSvgX(0) + ',' + toSvgY(s.data[0].y) + ' ' + s.data.map(function (p, i) { return 'L' + toSvgX(i) + ',' + toSvgY(p.y); }).join(' ') + ' L' + toSvgX(s.data.length - 1) + ',' + (H - PAD.bottom) + ' L' + toSvgX(0) + ',' + (H - PAD.bottom) + ' Z' : '';
1329
+ return el('g', { key: si },
1330
+ s.fill ? el('path', { d: fillPath, fill: color, opacity: 0.15 }) : null,
1331
+ el('polyline', { points: pts, fill: 'none', stroke: color, strokeWidth: 2, strokeLinejoin: 'round', strokeLinecap: 'round' }),
1332
+ showDots ? s.data.map(function (p, i) { return el('circle', { key: i, cx: toSvgX(i), cy: toSvgY(p.y), r: hovered && hovered.idx === i ? 5 : 3, fill: color, style: { transition: 'r 0.15s' } }); }) : null
1333
+ );
1334
+ }),
1335
+ // Hover crosshair line
1336
+ hovered ? el('line', { x1: toSvgX(hovered.idx), y1: PAD.top, x2: toSvgX(hovered.idx), y2: H - PAD.bottom, stroke: 'var(--ican-accent)', strokeWidth: 1, strokeDasharray: '3,3', opacity: 0.6 }) : null
1337
+ ),
1338
+ // Floating tooltip
1339
+ hovered ? el('div', {
1340
+ style: { position: 'absolute', left: ((toSvgX(hovered.idx) / W) * 100) + '%', top: '6px', transform: 'translateX(-50%)', pointerEvents: 'none', background: 'var(--ican-card-bg)', color: 'var(--ican-primary-text)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '8px 12px', fontSize: '12px', fontFamily: 'var(--ican-font-body)', boxShadow: 'var(--ican-card-shadow, 0 4px 12px rgba(0,0,0,0.25))', zIndex: 10, whiteSpace: 'nowrap', backdropFilter: 'var(--ican-backdrop-filter)', WebkitBackdropFilter: 'var(--ican-backdrop-filter)' }
1341
+ },
1342
+ el('div', { style: { fontWeight: 600, marginBottom: '4px' } }, String(hovered.label)),
1343
+ series.map(function (s, si) {
1344
+ var color = s.color || LINE_COLORS[si % LINE_COLORS.length];
1345
+ var p = s.data[hovered.idx];
1346
+ return p ? el('div', { key: si, style: { display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--ican-secondary-text)' } },
1347
+ el('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: color, display: 'inline-block', flexShrink: 0 } }),
1348
+ el('span', null, s.label + ': '),
1349
+ el('strong', { style: { color: 'var(--ican-primary-text)' } }, p.y)
1350
+ ) : null;
1351
+ })
1352
+ ) : null,
1353
+ el('div', { style: { display: 'flex', gap: '12px', flexWrap: 'wrap', marginTop: '8px' } },
1354
+ series.map(function (s, si) {
1355
+ return el('div', { key: si, style: { display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px', color: 'var(--ican-secondary-text)' } },
1356
+ el('div', { style: { width: '16px', height: '2px', background: s.color || LINE_COLORS[si % LINE_COLORS.length], borderRadius: '1px' } }),
1357
+ s.label
1358
+ );
1359
+ })
1360
+ )
1361
+ );
1362
+ }
1363
+
1364
+ function BarChart(props) {
1365
+ var R = window.React;
1366
+ var hoverState = R.useState(null); var hovered = hoverState[0]; var setHovered = hoverState[1];
1367
+ var posState = R.useState({ x: 0, y: 0 }); var pos = posState[0]; var setPos = posState[1];
1368
+
1369
+ var data = props.data || [];
1370
+ if (data.length === 0) {
1371
+ return el('div', { className: props.className, style: Object.assign({ color: 'var(--ican-secondary-text)', fontSize: '13px', textAlign: 'center', padding: '24px', fontFamily: 'var(--ican-font-body)' }, props.style || {}) }, 'No data');
1372
+ }
1373
+ var height = props.height || 200;
1374
+ var horizontal = !!props.horizontal;
1375
+ var showValues = props.showValues !== false;
1376
+ var showGrid = props.showGrid !== false;
1377
+ var maxVal = Math.max.apply(null, data.map(function (d) { return d.value; }).concat([1]));
1378
+ var W = 400; var H = height;
1379
+ var PAD = { top: 16, right: 16, bottom: 28, left: 36 };
1380
+ var chartW = W - PAD.left - PAD.right;
1381
+ var chartH = H - PAD.top - PAD.bottom;
1382
+ var nLen = data.length;
1383
+ var barGap = 4;
1384
+ var barW = (horizontal ? chartH : chartW) / nLen - barGap;
1385
+
1386
+ function trackPos(e) {
1387
+ var rect = e.currentTarget.getBoundingClientRect();
1388
+ setPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1389
+ }
1390
+
1391
+ return el('div', {
1392
+ className: props.className,
1393
+ style: Object.assign({ fontFamily: 'var(--ican-font-body)', position: 'relative' }, props.style || {}),
1394
+ onMouseMove: trackPos,
1395
+ onMouseLeave: function () { setHovered(null); }
1396
+ },
1397
+ el('svg', { viewBox: '0 0 ' + W + ' ' + H, width: '100%', style: { display: 'block' } },
1398
+ showGrid ? el('g', null,
1399
+ [0, 0.25, 0.5, 0.75, 1].map(function (f, i) {
1400
+ if (horizontal) {
1401
+ var x = PAD.left + f * chartW;
1402
+ return el('g', { key: i },
1403
+ el('line', { x1: x, y1: PAD.top, x2: x, y2: H - PAD.bottom, stroke: 'var(--ican-border)', strokeWidth: 0.5 }),
1404
+ el('text', { x: x, y: H - PAD.bottom + 12, textAnchor: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)' }, Math.round(maxVal * f))
1405
+ );
1406
+ }
1407
+ var y = PAD.top + (1 - f) * chartH;
1408
+ return el('g', { key: i },
1409
+ el('line', { x1: PAD.left, y1: y, x2: W - PAD.right, y2: y, stroke: 'var(--ican-border)', strokeWidth: 0.5 }),
1410
+ el('text', { x: PAD.left - 4, y: y, textAnchor: 'end', dominantBaseline: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)' }, Math.round(maxVal * f))
1411
+ );
1412
+ })
1413
+ ) : null,
1414
+ data.map(function (d, i) {
1415
+ var color = d.color || PIE_COLORS[i % PIE_COLORS.length];
1416
+ var ratio = d.value / maxVal;
1417
+ var isHover = hovered && hovered.i === i;
1418
+ if (horizontal) {
1419
+ var y = PAD.top + i * (barW + barGap);
1420
+ var w = ratio * chartW;
1421
+ return el('g', { key: i, onMouseEnter: function () { setHovered({ i: i, d: d, color: color }); }, style: { cursor: 'pointer' } },
1422
+ el('rect', { x: PAD.left, y: y, width: w, height: barW, fill: color, opacity: hovered == null || isHover ? 1 : 0.5, rx: 3, style: { transition: 'opacity 0.15s' } }),
1423
+ el('text', { x: PAD.left - 4, y: y + barW / 2, textAnchor: 'end', dominantBaseline: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)' }, d.label),
1424
+ showValues ? el('text', { x: PAD.left + w + 4, y: y + barW / 2, dominantBaseline: 'middle', fontSize: 9, fill: 'var(--ican-primary-text)', fontWeight: '600' }, d.value) : null
1425
+ );
1426
+ }
1427
+ var x = PAD.left + i * (barW + barGap);
1428
+ var bh = ratio * chartH;
1429
+ var by = PAD.top + chartH - bh;
1430
+ return el('g', { key: i, onMouseEnter: function () { setHovered({ i: i, d: d, color: color }); }, style: { cursor: 'pointer' } },
1431
+ el('rect', { x: x, y: by, width: barW, height: bh, fill: color, opacity: hovered == null || isHover ? 1 : 0.5, rx: 3, style: { transition: 'opacity 0.15s' } }),
1432
+ el('text', { x: x + barW / 2, y: H - PAD.bottom + 12, textAnchor: 'middle', fontSize: 9, fill: 'var(--ican-secondary-text)' }, d.label),
1433
+ showValues ? el('text', { x: x + barW / 2, y: by - 3, textAnchor: 'middle', fontSize: 9, fill: 'var(--ican-primary-text)', fontWeight: '600' }, d.value) : null
1434
+ );
1435
+ })
1436
+ ),
1437
+ hovered ? el('div', {
1438
+ style: { position: 'absolute', left: pos.x + 12, top: pos.y + 12, pointerEvents: 'none', background: 'var(--ican-card-bg)', color: 'var(--ican-primary-text)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '8px 12px', fontSize: '12px', fontFamily: 'var(--ican-font-body)', boxShadow: 'var(--ican-card-shadow, 0 4px 12px rgba(0,0,0,0.25))', zIndex: 10, whiteSpace: 'nowrap', backdropFilter: 'var(--ican-backdrop-filter)', WebkitBackdropFilter: 'var(--ican-backdrop-filter)' }
1439
+ },
1440
+ el('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '2px' } },
1441
+ el('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: hovered.color, display: 'inline-block' } }),
1442
+ el('strong', null, hovered.d.label)
1443
+ ),
1444
+ el('div', { style: { color: 'var(--ican-secondary-text)', fontSize: '11px' } }, 'Value: ' + hovered.d.value)
1445
+ ) : null
1446
+ );
1447
+ }
1448
+
1449
+ function RadialGauge(props) {
1450
+ var _h = useState(false); var hover = _h[0]; var setHover = _h[1];
1451
+ var value = props.value;
1452
+ var min = props.min != null ? props.min : 0;
1453
+ var max = props.max != null ? props.max : 100;
1454
+ var unit = props.unit || '';
1455
+ var size = props.size || 180;
1456
+ var thickness = props.thickness || 18;
1457
+ var defaultColors = props.colors || [{ color: 'var(--ican-success)', stopAt: 0.6 }, { color: 'var(--ican-warning)', stopAt: 0.85 }, { color: 'var(--ican-error)', stopAt: 1 }];
1458
+ var pct = Math.max(0, Math.min(1, (value - min) / (max - min)));
1459
+ var cx = size / 2; var cy = size / 2;
1460
+ var r = (size - thickness) / 2;
1461
+ var startAngle = -210; var endAngle = 30; var totalSpan = endAngle - startAngle;
1462
+ function toRad(deg) { return (deg * Math.PI) / 180; }
1463
+ function arc(startDeg, endDeg) {
1464
+ var s = toRad(startDeg); var e = toRad(endDeg);
1465
+ var x1 = cx + r * Math.cos(s); var y1 = cy + r * Math.sin(s);
1466
+ var x2 = cx + r * Math.cos(e); var y2 = cy + r * Math.sin(e);
1467
+ var large = endDeg - startDeg > 180 ? 1 : 0;
1468
+ return 'M' + x1 + ',' + y1 + ' A' + r + ',' + r + ' 0 ' + large + ',1 ' + x2 + ',' + y2;
1469
+ }
1470
+ var valueColor = (function () {
1471
+ for (var c = 0; c < defaultColors.length; c++) { if (pct <= defaultColors[c].stopAt) return defaultColors[c].color; }
1472
+ return defaultColors[defaultColors.length - 1].color;
1473
+ })();
1474
+ var valueDeg = startAngle + pct * totalSpan;
1475
+ return el('div', {
1476
+ className: props.className,
1477
+ onMouseEnter: function () { setHover(true); },
1478
+ onMouseLeave: function () { setHover(false); },
1479
+ style: Object.assign({ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', fontFamily: 'var(--ican-font-body)', position: 'relative' }, props.style || {})
1480
+ },
1481
+ el('svg', { width: size, height: size * 0.7, viewBox: '0 0 ' + size + ' ' + (size * 0.8), style: { overflow: 'visible' } },
1482
+ el('path', { d: arc(startAngle, endAngle), fill: 'none', stroke: 'var(--ican-border)', strokeWidth: thickness, strokeLinecap: 'round' }),
1483
+ el('path', { d: arc(startAngle, valueDeg), fill: 'none', stroke: valueColor, strokeWidth: thickness, strokeLinecap: 'round' }),
1484
+ el('text', { x: cx, y: cy * 1.1, textAnchor: 'middle', dominantBaseline: 'middle', fontSize: size * 0.13, fontWeight: '700', fill: 'var(--ican-primary-text)' }, value + unit),
1485
+ props.label ? el('text', { x: cx, y: cy * 1.28, textAnchor: 'middle', fontSize: size * 0.07, fill: 'var(--ican-secondary-text)' }, props.label) : null
1486
+ ),
1487
+ hover ? el('div', {
1488
+ style: { position: 'absolute', top: '-8px', left: '50%', transform: 'translate(-50%, -100%)', pointerEvents: 'none', background: 'var(--ican-card-bg)', color: 'var(--ican-primary-text)', border: '1px solid var(--ican-border)', borderRadius: 'var(--ican-radius-md)', padding: '6px 10px', fontSize: '12px', fontFamily: 'var(--ican-font-body)', boxShadow: 'var(--ican-card-shadow, 0 4px 12px rgba(0,0,0,0.25))', zIndex: 10, whiteSpace: 'nowrap' }
1489
+ },
1490
+ el('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
1491
+ el('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: valueColor, display: 'inline-block' } }),
1492
+ el('strong', null, value + unit),
1493
+ el('span', { style: { color: 'var(--ican-secondary-text)' } }, '(' + Math.round(pct * 100) + '% of ' + max + unit + ')')
1494
+ )
1495
+ ) : null
1496
+ );
1497
+ }
1498
+
1499
+ /* ─────────────────────────────────────────────────────
1500
+ * CHART LOADERS
1501
+ * ───────────────────────────────────────────────────── */
1502
+
1503
+ function shimmerStyle(animated) {
1504
+ return {
1505
+ background: animated ? 'linear-gradient(90deg, var(--ican-secondary-bg) 25%, var(--ican-border) 50%, var(--ican-secondary-bg) 75%)' : 'var(--ican-secondary-bg)',
1506
+ backgroundSize: '200% 100%',
1507
+ animation: animated ? 'ican-shimmer 1.5s infinite' : 'none',
1508
+ borderRadius: 'var(--ican-radius-sm)'
1509
+ };
1510
+ }
1511
+
1512
+ function ChartLoader(props) {
1513
+ return el('div', { className: props.className, style: Object.assign({ width: '100%', height: (props.height || 200) + 'px' }, shimmerStyle(props.animated !== false), props.style || {}) });
1514
+ }
1515
+
1516
+ function BarChartLoader(props) {
1517
+ var height = props.height || 200;
1518
+ var animated = props.animated !== false;
1519
+ return el('div', {
1520
+ className: props.className,
1521
+ style: Object.assign({ width: '100%', height: height + 'px', display: 'flex', alignItems: 'flex-end', gap: '8px', padding: '0 8px', boxSizing: 'border-box' }, props.style || {})
1522
+ },
1523
+ [0.6, 0.8, 0.4, 0.9, 0.5, 0.7, 0.3].map(function (h, i) {
1524
+ return el('div', { key: i, style: Object.assign({ flex: 1, height: (h * 100) + '%' }, shimmerStyle(animated)) });
1525
+ })
1526
+ );
1527
+ }
1528
+
1529
+ function DonutChartLoader(props) {
1530
+ var height = props.height || 160;
1531
+ var animated = props.animated !== false;
1532
+ var s = Math.min(height, 160);
1533
+ return el('div', {
1534
+ className: props.className,
1535
+ style: Object.assign({ width: s + 'px', height: s + 'px', borderRadius: '50%', background: 'conic-gradient(var(--ican-border) 0%, var(--ican-secondary-bg) 50%, var(--ican-border) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', animation: animated ? 'ican-spin 2s linear infinite' : 'none' }, props.style || {})
1536
+ },
1537
+ el('div', { style: { width: (s * 0.55) + 'px', height: (s * 0.55) + 'px', borderRadius: '50%', background: 'var(--ican-card-bg)' } })
1538
+ );
1539
+ }
1540
+
1541
+ function GaugeLoader(props) {
1542
+ var height = props.height || 120;
1543
+ var animated = props.animated !== false;
1544
+ return el('div', { className: props.className, style: Object.assign({ width: (height * 1.5) + 'px', height: height + 'px', position: 'relative' }, props.style || {}) },
1545
+ el('div', { style: Object.assign({ position: 'absolute', bottom: 0, left: 0, right: 0, height: height + 'px', borderRadius: height + 'px ' + height + 'px 0 0' }, shimmerStyle(animated)) })
1546
+ );
1547
+ }
1548
+
1549
+ /* ─────────────────────────────────────────────────────
1550
+ * MAP COMPONENT (lazy Leaflet loader)
1551
+ * ───────────────────────────────────────────────────── */
1552
+
1553
+ function MapComponent(props) {
1554
+ var ref = useRef(null);
1555
+ var _map = useRef(null);
1556
+ var _s = useState('loading');
1557
+ var status = _s[0]; var setStatus = _s[1];
1558
+
1559
+ function initMap(L) {
1560
+ if (!ref.current || _map.current) return;
1561
+ try {
1562
+ var center = props.center || [51.505, -0.09];
1563
+ var zoom = props.zoom || 13;
1564
+ var map = L.map(ref.current).setView(center, zoom);
1565
+ _map.current = map;
1566
+ if (props.staticImage) {
1567
+ var si = props.staticImage;
1568
+ var bounds = [[0, 0], [si.height, si.width]];
1569
+ L.imageOverlay(si.url, bounds).addTo(map);
1570
+ map.fitBounds(bounds);
1571
+ } else {
1572
+ var tileUrl = props.tileUrl || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
1573
+ L.tileLayer(tileUrl, { attribution: '© OpenStreetMap contributors' }).addTo(map);
1574
+ }
1575
+ (props.markers || []).forEach(function (m) {
1576
+ var marker = L.marker([m.lat, m.lng]).addTo(map);
1577
+ if (m.popup) marker.bindPopup(m.popup);
1578
+ if (props.onMarkerClick) marker.on('click', function () { props.onMarkerClick(m); });
1579
+ });
1580
+ setStatus('ready');
1581
+ } catch (e) { setStatus('error'); }
1582
+ }
1583
+
1584
+ useEffect(function () {
1585
+ if (window.L) { initMap(window.L); return; }
1586
+ if (!document.getElementById('leaflet-css')) {
1587
+ var link = document.createElement('link');
1588
+ link.id = 'leaflet-css';
1589
+ link.rel = 'stylesheet';
1590
+ link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
1591
+ document.head.appendChild(link);
1592
+ }
1593
+ var existing = document.getElementById('leaflet-js');
1594
+ if (!existing) {
1595
+ var script = document.createElement('script');
1596
+ script.id = 'leaflet-js';
1597
+ script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
1598
+ script.async = true;
1599
+ script.onload = function () { initMap(window.L); };
1600
+ script.onerror = function () { setStatus('error'); };
1601
+ document.head.appendChild(script);
1602
+ } else {
1603
+ var attempts = 0;
1604
+ var poll = setInterval(function () {
1605
+ attempts += 1;
1606
+ if (window.L) { clearInterval(poll); initMap(window.L); }
1607
+ else if (attempts > 150) { clearInterval(poll); }
1608
+ }, 100);
1609
+ return function () { clearInterval(poll); };
1610
+ }
1611
+ return function () { if (_map.current) { _map.current.remove(); _map.current = null; } };
1612
+ }, []);
1613
+
1614
+ return el('div', {
1615
+ className: props.className,
1616
+ style: Object.assign({ position: 'relative', width: '100%', height: props.height || '300px', borderRadius: 'var(--ican-radius-md)', overflow: 'hidden', border: '1px solid var(--ican-border)' }, props.style || {})
1617
+ },
1618
+ el('div', { ref: ref, style: { width: '100%', height: '100%' } }),
1619
+ status === 'loading' ? el('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--ican-secondary-bg)', color: 'var(--ican-secondary-text)', fontSize: '13px', fontFamily: 'var(--ican-font-body)', gap: '8px' } }, '↻ Loading map…') : null,
1620
+ status === 'error' ? el('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--ican-error-dim)', color: 'var(--ican-error)', fontSize: '13px', fontFamily: 'var(--ican-font-body)' } }, '⚠ Map unavailable') : null
1621
+ );
1622
+ }
1623
+
1624
+ /* ─────────────────────────────────────────────────────
1625
+ * INJECT KEYFRAME STYLES
1626
+ * ───────────────────────────────────────────────────── */
1627
+
1628
+ (function injectStyles() {
1629
+ if (document.getElementById('ican-keyframes')) return;
1630
+ var style = document.createElement('style');
1631
+ style.id = 'ican-keyframes';
1632
+ style.textContent = [
1633
+ '@keyframes ican-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }',
1634
+ '@keyframes ican-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }',
1635
+ '@keyframes ican-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }',
1636
+ // Themed focus ring for ICan form inputs (replaces browser default outline removed via inline style).
1637
+ '[data-module="ican"] input:focus,',
1638
+ '[data-module="ican"] textarea:focus,',
1639
+ '[data-module="ican"] select:focus {',
1640
+ ' border-color: var(--ican-accent) !important;',
1641
+ ' box-shadow: 0 0 0 3px var(--ican-accent-dim, rgba(99,102,241,0.15)) !important;',
1642
+ '}',
1643
+ // Same selectors un-scoped for the dev harness (which may not have data-module).
1644
+ 'body input:focus,',
1645
+ 'body textarea:focus,',
1646
+ 'body select:focus {',
1647
+ ' border-color: var(--ican-accent);',
1648
+ ' box-shadow: 0 0 0 3px var(--ican-accent-dim, rgba(99,102,241,0.15));',
1649
+ '}'
1650
+ ].join('\n');
1651
+ document.head.appendChild(style);
1652
+ })();
1653
+
1654
+ /* ─────────────────────────────────────────────────────
1655
+ * EXPORT
1656
+ * ───────────────────────────────────────────────────── */
1657
+
1658
+ window.ICanComponents = {
1659
+ // Layout
1660
+ WidgetWrapper: WidgetWrapper,
1661
+ Loading: Loading,
1662
+ TitleBar: TitleBar,
1663
+ FilterPanel: FilterPanel,
1664
+ ErrorDisplay: ErrorDisplay,
1665
+ Card: Card,
1666
+ DataList: DataList,
1667
+ // Buttons
1668
+ Button: Button,
1669
+ AsyncButton: AsyncButton,
1670
+ ConfirmButton: ConfirmButton,
1671
+ IconButton: IconButton,
1672
+ // Form Inputs
1673
+ Input: Input,
1674
+ TextArea: TextArea,
1675
+ Select: Select,
1676
+ Checkbox: Checkbox,
1677
+ MultiSelect: MultiSelect,
1678
+ ToggleFilter: ToggleFilter,
1679
+ ColorPicker: ColorPicker,
1680
+ SearchInput: SearchInput,
1681
+ FormField: FormField,
1682
+ Label: Label,
1683
+ FormFeedback: FormFeedback,
1684
+ // Date & Time
1685
+ DatePicker: DatePicker,
1686
+ TimePicker: TimePicker,
1687
+ DateRangePicker: DateRangePicker,
1688
+ DateTimePicker: DateTimePicker,
1689
+ CalendarView: CalendarView,
1690
+ // Data Display
1691
+ Badge: Badge,
1692
+ StatCard: StatCard,
1693
+ EmptyState: EmptyState,
1694
+ DataTable: DataTable,
1695
+ DataGrid: DataGrid,
1696
+ ItemCard: ItemCard,
1697
+ ItemListCard: ItemListCard,
1698
+ HorizontalScrollList: HorizontalScrollList,
1699
+ NotificationBlock: NotificationBlock,
1700
+ ProfileImage: ProfileImage,
1701
+ SampleDataLabel: SampleDataLabel,
1702
+ // Overlays
1703
+ Tooltip: Tooltip,
1704
+ Popover: Popover,
1705
+ DropDownButton: DropDownButton,
1706
+ Tabs: Tabs,
1707
+ Modal: Modal,
1708
+ Pagination: Pagination,
1709
+ // Wizards
1710
+ Wizard: Wizard,
1711
+ ModalWizard: ModalWizard,
1712
+ // Charts
1713
+ PieChart: PieChart,
1714
+ LineChart: LineChart,
1715
+ BarChart: BarChart,
1716
+ RadialGauge: RadialGauge,
1717
+ ChartLoader: ChartLoader,
1718
+ BarChartLoader: BarChartLoader,
1719
+ DonutChartLoader: DonutChartLoader,
1720
+ GaugeLoader: GaugeLoader,
1721
+ // Map
1722
+ MapComponent: MapComponent
1723
+ };
1724
+
1725
+ if (!window.registerWidget) window.registerWidget = function () {};
1726
+ if (!window.registerLink) window.registerLink = function () {};
1727
+ if (!window.registerUI) window.registerUI = function () {};
1728
+ if (!window.registerMenuItem) window.registerMenuItem = function () {};
1729
+ if (!window.registerLocalization) window.registerLocalization = function () {};
1730
+ if (!window.UXPComponents) window.UXPComponents = {};
1731
+ if (!window.WidgetDesignerComponents) window.WidgetDesignerComponents = {};
1732
+
1733
+ console.log('[ICan] Components loaded — InfAIra Canvas v2.0 (canonical, matches widget-bridge.ts)');
1734
+ })();