ezfw-core 1.0.93 → 1.0.95

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.
Files changed (121) hide show
  1. package/components/EzIcon.ts +83 -42
  2. package/components/EzOutlet.ts +4 -8
  3. package/components/HtmlWrapper.ts +6 -1
  4. package/components/alert/EzAlert.test.ts +6 -5
  5. package/components/alert/EzAlert.ts +9 -8
  6. package/components/avatar/EzAvatar.ts +61 -53
  7. package/components/badge/EzBadge.ts +2 -1
  8. package/components/base/EzBaseComponentBindings.ts +3 -0
  9. package/components/base/EzBaseComponentCore.ts +4 -1
  10. package/components/base/EzBaseComponentTooltip.ts +35 -11
  11. package/components/base/EzBaseComponentTypes.ts +3 -1
  12. package/components/base/EzBindingContent.ts +2 -2
  13. package/components/base/EzBindingContext.ts +2 -1
  14. package/components/base/EzBindingDisplay.ts +2 -2
  15. package/components/base/EzBindingIcon.ts +98 -0
  16. package/components/button/EzButton.d.ts +5 -1
  17. package/components/button/EzButton.module.scss +10 -7
  18. package/components/button/EzButton.test.ts +8 -7
  19. package/components/button/EzButton.ts +42 -5
  20. package/components/calendar/EzCalendar.d.ts +47 -0
  21. package/components/calendar/EzCalendar.mobile.d.ts +23 -0
  22. package/components/calendar/EzCalendar.mobile.module.scss +301 -0
  23. package/components/calendar/EzCalendar.mobile.ts +476 -0
  24. package/components/calendar/EzCalendar.module.scss +544 -0
  25. package/components/calendar/EzCalendar.ts +854 -0
  26. package/components/card/EzCard.module.scss +2 -0
  27. package/components/checkbox/EzCheckbox.d.ts +13 -1
  28. package/components/checkbox/EzCheckbox.ts +2 -1
  29. package/components/colorpicker/EzColorPicker.d.ts +31 -0
  30. package/components/colorpicker/EzColorPicker.module.scss +311 -0
  31. package/components/colorpicker/EzColorPicker.ts +808 -0
  32. package/components/combobox/EzCombobox.ts +11 -8
  33. package/components/dataview/EzDataView.ts +48 -7
  34. package/components/dataview/footer/EzDataViewFooter.ts +2 -0
  35. package/components/dataview/modes/EzDataViewCards.ts +13 -3
  36. package/components/datepicker/EzCalendarPanel.ts +389 -0
  37. package/components/datepicker/EzDatePicker.d.ts +2 -1
  38. package/components/datepicker/EzDatePicker.module.scss +191 -0
  39. package/components/datepicker/EzDatePicker.ts +62 -64
  40. package/components/dialog/EzDialog.ts +3 -2
  41. package/components/dropdown/EzDropdown.module.scss +3 -3
  42. package/components/dropdown/EzDropdown.ts +2 -1
  43. package/components/feed/EzActivityFeed.module.scss +13 -38
  44. package/components/feed/EzActivityFeed.ts +3 -2
  45. package/components/fileupload/EzFileUpload.module.scss +20 -20
  46. package/components/fileupload/EzFileUpload.test.ts +11 -4
  47. package/components/fileupload/EzFileUpload.ts +20 -22
  48. package/components/floatingpanel/EzFloatingPanel.ts +22 -0
  49. package/components/grid/EzGrid.scss +4 -4
  50. package/components/grid/EzGrid.ts +9 -4
  51. package/components/grid/body/EzGridBody.ts +17 -6
  52. package/components/grid/footer/EzGridFooter.ts +4 -3
  53. package/components/grid/header/EzGridHeader.ts +8 -9
  54. package/components/grid/state/EzGridController.ts +8 -1
  55. package/components/grid/state/EzGridLifecycle.ts +1 -0
  56. package/components/grid/state/EzGridParts.ts +9 -4
  57. package/components/icon/icons.ts +37 -0
  58. package/components/input/EzInput.d.ts +3 -1
  59. package/components/input/EzInput.module.scss +23 -0
  60. package/components/input/EzInput.test.ts +2 -2
  61. package/components/input/EzInput.ts +33 -10
  62. package/components/kanban/EzKanban.ts +2 -1
  63. package/components/kanban/board/EzKanbanBoard.ts +2 -1
  64. package/components/kanban/card/EzKanbanCard.ts +5 -4
  65. package/components/kanban/column/EzKanbanColumn.ts +4 -3
  66. package/components/orgchart/EzOrgChart.ts +7 -6
  67. package/components/pagination/EzPagination.module.scss +42 -8
  68. package/components/pagination/EzPagination.test.ts +69 -93
  69. package/components/pagination/EzPagination.ts +23 -16
  70. package/components/panel/EzPanel.ts +2 -1
  71. package/components/popover/EzPopover.ts +2 -1
  72. package/components/radio/EzRadio.d.ts +13 -1
  73. package/components/radio/EzRadio.module.scss +3 -3
  74. package/components/searchfilter/EzSearchFilter.ts +3 -2
  75. package/components/select/EzSelect.d.ts +10 -0
  76. package/components/select/EzSelect.ts +1 -1
  77. package/components/slider/EzSlider.d.ts +35 -0
  78. package/components/slider/EzSlider.module.scss +4 -4
  79. package/components/slider/EzSlider.ts +1 -1
  80. package/components/stepper/EzStepper.test.ts +10 -6
  81. package/components/stepper/EzStepper.ts +8 -22
  82. package/components/switch/EzSwitch.d.ts +13 -1
  83. package/components/switch/EzSwitch.module.scss +1 -1
  84. package/components/tabs/EzTab.d.ts +24 -0
  85. package/components/tabs/EzTab.ts +66 -0
  86. package/components/tabs/EzTabPanel.ts +32 -2
  87. package/components/textarea/EzTextarea.d.ts +1 -1
  88. package/components/timepicker/EzTimePicker.d.ts +2 -1
  89. package/components/timepicker/EzTimePicker.ts +44 -3
  90. package/components/tree/EzTree.ts +11 -9
  91. package/core/EzComponentTypes.ts +26 -0
  92. package/core/EzGlobal.ts +32 -1
  93. package/core/EzModel.test.ts +3 -2
  94. package/core/EzTypes.ts +14 -7
  95. package/core/eventBus.test.ts +4 -3
  96. package/core/ez.test.ts +22 -39
  97. package/core/ez.ts +24 -10
  98. package/core/ezEntryPlugin.js +276 -0
  99. package/core/loader.ts +43 -20
  100. package/core/public-api.ts +4 -4
  101. package/core/renderer/RendererCore.ts +1072 -1019
  102. package/core/renderer/RendererSkeleton.ts +1 -1
  103. package/core/renderer.test.ts +12 -12
  104. package/core/router/RouterTypes.ts +5 -0
  105. package/core/router.test.ts +2 -1
  106. package/core/router.ts +74 -25
  107. package/core/services.ts +6 -25
  108. package/core/state.test.ts +3 -3
  109. package/core/styles.ts +20 -0
  110. package/islands/StaticHtmlRenderer.js +4 -2
  111. package/islands/StaticHtmlRenderer.ts +6 -4
  112. package/islands/ViteIslandsPlugin.js +13 -6
  113. package/islands/ViteIslandsPlugin.ts +11 -0
  114. package/islands/metaUtils.ts +0 -3
  115. package/islands/ssrRenderer.ts +4 -6
  116. package/islands/ssrShim.js +31 -4
  117. package/modules.ts +10 -0
  118. package/package.json +1 -1
  119. package/services/firebase.js +54 -0
  120. package/themes/ez-theme-slate.scss +21 -3
  121. package/themes/ez-theme.scss +11 -0
