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.
- package/components/EzIcon.ts +83 -42
- package/components/EzOutlet.ts +4 -8
- package/components/HtmlWrapper.ts +6 -1
- package/components/alert/EzAlert.test.ts +6 -5
- package/components/alert/EzAlert.ts +9 -8
- package/components/avatar/EzAvatar.ts +61 -53
- package/components/badge/EzBadge.ts +2 -1
- package/components/base/EzBaseComponentBindings.ts +3 -0
- package/components/base/EzBaseComponentCore.ts +4 -1
- package/components/base/EzBaseComponentTooltip.ts +35 -11
- package/components/base/EzBaseComponentTypes.ts +3 -1
- package/components/base/EzBindingContent.ts +2 -2
- package/components/base/EzBindingContext.ts +2 -1
- package/components/base/EzBindingDisplay.ts +2 -2
- package/components/base/EzBindingIcon.ts +98 -0
- package/components/button/EzButton.d.ts +5 -1
- package/components/button/EzButton.module.scss +10 -7
- package/components/button/EzButton.test.ts +8 -7
- package/components/button/EzButton.ts +42 -5
- package/components/calendar/EzCalendar.d.ts +47 -0
- package/components/calendar/EzCalendar.mobile.d.ts +23 -0
- package/components/calendar/EzCalendar.mobile.module.scss +301 -0
- package/components/calendar/EzCalendar.mobile.ts +476 -0
- package/components/calendar/EzCalendar.module.scss +544 -0
- package/components/calendar/EzCalendar.ts +854 -0
- package/components/card/EzCard.module.scss +2 -0
- package/components/checkbox/EzCheckbox.d.ts +13 -1
- package/components/checkbox/EzCheckbox.ts +2 -1
- package/components/colorpicker/EzColorPicker.d.ts +31 -0
- package/components/colorpicker/EzColorPicker.module.scss +311 -0
- package/components/colorpicker/EzColorPicker.ts +808 -0
- package/components/combobox/EzCombobox.ts +11 -8
- package/components/dataview/EzDataView.ts +48 -7
- package/components/dataview/footer/EzDataViewFooter.ts +2 -0
- package/components/dataview/modes/EzDataViewCards.ts +13 -3
- package/components/datepicker/EzCalendarPanel.ts +389 -0
- package/components/datepicker/EzDatePicker.d.ts +2 -1
- package/components/datepicker/EzDatePicker.module.scss +191 -0
- package/components/datepicker/EzDatePicker.ts +62 -64
- package/components/dialog/EzDialog.ts +3 -2
- package/components/dropdown/EzDropdown.module.scss +3 -3
- package/components/dropdown/EzDropdown.ts +2 -1
- package/components/feed/EzActivityFeed.module.scss +13 -38
- package/components/feed/EzActivityFeed.ts +3 -2
- package/components/fileupload/EzFileUpload.module.scss +20 -20
- package/components/fileupload/EzFileUpload.test.ts +11 -4
- package/components/fileupload/EzFileUpload.ts +20 -22
- package/components/floatingpanel/EzFloatingPanel.ts +22 -0
- package/components/grid/EzGrid.scss +4 -4
- package/components/grid/EzGrid.ts +9 -4
- package/components/grid/body/EzGridBody.ts +17 -6
- package/components/grid/footer/EzGridFooter.ts +4 -3
- package/components/grid/header/EzGridHeader.ts +8 -9
- package/components/grid/state/EzGridController.ts +8 -1
- package/components/grid/state/EzGridLifecycle.ts +1 -0
- package/components/grid/state/EzGridParts.ts +9 -4
- package/components/icon/icons.ts +37 -0
- package/components/input/EzInput.d.ts +3 -1
- package/components/input/EzInput.module.scss +23 -0
- package/components/input/EzInput.test.ts +2 -2
- package/components/input/EzInput.ts +33 -10
- package/components/kanban/EzKanban.ts +2 -1
- package/components/kanban/board/EzKanbanBoard.ts +2 -1
- package/components/kanban/card/EzKanbanCard.ts +5 -4
- package/components/kanban/column/EzKanbanColumn.ts +4 -3
- package/components/orgchart/EzOrgChart.ts +7 -6
- package/components/pagination/EzPagination.module.scss +42 -8
- package/components/pagination/EzPagination.test.ts +69 -93
- package/components/pagination/EzPagination.ts +23 -16
- package/components/panel/EzPanel.ts +2 -1
- package/components/popover/EzPopover.ts +2 -1
- package/components/radio/EzRadio.d.ts +13 -1
- package/components/radio/EzRadio.module.scss +3 -3
- package/components/searchfilter/EzSearchFilter.ts +3 -2
- package/components/select/EzSelect.d.ts +10 -0
- package/components/select/EzSelect.ts +1 -1
- package/components/slider/EzSlider.d.ts +35 -0
- package/components/slider/EzSlider.module.scss +4 -4
- package/components/slider/EzSlider.ts +1 -1
- package/components/stepper/EzStepper.test.ts +10 -6
- package/components/stepper/EzStepper.ts +8 -22
- package/components/switch/EzSwitch.d.ts +13 -1
- package/components/switch/EzSwitch.module.scss +1 -1
- package/components/tabs/EzTab.d.ts +24 -0
- package/components/tabs/EzTab.ts +66 -0
- package/components/tabs/EzTabPanel.ts +32 -2
- package/components/textarea/EzTextarea.d.ts +1 -1
- package/components/timepicker/EzTimePicker.d.ts +2 -1
- package/components/timepicker/EzTimePicker.ts +44 -3
- package/components/tree/EzTree.ts +11 -9
- package/core/EzComponentTypes.ts +26 -0
- package/core/EzGlobal.ts +32 -1
- package/core/EzModel.test.ts +3 -2
- package/core/EzTypes.ts +14 -7
- package/core/eventBus.test.ts +4 -3
- package/core/ez.test.ts +22 -39
- package/core/ez.ts +24 -10
- package/core/ezEntryPlugin.js +276 -0
- package/core/loader.ts +43 -20
- package/core/public-api.ts +4 -4
- package/core/renderer/RendererCore.ts +1072 -1019
- package/core/renderer/RendererSkeleton.ts +1 -1
- package/core/renderer.test.ts +12 -12
- package/core/router/RouterTypes.ts +5 -0
- package/core/router.test.ts +2 -1
- package/core/router.ts +74 -25
- package/core/services.ts +6 -25
- package/core/state.test.ts +3 -3
- package/core/styles.ts +20 -0
- package/islands/StaticHtmlRenderer.js +4 -2
- package/islands/StaticHtmlRenderer.ts +6 -4
- package/islands/ViteIslandsPlugin.js +13 -6
- package/islands/ViteIslandsPlugin.ts +11 -0
- package/islands/metaUtils.ts +0 -3
- package/islands/ssrRenderer.ts +4 -6
- package/islands/ssrShim.js +31 -4
- package/modules.ts +10 -0
- package/package.json +1 -1
- package/services/firebase.js +54 -0
- package/themes/ez-theme-slate.scss +21 -3
- package/themes/ez-theme.scss +11 -0
package/components/EzIcon.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EzBaseComponent, EzBaseComponentConfig } from './EzBaseComponent.js';
|
|
2
|
+
import { getIcon } from './icon/icons.js';
|
|
2
3
|
|
|
3
|
-
type
|
|
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: '
|
|
20
|
-
xs: '
|
|
21
|
-
sm: '
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
39
|
+
/** Icon style: 'outline' (default) or 'filled' */
|
|
40
|
+
variant?: IconStyle;
|
|
41
|
+
|
|
42
|
+
/** @deprecated Use 'variant' instead */
|
|
43
|
+
type?: string;
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
/** Icon size */
|
|
39
46
|
size?: IconSize | string;
|
|
47
|
+
|
|
48
|
+
/** Icon color (semantic or CSS value) */
|
|
40
49
|
color?: string;
|
|
41
50
|
|
|
42
|
-
|
|
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('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
84
|
-
if (this.config.
|
|
85
|
-
|
|
86
|
-
|
|
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);
|
package/components/EzOutlet.ts
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
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", "
|
|
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
|
-
|
|
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: '
|
|
93
|
+
icon: 'bell' // Tabler icon name
|
|
94
94
|
});
|
|
95
95
|
const el = await alert.render();
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
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: '
|
|
14
|
-
success: '
|
|
15
|
-
warning: '
|
|
16
|
-
error: '
|
|
17
|
-
danger: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
101
|
+
private _applyTextBinding(el: HTMLElement): void {
|
|
102
|
+
const bind = this.config.bind;
|
|
103
|
+
if (!bind) return;
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
let
|
|
110
|
-
let props: string[];
|
|
105
|
+
// Get text binding - supports both string and object format
|
|
106
|
+
let textBind: string | (() => string) | undefined;
|
|
111
107
|
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 (!
|
|
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
|
-
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
initialsSpan.textContent = this._calcInitials(text);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this._effects.push(stop);
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
private _calcInitials(
|
|
144
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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 -
|
|
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 +
|
|
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 -
|
|
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 +
|
|
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 -
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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[];
|