ezfw-core 1.0.63 → 1.0.65

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.
@@ -271,9 +271,9 @@ export class StaticHtmlRenderer {
271
271
 
272
272
  // Check if it's a registered component
273
273
  if (ctx.registry.has(eztype)) {
274
- // Pass the entire config as props (excluding eztype and items which are structural)
275
- // Props can be either in config.props or directly on the config object
276
- const { eztype: _, items: __, ...configProps } = config;
274
+ // Pass the entire config as props (including items - components handle their own structure)
275
+ // In SPA mode, components receive full config via constructor, SSR should match
276
+ const { eztype: _, ...configProps } = config;
277
277
  const mergedProps = { ...configProps, ...(config.props || {}) };
278
278
  return this.renderComponent(eztype, ctx, mergedProps);
279
279
  }
@@ -5,6 +5,9 @@
5
5
  * Uses a SINGLETON pattern - registry persists across module reloads.
6
6
  * Components can use ez.define() normally and definitions
7
7
  * are stored in the registry for SSR to access.
8
+ *
9
+ * ALL framework components have SSR templates here that render
10
+ * reasonable static HTML. Interactive features activate on hydration.
8
11
  */
9
12
 
10
13
  // Use globalThis to store the singleton registry (persists across module reloads)
@@ -18,8 +21,29 @@ if (!globalThis.__ezSSRControllers) {
18
21
  const ssrRegistry = globalThis.__ezSSRRegistry;
19
22
  const ssrControllers = globalThis.__ezSSRControllers;
20
23
 
21
- // Framework components for SSR (declarative versions of class-based Ez components)
24
+ // Helper: semantic color map
25
+ const semanticColors = {
26
+ primary: 'var(--ez-primary)', secondary: 'var(--ez-secondary)',
27
+ success: 'var(--ez-success)', danger: 'var(--ez-danger)',
28
+ warning: 'var(--ez-warning)', info: 'var(--ez-info)',
29
+ muted: 'var(--ez-muted)', light: 'var(--ez-light)', dark: 'var(--ez-dark)'
30
+ };
31
+
32
+ // Helper: size map for icons
33
+ const sizeMap = {
34
+ xxs: '0.65em', xs: '0.75em', sm: '0.875em',
35
+ lg: '1.25em', xl: '1.5em', '2x': '2em',
36
+ '3x': '3em', '4x': '4em', '5x': '5em'
37
+ };
38
+
39
+ /**
40
+ * Register ALL framework components for SSR
41
+ * Each component has a template() that returns static HTML config
42
+ */
22
43
  function registerFrameworkComponents() {
44
+ // ==================== BASIC COMPONENTS ====================
45
+
46
+ // EzIcon - Font Awesome icon
23
47
  ssrRegistry['EzIcon'] = {
24
48
  template(props) {
25
49
  const type = props.type || 'solid';
@@ -29,18 +53,6 @@ function registerFrameworkComponents() {
29
53
  if (props.pulse) classes.push('fa-pulse');
30
54
  if (props.bounce) classes.push('fa-bounce');
31
55
 
32
- const sizeMap = {
33
- xxs: '0.65em', xs: '0.75em', sm: '0.875em',
34
- lg: '1.25em', xl: '1.5em', '2x': '2em',
35
- '3x': '3em', '4x': '4em', '5x': '5em'
36
- };
37
- const semanticColors = {
38
- primary: 'var(--ez-primary)', secondary: 'var(--ez-secondary)',
39
- success: 'var(--ez-success)', danger: 'var(--ez-danger)',
40
- warning: 'var(--ez-warning)', info: 'var(--ez-info)',
41
- muted: 'var(--ez-muted)', light: 'var(--ez-light)', dark: 'var(--ez-dark)'
42
- };
43
-
44
56
  return {
45
57
  eztype: 'i',
46
58
  cls: classes.join(' '),
@@ -52,6 +64,7 @@ function registerFrameworkComponents() {
52
64
  }
53
65
  };
54
66
 
67
+ // EzLabel - Simple text label
55
68
  ssrRegistry['EzLabel'] = {
56
69
  template(props) {
57
70
  return {
@@ -63,25 +76,857 @@ function registerFrameworkComponents() {
63
76
  }
64
77
  };
65
78
 
79
+ // EzButton - Button with optional icon
66
80
  ssrRegistry['EzButton'] = {
67
81
  template(props) {
68
82
  const items = [];
69
83
  if (props.iconCls) {
70
84
  items.push({ eztype: 'i', cls: props.iconCls });
71
85
  }
86
+ if (props.icon) {
87
+ items.push({ eztype: 'EzIcon', fa: props.icon, type: props.iconType });
88
+ }
72
89
  if (props.text) {
73
90
  items.push({ eztype: 'span', text: props.text });
74
91
  }
92
+
93
+ const variantStyles = {
94
+ primary: { background: 'var(--ez-primary)', color: 'white' },
95
+ secondary: { background: 'var(--ez-secondary)', color: 'white' },
96
+ success: { background: 'var(--ez-success)', color: 'white' },
97
+ danger: { background: 'var(--ez-danger)', color: 'white' },
98
+ warning: { background: 'var(--ez-warning)', color: 'black' },
99
+ outline: { background: 'transparent', border: '1px solid var(--ez-border)' },
100
+ ghost: { background: 'transparent' },
101
+ link: { background: 'transparent', color: 'var(--ez-primary)' }
102
+ };
103
+
75
104
  return {
76
105
  eztype: 'button',
77
- cls: props.cls,
78
- style: props.style,
106
+ cls: `ez-button ${props.variant || ''} ${props.cls || ''}`.trim(),
107
+ style: { ...variantStyles[props.variant], ...props.style },
79
108
  attr: { type: props.type || 'button', disabled: props.disabled },
80
109
  items: items.length > 0 ? items : undefined,
81
110
  text: items.length === 0 ? props.text : undefined
82
111
  };
83
112
  }
84
113
  };
114
+
115
+ // EzButtonGroup - Group of buttons
116
+ ssrRegistry['EzButtonGroup'] = {
117
+ template(props) {
118
+ return {
119
+ eztype: 'div',
120
+ cls: 'ez-button-group',
121
+ layout: 'hbox',
122
+ gap: 1,
123
+ style: props.style,
124
+ items: props.items || []
125
+ };
126
+ }
127
+ };
128
+
129
+ // ==================== FORM INPUTS ====================
130
+
131
+ // EzInput - Text input field
132
+ ssrRegistry['EzInput'] = {
133
+ template(props) {
134
+ const items = [];
135
+
136
+ if (props.label) {
137
+ items.push({
138
+ eztype: 'label',
139
+ cls: 'ez-input-label',
140
+ text: props.label,
141
+ items: props.required ? [{ eztype: 'span', text: ' *', style: { color: 'var(--ez-danger)' } }] : undefined
142
+ });
143
+ }
144
+
145
+ items.push({
146
+ eztype: 'input',
147
+ cls: 'ez-input',
148
+ attr: {
149
+ type: props.inputType || 'text',
150
+ placeholder: props.placeholder,
151
+ disabled: props.disabled,
152
+ readonly: props.readonly,
153
+ value: props.value || ''
154
+ },
155
+ style: props.style
156
+ });
157
+
158
+ return {
159
+ eztype: 'div',
160
+ cls: 'ez-input-wrapper',
161
+ layout: 'vbox',
162
+ gap: 0.5,
163
+ items
164
+ };
165
+ }
166
+ };
167
+
168
+ // EzTextarea - Multi-line text input
169
+ ssrRegistry['EzTextarea'] = {
170
+ template(props) {
171
+ const items = [];
172
+
173
+ if (props.label) {
174
+ items.push({
175
+ eztype: 'label',
176
+ cls: 'ez-textarea-label',
177
+ text: props.label
178
+ });
179
+ }
180
+
181
+ items.push({
182
+ eztype: 'textarea',
183
+ cls: 'ez-textarea',
184
+ attr: {
185
+ placeholder: props.placeholder,
186
+ disabled: props.disabled,
187
+ rows: props.rows || 4
188
+ },
189
+ text: props.value || '',
190
+ style: props.style
191
+ });
192
+
193
+ return {
194
+ eztype: 'div',
195
+ cls: 'ez-textarea-wrapper',
196
+ layout: 'vbox',
197
+ gap: 0.5,
198
+ items
199
+ };
200
+ }
201
+ };
202
+
203
+ // EzSelect - Dropdown select
204
+ ssrRegistry['EzSelect'] = {
205
+ template(props) {
206
+ const options = (props.options || []).map(opt => ({
207
+ eztype: 'option',
208
+ attr: { value: opt.value || opt },
209
+ text: opt.label || opt.text || opt
210
+ }));
211
+
212
+ const items = [];
213
+ if (props.label) {
214
+ items.push({ eztype: 'label', cls: 'ez-select-label', text: props.label });
215
+ }
216
+ items.push({
217
+ eztype: 'select',
218
+ cls: 'ez-select',
219
+ attr: { disabled: props.disabled },
220
+ style: props.style,
221
+ items: options
222
+ });
223
+
224
+ return {
225
+ eztype: 'div',
226
+ cls: 'ez-select-wrapper',
227
+ layout: 'vbox',
228
+ gap: 0.5,
229
+ items
230
+ };
231
+ }
232
+ };
233
+
234
+ // EzCheckbox - Checkbox input
235
+ ssrRegistry['EzCheckbox'] = {
236
+ template(props) {
237
+ return {
238
+ eztype: 'label',
239
+ cls: 'ez-checkbox',
240
+ layout: 'hbox',
241
+ gap: 1,
242
+ style: { alignItems: 'center', cursor: 'pointer', ...props.style },
243
+ items: [
244
+ {
245
+ eztype: 'input',
246
+ attr: { type: 'checkbox', checked: props.checked, disabled: props.disabled }
247
+ },
248
+ { eztype: 'span', text: props.label || props.text || '' }
249
+ ]
250
+ };
251
+ }
252
+ };
253
+
254
+ // EzRadio - Radio button group
255
+ ssrRegistry['EzRadio'] = {
256
+ template(props) {
257
+ const options = (props.options || []).map(opt => ({
258
+ eztype: 'label',
259
+ cls: 'ez-radio-option',
260
+ layout: 'hbox',
261
+ gap: 1,
262
+ style: { alignItems: 'center', cursor: 'pointer' },
263
+ items: [
264
+ {
265
+ eztype: 'input',
266
+ attr: { type: 'radio', name: props.name, value: opt.value || opt, checked: props.value === (opt.value || opt) }
267
+ },
268
+ { eztype: 'span', text: opt.label || opt }
269
+ ]
270
+ }));
271
+
272
+ return {
273
+ eztype: 'div',
274
+ cls: 'ez-radio-group',
275
+ layout: props.horizontal ? 'hbox' : 'vbox',
276
+ gap: 1,
277
+ style: props.style,
278
+ items: options
279
+ };
280
+ }
281
+ };
282
+
283
+ // EzSwitch - Toggle switch
284
+ ssrRegistry['EzSwitch'] = {
285
+ template(props) {
286
+ return {
287
+ eztype: 'label',
288
+ cls: 'ez-switch',
289
+ layout: 'hbox',
290
+ gap: 1,
291
+ style: { alignItems: 'center', cursor: 'pointer', ...props.style },
292
+ items: [
293
+ {
294
+ eztype: 'div',
295
+ cls: `ez-switch-track ${props.checked ? 'checked' : ''}`,
296
+ style: {
297
+ width: '40px', height: '20px', borderRadius: '10px',
298
+ background: props.checked ? 'var(--ez-primary)' : 'var(--ez-border)',
299
+ position: 'relative'
300
+ },
301
+ items: [{
302
+ eztype: 'div',
303
+ cls: 'ez-switch-thumb',
304
+ style: {
305
+ width: '16px', height: '16px', borderRadius: '50%',
306
+ background: 'white', position: 'absolute', top: '2px',
307
+ left: props.checked ? '22px' : '2px',
308
+ transition: 'left 0.2s'
309
+ }
310
+ }]
311
+ },
312
+ props.label ? { eztype: 'span', text: props.label } : null
313
+ ].filter(Boolean)
314
+ };
315
+ }
316
+ };
317
+
318
+ // EzForm - Form container
319
+ ssrRegistry['EzForm'] = {
320
+ template(props) {
321
+ return {
322
+ eztype: 'form',
323
+ cls: 'ez-form',
324
+ layout: 'vbox',
325
+ gap: 2,
326
+ style: props.style,
327
+ items: props.items || []
328
+ };
329
+ }
330
+ };
331
+
332
+ // ==================== DATE/TIME ====================
333
+
334
+ // EzDatePicker - Date picker
335
+ ssrRegistry['EzDatePicker'] = {
336
+ template(props) {
337
+ const items = [];
338
+ if (props.label) {
339
+ items.push({ eztype: 'label', cls: 'ez-datepicker-label', text: props.label });
340
+ }
341
+ items.push({
342
+ eztype: 'input',
343
+ cls: 'ez-datepicker',
344
+ attr: { type: 'date', value: props.value, disabled: props.disabled },
345
+ style: props.style
346
+ });
347
+
348
+ return {
349
+ eztype: 'div',
350
+ cls: 'ez-datepicker-wrapper',
351
+ layout: 'vbox',
352
+ gap: 0.5,
353
+ items
354
+ };
355
+ }
356
+ };
357
+
358
+ // EzTimePicker - Time picker
359
+ ssrRegistry['EzTimePicker'] = {
360
+ template(props) {
361
+ const items = [];
362
+ if (props.label) {
363
+ items.push({ eztype: 'label', cls: 'ez-timepicker-label', text: props.label });
364
+ }
365
+ items.push({
366
+ eztype: 'input',
367
+ cls: 'ez-timepicker',
368
+ attr: { type: 'time', value: props.value, disabled: props.disabled },
369
+ style: props.style
370
+ });
371
+
372
+ return {
373
+ eztype: 'div',
374
+ cls: 'ez-timepicker-wrapper',
375
+ layout: 'vbox',
376
+ gap: 0.5,
377
+ items
378
+ };
379
+ }
380
+ };
381
+
382
+ // ==================== DISPLAY COMPONENTS ====================
383
+
384
+ // EzAvatar - User avatar
385
+ ssrRegistry['EzAvatar'] = {
386
+ template(props) {
387
+ const size = props.size || 40;
388
+ const initials = (props.name || '??').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
389
+
390
+ if (props.src) {
391
+ return {
392
+ eztype: 'img',
393
+ cls: 'ez-avatar',
394
+ attr: { src: props.src, alt: props.name || 'Avatar' },
395
+ style: {
396
+ width: `${size}px`, height: `${size}px`,
397
+ borderRadius: props.rounded ? '50%' : '8px',
398
+ objectFit: 'cover',
399
+ ...props.style
400
+ }
401
+ };
402
+ }
403
+
404
+ return {
405
+ eztype: 'div',
406
+ cls: 'ez-avatar',
407
+ text: initials,
408
+ style: {
409
+ width: `${size}px`, height: `${size}px`,
410
+ borderRadius: props.rounded !== false ? '50%' : '8px',
411
+ background: props.color || 'var(--ez-primary)',
412
+ color: 'white',
413
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
414
+ fontWeight: '600', fontSize: `${size * 0.4}px`,
415
+ ...props.style
416
+ }
417
+ };
418
+ }
419
+ };
420
+
421
+ // EzBadge - Status badge
422
+ ssrRegistry['EzBadge'] = {
423
+ template(props) {
424
+ const colorMap = {
425
+ primary: { bg: 'var(--ez-primary)', color: 'white' },
426
+ secondary: { bg: 'var(--ez-secondary)', color: 'white' },
427
+ success: { bg: 'var(--ez-success)', color: 'white' },
428
+ danger: { bg: 'var(--ez-danger)', color: 'white' },
429
+ warning: { bg: 'var(--ez-warning)', color: 'black' },
430
+ info: { bg: 'var(--ez-info)', color: 'white' }
431
+ };
432
+ const colors = colorMap[props.variant] || colorMap.primary;
433
+
434
+ return {
435
+ eztype: 'span',
436
+ cls: 'ez-badge',
437
+ text: props.text || props.label || '',
438
+ style: {
439
+ display: 'inline-flex', alignItems: 'center',
440
+ padding: '2px 8px', borderRadius: '12px',
441
+ fontSize: '12px', fontWeight: '500',
442
+ background: colors.bg, color: colors.color,
443
+ ...props.style
444
+ }
445
+ };
446
+ }
447
+ };
448
+
449
+ // EzCard - Card container
450
+ ssrRegistry['EzCard'] = {
451
+ template(props) {
452
+ const items = [];
453
+
454
+ if (props.title || props.subtitle) {
455
+ items.push({
456
+ eztype: 'div',
457
+ cls: 'ez-card-header',
458
+ layout: 'vbox',
459
+ style: { padding: '16px', borderBottom: '1px solid var(--ez-border)' },
460
+ items: [
461
+ props.title ? { eztype: 'h3', text: props.title, style: { margin: 0, fontWeight: '600' } } : null,
462
+ props.subtitle ? { eztype: 'p', text: props.subtitle, style: { margin: '4px 0 0', color: 'var(--ez-muted)' } } : null
463
+ ].filter(Boolean)
464
+ });
465
+ }
466
+
467
+ items.push({
468
+ eztype: 'div',
469
+ cls: 'ez-card-body',
470
+ style: { padding: '16px' },
471
+ items: props.items || []
472
+ });
473
+
474
+ return {
475
+ eztype: 'div',
476
+ cls: 'ez-card',
477
+ style: {
478
+ background: 'var(--ez-surface)',
479
+ border: '1px solid var(--ez-border)',
480
+ borderRadius: '8px',
481
+ ...props.style
482
+ },
483
+ items
484
+ };
485
+ }
486
+ };
487
+
488
+ // EzPanel - Collapsible panel
489
+ ssrRegistry['EzPanel'] = {
490
+ template(props) {
491
+ return {
492
+ eztype: 'div',
493
+ cls: 'ez-panel',
494
+ style: {
495
+ border: '1px solid var(--ez-border)',
496
+ borderRadius: '4px',
497
+ ...props.style
498
+ },
499
+ items: [
500
+ {
501
+ eztype: 'div',
502
+ cls: 'ez-panel-header',
503
+ layout: 'hbox',
504
+ style: {
505
+ padding: '12px 16px',
506
+ background: 'var(--ez-surface)',
507
+ borderBottom: props.collapsed ? 'none' : '1px solid var(--ez-border)',
508
+ cursor: 'pointer'
509
+ },
510
+ items: [
511
+ { eztype: 'span', text: props.title || '', style: { fontWeight: '600', flex: 1 } },
512
+ { eztype: 'EzIcon', fa: props.collapsed ? 'chevron-down' : 'chevron-up' }
513
+ ]
514
+ },
515
+ !props.collapsed ? {
516
+ eztype: 'div',
517
+ cls: 'ez-panel-body',
518
+ style: { padding: '16px' },
519
+ items: props.items || []
520
+ } : null
521
+ ].filter(Boolean)
522
+ };
523
+ }
524
+ };
525
+
526
+ // EzPaper - Paper surface
527
+ ssrRegistry['EzPaper'] = {
528
+ template(props) {
529
+ return {
530
+ eztype: 'div',
531
+ cls: 'ez-paper',
532
+ style: {
533
+ background: 'var(--ez-surface)',
534
+ borderRadius: '8px',
535
+ boxShadow: props.elevation ? `0 ${props.elevation * 2}px ${props.elevation * 4}px rgba(0,0,0,0.1)` : undefined,
536
+ padding: '16px',
537
+ ...props.style
538
+ },
539
+ items: props.items || []
540
+ };
541
+ }
542
+ };
543
+
544
+ // EzSkeleton - Loading skeleton
545
+ ssrRegistry['EzSkeleton'] = {
546
+ template(props) {
547
+ return {
548
+ eztype: 'div',
549
+ cls: 'ez-skeleton',
550
+ style: {
551
+ background: 'linear-gradient(90deg, var(--ez-border) 25%, var(--ez-surface) 50%, var(--ez-border) 75%)',
552
+ backgroundSize: '200% 100%',
553
+ animation: 'skeleton-loading 1.5s infinite',
554
+ borderRadius: props.variant === 'circle' ? '50%' : '4px',
555
+ width: props.width || '100%',
556
+ height: props.height || '20px',
557
+ ...props.style
558
+ }
559
+ };
560
+ }
561
+ };
562
+
563
+ // EzTooltip - Tooltip wrapper (renders children only in SSR)
564
+ ssrRegistry['EzTooltip'] = {
565
+ template(props) {
566
+ return {
567
+ eztype: 'div',
568
+ cls: 'ez-tooltip-wrapper',
569
+ attr: { title: props.text || props.content },
570
+ items: props.items || []
571
+ };
572
+ }
573
+ };
574
+
575
+ // ==================== TABS & NAVIGATION ====================
576
+
577
+ // EzTabPanel - Tabbed content (renders first/active tab in SSR)
578
+ ssrRegistry['EzTabPanel'] = {
579
+ template(props) {
580
+ const tabs = props.items || [];
581
+ const activeId = props.activeTab || props.active || (tabs[0]?.id);
582
+ const activeTab = tabs.find(t => t.id === activeId) || tabs[0];
583
+
584
+ // Tab headers
585
+ const tabHeaders = tabs.map(tab => ({
586
+ eztype: 'div',
587
+ cls: `ez-tab ${tab.id === activeId ? 'active' : ''}`,
588
+ text: tab.title || tab.label || tab.id,
589
+ style: {
590
+ padding: '8px 16px',
591
+ cursor: 'pointer',
592
+ borderBottom: tab.id === activeId ? '2px solid var(--ez-primary)' : '2px solid transparent',
593
+ color: tab.id === activeId ? 'var(--ez-primary)' : 'inherit'
594
+ }
595
+ }));
596
+
597
+ return {
598
+ eztype: 'div',
599
+ cls: 'ez-tab-panel',
600
+ style: props.style,
601
+ items: [
602
+ {
603
+ eztype: 'div',
604
+ cls: 'ez-tab-headers',
605
+ layout: 'hbox',
606
+ style: { borderBottom: '1px solid var(--ez-border)' },
607
+ items: tabHeaders
608
+ },
609
+ {
610
+ eztype: 'div',
611
+ cls: 'ez-tab-content',
612
+ style: { padding: '16px' },
613
+ items: activeTab?.items || []
614
+ }
615
+ ]
616
+ };
617
+ }
618
+ };
619
+
620
+ // ==================== DATA DISPLAY ====================
621
+
622
+ // EzGrid - Data grid (renders simple table in SSR)
623
+ ssrRegistry['EzGrid'] = {
624
+ template(props) {
625
+ const columns = props.columns || [];
626
+ const data = props.data || [];
627
+
628
+ const headerCells = columns.map(col => ({
629
+ eztype: 'th',
630
+ text: col.text || col.header || col.index,
631
+ style: { padding: '12px', textAlign: 'left', fontWeight: '600', borderBottom: '2px solid var(--ez-border)' }
632
+ }));
633
+
634
+ const rows = data.slice(0, 20).map(row => ({
635
+ eztype: 'tr',
636
+ items: columns.map(col => ({
637
+ eztype: 'td',
638
+ text: String(row[col.index] ?? ''),
639
+ style: { padding: '12px', borderBottom: '1px solid var(--ez-border)' }
640
+ }))
641
+ }));
642
+
643
+ return {
644
+ eztype: 'div',
645
+ cls: 'ez-grid',
646
+ style: { overflow: 'auto', ...props.style },
647
+ items: [{
648
+ eztype: 'table',
649
+ style: { width: '100%', borderCollapse: 'collapse' },
650
+ items: [
651
+ { eztype: 'thead', items: [{ eztype: 'tr', items: headerCells }] },
652
+ { eztype: 'tbody', items: rows }
653
+ ]
654
+ }]
655
+ };
656
+ }
657
+ };
658
+
659
+ // EzDataView - Data view (cards/grid/list modes)
660
+ ssrRegistry['EzDataView'] = {
661
+ template(props) {
662
+ return {
663
+ eztype: 'div',
664
+ cls: 'ez-dataview',
665
+ style: props.style,
666
+ items: props.items || [{ eztype: 'div', text: 'Loading...', cls: 'ez-dataview-empty' }]
667
+ };
668
+ }
669
+ };
670
+
671
+ // EzTree - Tree view
672
+ ssrRegistry['EzTree'] = {
673
+ template(props) {
674
+ function renderNode(node, level = 0) {
675
+ const hasChildren = node.children && node.children.length > 0;
676
+ return {
677
+ eztype: 'div',
678
+ cls: 'ez-tree-node',
679
+ style: { paddingLeft: `${level * 20}px` },
680
+ items: [
681
+ {
682
+ eztype: 'div',
683
+ cls: 'ez-tree-node-content',
684
+ layout: 'hbox',
685
+ gap: 1,
686
+ style: { padding: '4px 8px', cursor: 'pointer' },
687
+ items: [
688
+ hasChildren ? { eztype: 'EzIcon', fa: 'chevron-right', size: 'sm' } : { eztype: 'span', style: { width: '14px' } },
689
+ node.icon ? { eztype: 'EzIcon', fa: node.icon } : null,
690
+ { eztype: 'span', text: node.text || node.label || node.name || '' }
691
+ ].filter(Boolean)
692
+ },
693
+ ...(hasChildren ? node.children.map(child => renderNode(child, level + 1)) : [])
694
+ ]
695
+ };
696
+ }
697
+
698
+ const data = props.data || [];
699
+ return {
700
+ eztype: 'div',
701
+ cls: 'ez-tree',
702
+ style: props.style,
703
+ items: data.map(node => renderNode(node))
704
+ };
705
+ }
706
+ };
707
+
708
+ // EzKanban - Kanban board
709
+ ssrRegistry['EzKanban'] = {
710
+ template(props) {
711
+ const columns = props.columns || [];
712
+ return {
713
+ eztype: 'div',
714
+ cls: 'ez-kanban',
715
+ layout: 'hbox',
716
+ gap: 2,
717
+ style: { overflow: 'auto', ...props.style },
718
+ items: columns.map(col => ({
719
+ eztype: 'div',
720
+ cls: 'ez-kanban-column',
721
+ style: {
722
+ minWidth: '280px', background: 'var(--ez-surface)',
723
+ borderRadius: '8px', padding: '12px'
724
+ },
725
+ items: [
726
+ { eztype: 'h4', text: col.title || col.name, style: { margin: '0 0 12px' } },
727
+ {
728
+ eztype: 'div',
729
+ cls: 'ez-kanban-cards',
730
+ layout: 'vbox',
731
+ gap: 1,
732
+ items: (col.cards || col.items || []).map(card => ({
733
+ eztype: 'div',
734
+ cls: 'ez-kanban-card',
735
+ style: {
736
+ background: 'var(--ez-background)',
737
+ border: '1px solid var(--ez-border)',
738
+ borderRadius: '4px', padding: '12px'
739
+ },
740
+ text: card.title || card.text || ''
741
+ }))
742
+ }
743
+ ]
744
+ }))
745
+ };
746
+ }
747
+ };
748
+
749
+ // EzActivityFeed - Activity feed
750
+ ssrRegistry['EzActivityFeed'] = {
751
+ template(props) {
752
+ const items = (props.items || []).map(item => ({
753
+ eztype: 'div',
754
+ cls: 'ez-activity-item',
755
+ layout: 'hbox',
756
+ gap: 2,
757
+ style: { padding: '12px 0', borderBottom: '1px solid var(--ez-border)' },
758
+ items: [
759
+ item.avatar ? { eztype: 'EzAvatar', ...item.avatar, size: 32 } : null,
760
+ {
761
+ eztype: 'div',
762
+ flex: 1,
763
+ items: [
764
+ { eztype: 'div', text: item.title || item.text },
765
+ item.time ? { eztype: 'div', text: item.time, style: { fontSize: '12px', color: 'var(--ez-muted)' } } : null
766
+ ].filter(Boolean)
767
+ }
768
+ ].filter(Boolean)
769
+ }));
770
+
771
+ return {
772
+ eztype: 'div',
773
+ cls: 'ez-activity-feed',
774
+ style: props.style,
775
+ items
776
+ };
777
+ }
778
+ };
779
+
780
+ // ==================== OVERLAYS ====================
781
+
782
+ // EzDialog - Dialog/Modal (renders hidden in SSR)
783
+ ssrRegistry['EzDialog'] = {
784
+ template(props) {
785
+ // Don't render dialog content in SSR - it's an overlay
786
+ return { eztype: 'div', cls: 'ez-dialog-placeholder', style: { display: 'none' } };
787
+ }
788
+ };
789
+
790
+ // EzDrawer - Side drawer (renders hidden in SSR)
791
+ ssrRegistry['EzDrawer'] = {
792
+ template(props) {
793
+ return { eztype: 'div', cls: 'ez-drawer-placeholder', style: { display: 'none' } };
794
+ }
795
+ };
796
+
797
+ // EzDropdown - Dropdown menu
798
+ ssrRegistry['EzDropdown'] = {
799
+ template(props) {
800
+ // Just render the trigger, dropdown opens on interaction
801
+ return {
802
+ eztype: 'div',
803
+ cls: 'ez-dropdown',
804
+ style: { position: 'relative', display: 'inline-block', ...props.style },
805
+ items: props.trigger ? [props.trigger] : [{ eztype: 'EzButton', text: props.text || 'Dropdown' }]
806
+ };
807
+ }
808
+ };
809
+
810
+ // EzPicker - Item picker
811
+ ssrRegistry['EzPicker'] = {
812
+ template(props) {
813
+ return {
814
+ eztype: 'div',
815
+ cls: 'ez-picker',
816
+ style: {
817
+ border: '1px solid var(--ez-border)',
818
+ borderRadius: '4px', padding: '8px',
819
+ ...props.style
820
+ },
821
+ text: props.placeholder || 'Select...'
822
+ };
823
+ }
824
+ };
825
+
826
+ // EzSearchFilter - Search with filters
827
+ ssrRegistry['EzSearchFilter'] = {
828
+ template(props) {
829
+ return {
830
+ eztype: 'div',
831
+ cls: 'ez-search-filter',
832
+ layout: 'hbox',
833
+ gap: 1,
834
+ style: props.style,
835
+ items: [
836
+ {
837
+ eztype: 'EzInput',
838
+ placeholder: props.placeholder || 'Search...',
839
+ flex: 1
840
+ }
841
+ ]
842
+ };
843
+ }
844
+ };
845
+
846
+ // ==================== CHARTS ====================
847
+
848
+ // EzChart - Chart container (placeholder in SSR)
849
+ ssrRegistry['EzChart'] = {
850
+ template(props) {
851
+ return {
852
+ eztype: 'div',
853
+ cls: 'ez-chart',
854
+ style: {
855
+ height: props.height || '300px',
856
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
857
+ background: 'var(--ez-surface)', borderRadius: '8px',
858
+ ...props.style
859
+ },
860
+ text: 'Chart loading...'
861
+ };
862
+ }
863
+ };
864
+
865
+ ssrRegistry['EzLineChart'] = ssrRegistry['EzChart'];
866
+ ssrRegistry['EzBarChart'] = ssrRegistry['EzChart'];
867
+ ssrRegistry['EzDoughnutChart'] = ssrRegistry['EzChart'];
868
+
869
+ // ==================== LAYOUT ====================
870
+
871
+ // EzLayout - Layout container
872
+ ssrRegistry['EzLayout'] = {
873
+ template(props) {
874
+ return {
875
+ eztype: 'div',
876
+ cls: 'ez-layout',
877
+ layout: props.layout || 'vbox',
878
+ flex: props.flex,
879
+ gap: props.gap,
880
+ style: props.style,
881
+ items: props.items || []
882
+ };
883
+ }
884
+ };
885
+
886
+ // EzMask - Loading mask (renders hidden in SSR)
887
+ ssrRegistry['EzMask'] = {
888
+ template(props) {
889
+ return { eztype: 'div', cls: 'ez-mask-placeholder', style: { display: 'none' } };
890
+ }
891
+ };
892
+
893
+ // EzOrgChart - Org chart
894
+ ssrRegistry['EzOrgChart'] = {
895
+ template(props) {
896
+ return {
897
+ eztype: 'div',
898
+ cls: 'ez-orgchart',
899
+ style: {
900
+ padding: '20px',
901
+ background: 'var(--ez-surface)',
902
+ borderRadius: '8px',
903
+ ...props.style
904
+ },
905
+ text: 'Organization Chart'
906
+ };
907
+ }
908
+ };
909
+
910
+ // EzOutlet - Router outlet
911
+ ssrRegistry['EzOutlet'] = {
912
+ template(props) {
913
+ return {
914
+ eztype: 'div',
915
+ cls: 'ez-outlet',
916
+ style: props.style,
917
+ items: props.items || []
918
+ };
919
+ }
920
+ };
921
+
922
+ // EzToast - Toast notifications (handled by service, not rendered)
923
+ ssrRegistry['EzToast'] = {
924
+ template(props) {
925
+ return { eztype: 'div', cls: 'ez-toast-placeholder', style: { display: 'none' } };
926
+ }
927
+ };
928
+
929
+ console.log('[SSR Shim] Framework components registered:', Object.keys(ssrRegistry).filter(k => k.startsWith('Ez')).join(', '));
85
930
  }
86
931
 
87
932
  const ezSSR = {
@@ -99,17 +944,9 @@ const ezSSR = {
99
944
  },
100
945
 
101
946
  // Mock getControllerSync to return controller state for SSR
102
- // Handles both "MyController" and "My" (framework adds "Controller" suffix)
103
947
  getControllerSync(name) {
104
- // Try exact match first
105
948
  let ctrl = ssrControllers[name];
106
-
107
- // Try with Controller suffix
108
- if (!ctrl) {
109
- ctrl = ssrControllers[name + 'Controller'];
110
- }
111
-
112
- // Try removing Controller suffix
949
+ if (!ctrl) ctrl = ssrControllers[name + 'Controller'];
113
950
  if (!ctrl && name.endsWith('Controller')) {
114
951
  ctrl = ssrControllers[name.replace('Controller', '')];
115
952
  }
@@ -118,12 +955,9 @@ const ezSSR = {
118
955
  return { state: ctrl.state || {} };
119
956
  }
120
957
 
121
- console.warn(`[SSR Shim] Controller not found: ${name}`);
122
- console.warn(`[SSR Shim] Available controllers: ${Object.keys(ssrControllers).join(', ')}`);
123
958
  return { state: {} };
124
959
  },
125
960
 
126
- // Mock getController (async version)
127
961
  getController(name) {
128
962
  return this.getControllerSync(name);
129
963
  },
@@ -132,26 +966,23 @@ const ezSSR = {
132
966
  return ssrRegistry[name];
133
967
  },
134
968
 
135
- // Clear registry (useful between renders) - preserves framework components
136
969
  _clear() {
137
970
  Object.keys(ssrRegistry).forEach(k => delete ssrRegistry[k]);
138
971
  Object.keys(ssrControllers).forEach(k => delete ssrControllers[k]);
139
- // Re-register framework components after clear
140
972
  registerFrameworkComponents();
141
973
  }
142
974
  };
143
975
 
144
- // Make available globally for SSR context
976
+ // Make available globally
145
977
  globalThis.ez = ezSSR;
146
978
  if (typeof global !== 'undefined') {
147
979
  global.ez = ezSSR;
148
980
  }
149
981
 
150
- // Register framework components on init
982
+ // Register all components
151
983
  registerFrameworkComponents();
152
984
 
153
- console.log('[SSR Shim] Initialized - ez is now available globally');
154
- console.log('[SSR Shim] Framework components registered: EzIcon, EzLabel, EzButton');
985
+ console.log('[SSR Shim] Initialized with all framework components');
155
986
 
156
987
  export { ezSSR as ez };
157
988
  export default ezSSR;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezfw-core",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "Ez Framework - A declarative component framework for building modern web applications",
5
5
  "type": "module",
6
6
  "main": "./core/ez.ts",