@@ -1,7 +1,8 @@
1
1
  import { EzBaseComponent, EzBaseComponentConfig } from './EzBaseComponent.js';
2
+ import { getIcon } from './icon/icons.js';
2
3
 
3
- type IconType = 'solid' | 'regular' | 'brands' | 'light' | 'thin' | 'duotone';
4
- type IconSize = 'xxs' | 'xs' | 'sm' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x';
4
+ type IconStyle = 'outline' | 'filled';
5
+ type IconSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x';
5
6
 
6
7
  const SEMANTIC_COLORS: Record<string, string> = {
7
8
  primary: 'var(--ez-primary)',
@@ -16,32 +17,47 @@ const SEMANTIC_COLORS: Record<string, string> = {
16
17
  };
17
18
 
18
19
  const SIZE_MAP: Record<string, string> = {
19
- xxs: '0.65em',
20
- xs: '0.75em',
21
- sm: '0.875em',
22
- lg: '1.25em',
23
- xl: '1.5em',
24
- '2x': '2em',
25
- '3x': '3em',
26
- '4x': '4em',
27
- '5x': '5em'
20
+ xxs: '12px',
21
+ xs: '14px',
22
+ sm: '16px',
23
+ md: '20px',
24
+ lg: '24px',
25
+ xl: '28px',
26
+ '2x': '32px',
27
+ '3x': '40px',
28
+ '4x': '48px',
29
+ '5x': '56px'
28
30
  };
29
31
 
30
32
  export interface EzIconConfig extends EzBaseComponentConfig {
31
- // FontAwesome
33
+ /** Tabler icon name (e.g., 'user', 'settings', 'home') */
34
+ icon?: string;
35
+
36
+ /** @deprecated Use 'icon' instead. FontAwesome icon name for backwards compatibility */
32
37
  fa?: string;
33
- type?: IconType;
34
38
 
35
- // Future: Material Icons, etc.
36
- // mat?: string;
39
+ /** Icon style: 'outline' (default) or 'filled' */
40
+ variant?: IconStyle;
41
+
42
+ /** @deprecated Use 'variant' instead */
43
+ type?: string;
37
44
 
38
- // Styling
45
+ /** Icon size */
39
46
  size?: IconSize | string;
47
+
48
+ /** Icon color (semantic or CSS value) */
40
49
  color?: string;
41
50
 
42
- // Animations
51
+ /** Stroke width (1-3, default 2) */
52
+ stroke?: number;
53
+
54
+ /** Spin animation */
43
55
  spin?: boolean;
56
+
57
+ /** Pulse animation */
44
58
  pulse?: boolean;
59
+
60
+ /** @deprecated No longer supported */
45
61
  bounce?: boolean;
46
62
  }
47
63
 
@@ -54,36 +70,61 @@ export class EzIcon extends EzBaseComponent {
54
70
  }
55
71
 
56
72
  async render(): Promise<HTMLElement> {
57
- const el = document.createElement('i');
58
- const classes: string[] = [];
59
-
60
- // FontAwesome icon
61
- if (this.config.fa) {
62
- const type = this.config.type || 'solid';
63
- classes.push(`fa-${type}`);
64
- classes.push(`fa-${this.config.fa}`);
73
+ const el = document.createElement('span');
74
+ el.classList.add('ez-icon');
75
+
76
+ // Store size config for dynamic icon binding updates
77
+ const size = this.config.size
78
+ ? (SIZE_MAP[this.config.size] || this.config.size)
79
+ : '1em';
80
+ el.dataset.ezIconSize = size;
81
+ if (this.config.color) {
82
+ el.dataset.ezIconColor = SEMANTIC_COLORS[this.config.color] || this.config.color;
65
83
  }
66
-
67
- // Animations (FA classes)
68
- if (this.config.spin) classes.push('fa-spin');
69
- if (this.config.pulse) classes.push('fa-pulse');
70
- if (this.config.bounce) classes.push('fa-bounce');
71
-
72
- // Apply FA classes
73
- if (classes.length > 0) {
74
- el.classList.add(...classes);
84
+ if (this.config.stroke) {
85
+ el.dataset.ezIconStroke = String(this.config.stroke);
75
86
  }
76
87
 
77
- // Size
78
- if (this.config.size) {
79
- const size = SIZE_MAP[this.config.size] || this.config.size;
80
- el.style.fontSize = size;
88
+ const iconName = this.config.icon || this.config.fa;
89
+
90
+ if (iconName) {
91
+ // Get SVG content (synchronous - icons are pre-imported)
92
+ const svgContent = getIcon(iconName);
93
+ el.innerHTML = svgContent;
94
+
95
+ // Get the SVG element and apply styles
96
+ const svg = el.querySelector('svg');
97
+ if (svg) {
98
+ // Size
99
+ const size = this.config.size
100
+ ? (SIZE_MAP[this.config.size] || this.config.size)
101
+ : '1em';
102
+ svg.style.width = size;
103
+ svg.style.height = size;
104
+
105
+ // Color
106
+ if (this.config.color) {
107
+ const color = SEMANTIC_COLORS[this.config.color] || this.config.color;
108
+ svg.style.color = color;
109
+ }
110
+
111
+ // Stroke width
112
+ if (this.config.stroke) {
113
+ svg.setAttribute('stroke-width', String(this.config.stroke));
114
+ }
115
+
116
+ // Ensure SVG inherits color
117
+ svg.style.display = 'inline-block';
118
+ svg.style.verticalAlign = 'middle';
119
+ }
81
120
  }
82
121
 
83
- // Color
84
- if (this.config.color) {
85
- const color = SEMANTIC_COLORS[this.config.color] || this.config.color;
86
- el.style.color = color;
122
+ // Animations
123
+ if (this.config.spin) {
124
+ el.classList.add('ez-icon-spin');
125
+ }
126
+ if (this.config.pulse) {
127
+ el.classList.add('ez-icon-pulse');
87
128
  }
88
129
 
89
130
  this.applyCls(el);
@@ -82,13 +82,9 @@ export class EzOutlet extends EzBaseComponent {
82
82
  const def = ez.get(view);
83
83
  const paramsChanged = JSON.stringify(this._currentParams) !== JSON.stringify(params);
84
84
 
85
- const isTemplateBased = typeof def?.template === 'function';
86
-
87
- if (isTemplateBased) {
88
- if (!paramsChanged) {
89
- return;
90
- }
91
- } else if (def?.controller) {
85
+ // Prioritize onRouteChange if available - allows controller to handle param changes
86
+ // without re-rendering the entire view (better for SPAs)
87
+ if (def?.controller) {
92
88
  const ctrl = await ez.getController(def.controller) as EzController | null;
93
89
  if (ctrl?.onRouteChange) {
94
90
  this._currentParams = params;
@@ -101,11 +97,11 @@ export class EzOutlet extends EzBaseComponent {
101
97
  }
102
98
  }
103
99
 
100
+ // No onRouteChange handler - only re-render if params changed
104
101
  if (!paramsChanged) {
105
102
  return;
106
103
  }
107
104
  }
108
-
109
105
  if (this._currentInstance) {
110
106
  this._currentInstance.destroy?.();
111
107
  this.el.innerHTML = '';
@@ -188,7 +188,7 @@ export class HtmlWrapper extends EzBaseComponent {
188
188
 
189
189
  // Boolean properties
190
190
  const booleanProps = [
191
- "checked", "disabled", "readonly", "required",
191
+ "checked", "disabled", "required",
192
192
  "autofocus", "multiple", "hidden", "draggable", "spellcheck"
193
193
  ] as const;
194
194
 
@@ -199,6 +199,11 @@ export class HtmlWrapper extends EzBaseComponent {
199
199
  }
200
200
  }
201
201
 
202
+ // readonly needs special handling - DOM property is readOnly (camelCase)
203
+ if (cfg.readonly !== undefined && cfg.readonly !== null) {
204
+ (el as unknown as { readOnly: boolean }).readOnly = !!cfg.readonly;
205
+ }
206
+
202
207
  // Numeric properties
203
208
  const numericProps = [
204
209
  "maxLength", "minLength", "rows", "cols", "tabIndex"
@@ -82,20 +82,21 @@ describe('EzAlert', () => {
82
82
  const alert = new EzAlert({ message: 'Test', variant: 'success' });
83
83
  const el = await alert.render();
84
84
 
85
- const icon = el.querySelector('.alertIcon i');
85
+ // Icons are now SVGs from Tabler, not Font Awesome <i> elements
86
+ const icon = el.querySelector('.alertIcon svg');
86
87
  expect(icon).not.toBeNull();
87
- expect(icon?.classList.contains('fa-circle-check')).toBe(true);
88
88
  });
89
89
 
90
90
  it('should render custom icon when provided', async () => {
91
91
  const alert = new EzAlert({
92
92
  message: 'Test',
93
- icon: 'fa-solid fa-bell'
93
+ icon: 'bell' // Tabler icon name
94
94
  });
95
95
  const el = await alert.render();
96
96
 
97
- const icon = el.querySelector('.alertIcon i');
98
- expect(icon?.classList.contains('fa-bell')).toBe(true);
97
+ // Icons are now SVGs from Tabler
98
+ const icon = el.querySelector('.alertIcon svg');
99
+ expect(icon).not.toBeNull();
99
100
  });
100
101
 
101
102
  it('should not render icon when icon is false', async () => {
@@ -1,6 +1,7 @@
1
1
  import styles from './EzAlert.module.scss';
2
2
  import { cx } from '../../utils/cssModules.js';
3
3
  import { EzBaseComponent, EzBaseComponentConfig } from '../EzBaseComponent.js';
4
+ import { getIcon } from '../icon/icons.js';
4
5
  import type { EzGlobal } from '../../core/EzGlobal.js';
5
6
 
6
7
  const cls = cx(styles);
@@ -10,11 +11,11 @@ declare const ez: EzGlobal;
10
11
  export type AlertVariant = 'info' | 'success' | 'warning' | 'error' | 'danger';
11
12
 
12
13
  const VARIANT_ICONS: Record<AlertVariant, string> = {
13
- info: 'fa-solid fa-circle-info',
14
- success: 'fa-solid fa-circle-check',
15
- warning: 'fa-solid fa-triangle-exclamation',
16
- error: 'fa-solid fa-circle-xmark',
17
- danger: 'fa-solid fa-circle-xmark'
14
+ info: 'info-circle',
15
+ success: 'circle-check',
16
+ warning: 'alert-triangle',
17
+ error: 'circle-x',
18
+ danger: 'circle-x'
18
19
  };
19
20
 
20
21
  export interface EzAlertConfig extends EzBaseComponentConfig {
@@ -44,11 +45,11 @@ export class EzAlert extends EzBaseComponent {
44
45
 
45
46
  // Icon
46
47
  if (cfg.icon !== false) {
47
- const iconClass = typeof cfg.icon === 'string' ? cfg.icon : VARIANT_ICONS[variant];
48
+ const iconName = typeof cfg.icon === 'string' ? cfg.icon : VARIANT_ICONS[variant];
48
49
  items.push({
49
50
  eztype: 'div',
50
51
  cls: cls('alertIcon'),
51
- items: [{ eztype: 'i', cls: iconClass }]
52
+ html: getIcon(iconName)
52
53
  });
53
54
  }
54
55
 
@@ -91,7 +92,7 @@ export class EzAlert extends EzBaseComponent {
91
92
  attrs: {
92
93
  'aria-label': 'Close alert'
93
94
  },
94
- items: [{ eztype: 'i', cls: 'fa-solid fa-xmark' }]
95
+ html: getIcon('x')
95
96
  });
96
97
  }
97
98
 
@@ -24,11 +24,10 @@ export interface EzAvatarConfig extends EzBaseComponentConfig {
24
24
  color?: 'primary' | 'success' | 'warning' | 'danger';
25
25
  src?: string;
26
26
  alt?: string;
27
- name?: string;
27
+ text?: string;
28
28
  initials?: string;
29
29
  status?: 'online' | 'offline' | 'busy' | 'away';
30
30
  onClick?: (e: MouseEvent, component: EzAvatar) => void;
31
- bindName?: string;
32
31
  }
33
32
 
34
33
  export class EzAvatar extends EzBaseComponent {
@@ -48,13 +47,13 @@ export class EzAvatar extends EzBaseComponent {
48
47
  eztype: 'img',
49
48
  cls: cls('avatarImg'),
50
49
  src: this.config.src,
51
- alt: this.config.alt || this.config.name || ''
50
+ alt: this.config.alt || this.config.text || ''
52
51
  });
53
52
  } else {
54
53
  items.push({
55
54
  eztype: 'span',
56
55
  cls: cls('avatarInitials'),
57
- text: this._getInitials()
56
+ text: this._calcInitials(this.config.text || '')
58
57
  });
59
58
  }
60
59
 
@@ -86,7 +85,7 @@ export class EzAvatar extends EzBaseComponent {
86
85
  const initialsEl = await ez._createElement({
87
86
  eztype: 'span',
88
87
  cls: cls('avatarInitials'),
89
- text: this._getInitials()
88
+ text: this._calcInitials(this.config.text || '')
90
89
  });
91
90
  el.insertBefore(initialsEl, el.firstChild);
92
91
  };
@@ -94,56 +93,82 @@ export class EzAvatar extends EzBaseComponent {
94
93
  }
95
94
 
96
95
  this.applyStyles(el);
97
-
98
- // Apply bind.name if configured
99
- this._applyNameBind(el);
96
+ this._applyTextBinding(el);
100
97
 
101
98
  return el;
102
99
  }
103
100
 
104
- private _applyNameBind(el: HTMLElement): void {
105
- const bindName = this.config.bindName;
106
- if (!bindName) return;
101
+ private _applyTextBinding(el: HTMLElement): void {
102
+ const bind = this.config.bind;
103
+ if (!bind) return;
107
104
 
108
- const bindStr = bindName;
109
- let controllerName: string | undefined;
110
- let props: string[];
105
+ // Get text binding - supports both string and object format
106
+ let textBind: string | (() => string) | undefined;
111
107
 
112
- if (bindStr.includes(':')) {
113
- const [ctrl, path] = bindStr.split(':');
114
- controllerName = ctrl;
115
- props = path.split('.');
116
- } else {
117
- controllerName = (this.config.controller as string) ?? undefined;
118
- props = bindStr.split('.');
108
+ if (typeof bind === 'string') {
109
+ // bind: 'Controller:path' -> binds to text
110
+ textBind = bind;
111
+ } else if (typeof bind === 'object' && 'text' in bind) {
112
+ // bind: { text: 'path' } or bind: { text: () => value }
113
+ textBind = (bind as { text: string | (() => string) }).text;
119
114
  }
120
115
 
121
- if (!controllerName) return;
116
+ if (!textBind) return;
122
117
 
123
118
  const initialsSpan = el.querySelector(`.${cls('avatarInitials')}`) as HTMLElement;
124
119
  if (!initialsSpan) return;
125
120
 
126
121
  this._effects ??= [];
127
122
 
128
- const stop = effect(() => {
129
- // Try exact name first, then with "Controller" suffix
130
- const ctrl = ez._controllers[controllerName!]
131
- || ez._controllers[`${controllerName}Controller`];
132
- const state = ctrl?.state;
133
- const name = (props.length === 1
134
- ? state?.[props[0]]
135
- : ez.getDeepValue(state, props)) as string || '';
123
+ if (typeof textBind === 'function') {
124
+ // Function binding: bind: { text: () => value }
125
+ const fn = textBind;
126
+ const stop = effect(() => {
127
+ const text = fn() || '';
128
+ initialsSpan.textContent = this._calcInitials(text);
129
+ });
130
+ this._effects.push(stop);
131
+ } else {
132
+ // String binding: 'Controller:path' or 'path'
133
+ let controllerName: string | undefined;
134
+ let props: string[];
135
+
136
+ if (textBind.includes(':')) {
137
+ const [ctrl, path] = textBind.split(':');
138
+ controllerName = ctrl;
139
+ props = path.split('.');
140
+ } else {
141
+ controllerName = this.config.controller as string | undefined;
142
+ props = textBind.split('.');
143
+ }
144
+
145
+ if (!controllerName) return;
136
146
 
137
- initialsSpan.textContent = this._calcInitials(name);
138
- });
147
+ const stop = effect(() => {
148
+ const ctrl = ez._controllers[controllerName!]
149
+ || ez._controllers[`${controllerName}Controller`];
150
+ const state = ctrl?.state;
151
+ const text = (props.length === 1
152
+ ? state?.[props[0]]
153
+ : ez.getDeepValue(state, props)) as string || '';
139
154
 
140
- this._effects.push(stop);
155
+ initialsSpan.textContent = this._calcInitials(text);
156
+ });
157
+
158
+ this._effects.push(stop);
159
+ }
141
160
  }
142
161
 
143
- private _calcInitials(name: string): string {
144
- if (!name) return '?';
162
+ private _calcInitials(text: string): string {
163
+ if (this.config.initials) {
164
+ return this.config.initials.substring(0, 2).toUpperCase();
165
+ }
166
+
167
+ const trimmed = (text || '').trim();
168
+ if (!trimmed) return '?';
145
169
 
146
- const parts = name.trim().split(/\s+/);
170
+ const parts = trimmed.split(/\s+/).filter(Boolean);
171
+ if (parts.length === 0) return '?';
147
172
 
148
173
  if (parts.length === 1) {
149
174
  return parts[0].substring(0, 2).toUpperCase();
@@ -196,21 +221,4 @@ export class EzAvatar extends EzBaseComponent {
196
221
 
197
222
  return Object.keys(styles).length > 0 ? styles : undefined;
198
223
  }
199
-
200
- private _getInitials(): string {
201
- if (this.config.initials) {
202
- return this.config.initials.substring(0, 2);
203
- }
204
-
205
- const name = this.config.name || '';
206
- if (!name) return '?';
207
-
208
- const parts = name.trim().split(/\s+/);
209
-
210
- if (parts.length === 1) {
211
- return parts[0].substring(0, 2).toUpperCase();
212
- }
213
-
214
- return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
215
- }
216
224
  }
@@ -1,6 +1,7 @@
1
1
  import styles from './EzBadge.module.scss';
2
2
  import { cx } from '../../utils/cssModules.js';
3
3
  import { EzBaseComponent, EzBaseComponentConfig } from '../EzBaseComponent.js';
4
+ import { getIcon } from '../icon/icons.js';
4
5
  import type { EzSize } from '../../core/types.js';
5
6
  import type { EzGlobal } from '../../core/EzGlobal.js';
6
7
 
@@ -72,7 +73,7 @@ export class EzBadge extends EzBaseComponent {
72
73
  eztype: 'button',
73
74
  type: 'button',
74
75
  cls: cls('close'),
75
- items: [{ eztype: 'i', cls: 'fa-solid fa-xmark' }],
76
+ html: getIcon('x'),
76
77
  onClick: (e: MouseEvent) => {
77
78
  e.stopPropagation();
78
79
  if (cfg.onClose) cfg.onClose(e);
@@ -14,3 +14,6 @@ export { applyVisibleBind, applyClsBind, applyStyleBind } from './EzBindingDispl
14
14
 
15
15
  // Re-export data binding (list rendering, reconciliation)
16
16
  export { applyDataBind } from './EzBindingData.js';
17
+
18
+ // Re-export icon binding (for EzButton and similar)
19
+ export { applyIconBind } from './EzBindingIcon.js';
@@ -23,6 +23,7 @@ import {
23
23
  applyStyleBind,
24
24
  applyTextBind,
25
25
  applyHtmlBind,
26
+ applyIconBind,
26
27
  BindingContext
27
28
  } from './EzBaseComponentBindings.js';
28
29
  import { applyTooltip, applyShowOnHover, TooltipState } from './EzBaseComponentTooltip.js';
@@ -140,7 +141,7 @@ export class EzBaseComponent {
140
141
  let ctrl: string | undefined = this.config.controller ?? undefined;
141
142
 
142
143
  // Default text (auto-translated by i18n)
143
- if (this.config.text !== undefined && !bind) {
144
+ if (this.config.text !== undefined && !(bind as { text?: unknown })?.text) {
144
145
  const text = String(this.config.text);
145
146
  const translated = ez.i18n?.translate(text, this.config.i18nParams as Record<string, unknown> | undefined) ?? text;
146
147
  el.textContent = translated;
@@ -156,6 +157,7 @@ export class EzBaseComponent {
156
157
 
157
158
  // Create binding context
158
159
  const ctx: BindingContext = {
160
+ instance: this,
159
161
  config: this.config,
160
162
  effects: this._effects,
161
163
  domListeners: this._domListeners,
@@ -218,6 +220,7 @@ export class EzBaseComponent {
218
220
  applyStyleBind(el, bind, ctrl, ctx);
219
221
  applyTextBind(el, bind, ctrl, ctx);
220
222
  applyHtmlBind(el, bind, ctrl, ctx);
223
+ applyIconBind(el, bind, ctrl, ctx);
221
224
 
222
225
  // Update instance state from context
223
226
  this._children = ctx.children;
@@ -66,45 +66,69 @@ export function applyTooltip(
66
66
  if (!tooltip) return;
67
67
  const rect = el.getBoundingClientRect();
68
68
  const tooltipRect = tooltip.getBoundingClientRect();
69
+ const gap = 8;
70
+ const padding = 8;
69
71
 
70
72
  let top: number;
71
73
  let left: number;
74
+ let finalPosition = position;
72
75
 
76
+ // Calculate initial position
73
77
  switch (position) {
74
78
  case 'top':
75
- top = rect.top - tooltipRect.height - 8;
79
+ top = rect.top - tooltipRect.height - gap;
76
80
  left = rect.left + (rect.width - tooltipRect.width) / 2;
81
+ // Flip to bottom if would overlap or go off screen
82
+ if (top < padding) {
83
+ top = rect.bottom + gap;
84
+ finalPosition = 'bottom';
85
+ }
77
86
  break;
78
87
  case 'bottom':
79
- top = rect.bottom + 8;
88
+ top = rect.bottom + gap;
80
89
  left = rect.left + (rect.width - tooltipRect.width) / 2;
90
+ // Flip to top if would go off screen
91
+ if (top + tooltipRect.height > window.innerHeight - padding) {
92
+ top = rect.top - tooltipRect.height - gap;
93
+ finalPosition = 'top';
94
+ }
81
95
  break;
82
96
  case 'left':
83
97
  top = rect.top + (rect.height - tooltipRect.height) / 2;
84
- left = rect.left - tooltipRect.width - 8;
98
+ left = rect.left - tooltipRect.width - gap;
99
+ // Flip to right if would go off screen
100
+ if (left < padding) {
101
+ left = rect.right + gap;
102
+ finalPosition = 'right';
103
+ }
85
104
  break;
86
105
  case 'right':
87
106
  top = rect.top + (rect.height - tooltipRect.height) / 2;
88
- left = rect.right + 8;
107
+ left = rect.right + gap;
108
+ // Flip to left if would go off screen
109
+ if (left + tooltipRect.width > window.innerWidth - padding) {
110
+ left = rect.left - tooltipRect.width - gap;
111
+ finalPosition = 'left';
112
+ }
89
113
  break;
90
114
  default:
91
- top = rect.top - tooltipRect.height - 8;
115
+ top = rect.top - tooltipRect.height - gap;
92
116
  left = rect.left + (rect.width - tooltipRect.width) / 2;
117
+ if (top < padding) {
118
+ top = rect.bottom + gap;
119
+ finalPosition = 'bottom';
120
+ }
93
121
  }
94
122
 
95
- const padding = 8;
123
+ // Constrain horizontal position
96
124
  if (left < padding) left = padding;
97
125
  if (left + tooltipRect.width > window.innerWidth - padding) {
98
126
  left = window.innerWidth - tooltipRect.width - padding;
99
127
  }
100
- if (top < padding) top = padding;
101
- if (top + tooltipRect.height > window.innerHeight - padding) {
102
- top = window.innerHeight - tooltipRect.height - padding;
103
- }
104
128
 
105
129
  tooltip.style.top = `${top + window.scrollY}px`;
106
130
  tooltip.style.left = `${left + window.scrollX}px`;
107
- tooltip.className = tooltipCls('tooltip', isComponentTooltip ? 'component' : variant, position);
131
+ tooltip.className = tooltipCls('tooltip', isComponentTooltip ? 'component' : variant, finalPosition);
108
132
  };
109
133
 
110
134
  const showTooltip = (): void => {
@@ -38,6 +38,8 @@ export interface BindConfig {
38
38
  allowUnsafeHtml?: boolean;
39
39
  style?: Record<string, string> | (() => Record<string, string>);
40
40
  text?: string | (() => string);
41
+ /** Icon binding for components with setIcon() method (e.g., EzButton) */
42
+ icon?: string | (() => string);
41
43
  [key: string]: unknown;
42
44
  }
43
45
 
@@ -189,7 +191,7 @@ export interface EzComponentConfig {
189
191
  _isRepaint?: boolean;
190
192
 
191
193
  /** @internal */
192
- attrs?: Record<string, unknown>;
194
+ attrs?: Record<string, string | number | boolean | null | undefined>;
193
195
 
194
196
  /** @internal */
195
197
  key?: string | number;
@@ -80,7 +80,7 @@ export function applyTextBind(
80
80
 
81
81
  if (typeof bind.text === 'function') {
82
82
  const stop = effect(() => {
83
- const next = (bind.text as () => string)();
83
+ const next = (bind.text as (ctx: { controller: unknown }) => string)({ controller: ctx.instance?.controller });
84
84
  const text = String(next ?? '');
85
85
  el.textContent = ez.i18n?.translate(text) ?? text;
86
86
  });
@@ -131,7 +131,7 @@ export function applyHtmlBind(
131
131
 
132
132
  if (typeof bind.html === 'function') {
133
133
  const stop = effect(() => {
134
- const next = (bind.html as () => string)();
134
+ const next = (bind.html as (ctx: { controller: unknown }) => string)({ controller: ctx.instance?.controller });
135
135
  const content = String(next ?? '');
136
136
  el.innerHTML = shouldSanitize ? sanitizeHtml(content) : content;
137
137
  });
@@ -10,7 +10,8 @@ import type { EzBaseComponent } from './EzBaseComponentCore.js';
10
10
  * Context object passed to all binding functions.
11
11
  * Contains references to component state and cleanup arrays.
12
12
  */
13
- export interface BindingContext {
13
+ export interface BindingContext {
14
+ instance?: EzBaseComponent;
14
15
  config: EzComponentConfig;
15
16
  effects: EffectCleanup[];
16
17
  domListeners: DomListenerCleanup[];