ezfw-core 1.0.0

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 (154) hide show
  1. package/components/EzBaseComponent.ts +648 -0
  2. package/components/EzComponent.ts +89 -0
  3. package/components/EzInput.module.scss +183 -0
  4. package/components/EzInput.ts +104 -0
  5. package/components/EzLabel.ts +22 -0
  6. package/components/EzOutlet.ts +181 -0
  7. package/components/HtmlWrapper.ts +305 -0
  8. package/components/avatar/EzAvatar.module.scss +200 -0
  9. package/components/avatar/EzAvatar.ts +130 -0
  10. package/components/badge/EzBadge.module.scss +202 -0
  11. package/components/badge/EzBadge.ts +77 -0
  12. package/components/button/EzButton.module.scss +402 -0
  13. package/components/button/EzButton.ts +175 -0
  14. package/components/button/EzButtonGroup.ts +48 -0
  15. package/components/card/EzCard.module.scss +71 -0
  16. package/components/card/EzCard.ts +120 -0
  17. package/components/chart/EzBarChart.ts +47 -0
  18. package/components/chart/EzChart.module.scss +14 -0
  19. package/components/chart/EzChart.ts +279 -0
  20. package/components/chart/EzDoughnutChart.ts +47 -0
  21. package/components/chart/EzLineChart.ts +53 -0
  22. package/components/checkbox/EzCheckbox.module.scss +145 -0
  23. package/components/checkbox/EzCheckbox.ts +115 -0
  24. package/components/dataview/EzDataView.module.scss +115 -0
  25. package/components/dataview/EzDataView.ts +355 -0
  26. package/components/dataview/modes/EzDataViewCards.ts +322 -0
  27. package/components/dataview/modes/EzDataViewGrid.ts +76 -0
  28. package/components/datepicker/EzDatePicker.module.scss +348 -0
  29. package/components/datepicker/EzDatePicker.ts +519 -0
  30. package/components/dialog/EzDialog.module.scss +180 -0
  31. package/components/dropdown/EzDropdown.module.scss +107 -0
  32. package/components/dropdown/EzDropdown.ts +235 -0
  33. package/components/feed/EzActivityFeed.module.scss +90 -0
  34. package/components/feed/EzActivityFeed.ts +78 -0
  35. package/components/form/EzForm.ts +364 -0
  36. package/components/form/EzValidators.test.js +421 -0
  37. package/components/form/EzValidators.ts +202 -0
  38. package/components/grid/EzGrid.scss +88 -0
  39. package/components/grid/EzGrid.ts +1085 -0
  40. package/components/grid/EzGridContainer.ts +104 -0
  41. package/components/grid/body/EzGridBody.scss +283 -0
  42. package/components/grid/body/EzGridBody.ts +549 -0
  43. package/components/grid/body/EzGridCell.ts +211 -0
  44. package/components/grid/body/EzGridRow.ts +196 -0
  45. package/components/grid/filter/EzGridFilters.scss +78 -0
  46. package/components/grid/filter/EzGridFilters.ts +285 -0
  47. package/components/grid/footer/EzGridFooter.scss +136 -0
  48. package/components/grid/footer/EzGridFooter.ts +448 -0
  49. package/components/grid/header/EzGridHeader.scss +199 -0
  50. package/components/grid/header/EzGridHeader.ts +430 -0
  51. package/components/grid/query/EzGridQuery.ts +81 -0
  52. package/components/grid/state/EzGridColumns.ts +155 -0
  53. package/components/grid/state/EzGridController.ts +470 -0
  54. package/components/grid/state/EzGridLifecycle.ts +136 -0
  55. package/components/grid/state/EzGridNormalizers.test.js +273 -0
  56. package/components/grid/state/EzGridNormalizers.ts +162 -0
  57. package/components/grid/state/EzGridParts.ts +233 -0
  58. package/components/grid/state/EzGridPersistence.ts +140 -0
  59. package/components/grid/state/EzGridRemote.test.js +573 -0
  60. package/components/grid/state/EzGridRemote.ts +335 -0
  61. package/components/grid/state/EzGridSelection.ts +231 -0
  62. package/components/grid/state/EzGridSort.ts +286 -0
  63. package/components/grid/title/EzGridActionBar.ts +98 -0
  64. package/components/grid/title/EzGridTitle.ts +114 -0
  65. package/components/grid/title/EzGridTitleBar.scss +65 -0
  66. package/components/grid/title/EzGridTitleBar.ts +87 -0
  67. package/components/grid/types.ts +607 -0
  68. package/components/panel/EzPanel.module.scss +133 -0
  69. package/components/panel/EzPanel.ts +147 -0
  70. package/components/radio/EzRadio.module.scss +190 -0
  71. package/components/radio/EzRadio.ts +149 -0
  72. package/components/select/EzSelect.module.scss +153 -0
  73. package/components/select/EzSelect.ts +238 -0
  74. package/components/skeleton/EzSkeleton.module.scss +95 -0
  75. package/components/skeleton/EzSkeleton.ts +70 -0
  76. package/components/store/EzStore.ts +344 -0
  77. package/components/switch/EzSwitch.module.scss +164 -0
  78. package/components/switch/EzSwitch.ts +117 -0
  79. package/components/tabs/EzTabPanel.module.scss +181 -0
  80. package/components/tabs/EzTabPanel.ts +402 -0
  81. package/components/textarea/EzTextarea.module.scss +131 -0
  82. package/components/textarea/EzTextarea.ts +161 -0
  83. package/components/timepicker/EzTimePicker.module.scss +282 -0
  84. package/components/timepicker/EzTimePicker.ts +540 -0
  85. package/components/toast/EzToast.module.scss +291 -0
  86. package/components/tooltip/EzTooltip.module.scss +124 -0
  87. package/components/tooltip/EzTooltip.ts +153 -0
  88. package/core/EzComponentTypes.ts +693 -0
  89. package/core/EzError.ts +63 -0
  90. package/core/EzModel.ts +268 -0
  91. package/core/EzTypes.ts +328 -0
  92. package/core/eventBus.ts +284 -0
  93. package/core/ez.ts +617 -0
  94. package/core/loader.ts +725 -0
  95. package/core/renderer.ts +1010 -0
  96. package/core/router.ts +490 -0
  97. package/core/services.ts +124 -0
  98. package/core/state.ts +142 -0
  99. package/core/utils.ts +81 -0
  100. package/package.json +51 -0
  101. package/services/RouteUI.js +17 -0
  102. package/services/crypto.js +64 -0
  103. package/services/dialog.js +222 -0
  104. package/services/fetchApi.js +63 -0
  105. package/services/firebase.js +30 -0
  106. package/services/toast.js +214 -0
  107. package/template/doc/EzDocs.js +15 -0
  108. package/template/doc/EzDocs.module.scss +627 -0
  109. package/template/doc/EzDocsController.js +164 -0
  110. package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
  111. package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
  112. package/template/doc/data/badge/EzBadgeDoc.js +92 -0
  113. package/template/doc/data/button/EzButtonDoc.js +77 -0
  114. package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
  115. package/template/doc/data/card/EzCardDoc.js +39 -0
  116. package/template/doc/data/chart/EzChartDoc.js +60 -0
  117. package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
  118. package/template/doc/data/component/EzComponentDoc.js +34 -0
  119. package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
  120. package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
  121. package/template/doc/data/dialog/EzDialogDoc.js +217 -0
  122. package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
  123. package/template/doc/data/form/EzFormDoc.js +90 -0
  124. package/template/doc/data/grid/EzGridDoc.js +99 -0
  125. package/template/doc/data/input/EzInputDoc.js +92 -0
  126. package/template/doc/data/label/EzLabelDoc.js +40 -0
  127. package/template/doc/data/model/EzModelDoc.js +53 -0
  128. package/template/doc/data/outlet/EzOutletDoc.js +63 -0
  129. package/template/doc/data/panel/EzPanelDoc.js +214 -0
  130. package/template/doc/data/radio/EzRadioDoc.js +174 -0
  131. package/template/doc/data/router/EzRouterDoc.js +75 -0
  132. package/template/doc/data/select/EzSelectDoc.js +37 -0
  133. package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
  134. package/template/doc/data/switch/EzSwitchDoc.js +82 -0
  135. package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
  136. package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
  137. package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
  138. package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
  139. package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
  140. package/template/doc/sidebar/EzDocsSidebar.js +32 -0
  141. package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
  142. package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
  143. package/template/doc/viewer/EzDocsViewer.js +18 -0
  144. package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
  145. package/template/doc/viewer/content/EzDocsContent.js +315 -0
  146. package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
  147. package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
  148. package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
  149. package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
  150. package/template/doc/welcome/EzDocsWelcome.js +48 -0
  151. package/themes/ez-theme.scss +179 -0
  152. package/themes/nature-fresh.scss +169 -0
  153. package/types/global.d.ts +21 -0
  154. package/utils/cssModules.js +81 -0
@@ -0,0 +1,648 @@
1
+ import { effect } from '@preact/signals';
2
+ import tooltipStyles from './tooltip/EzTooltip.module.scss';
3
+ import { cx } from '../utils/cssModules.js';
4
+
5
+ const tooltipCls = cx(tooltipStyles);
6
+
7
+ // ==========================================================
8
+ // Type Definitions
9
+ // ==========================================================
10
+
11
+ declare const ez: {
12
+ _controllers: Record<string, EzController | undefined>;
13
+ getController(name: string): EzController | null;
14
+ getDeepValue(obj: unknown, path: string[]): unknown;
15
+ setDeepValue(obj: unknown, path: string[], value: unknown): void;
16
+ _createElement(config: EzComponentConfig): Promise<HTMLElement>;
17
+ };
18
+
19
+ interface EzController {
20
+ state: Record<string, unknown>;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ interface StyleModule {
25
+ [className: string]: string;
26
+ }
27
+
28
+ type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
29
+ type TooltipVariant = 'dark' | 'light';
30
+
31
+ interface TooltipConfig {
32
+ text: string;
33
+ position?: TooltipPosition;
34
+ variant?: TooltipVariant;
35
+ delay?: number;
36
+ }
37
+
38
+ interface BindConfig {
39
+ value?: string;
40
+ data?: string;
41
+ visible?: string;
42
+ cls?: string | (() => string);
43
+ html?: string | (() => string);
44
+ }
45
+
46
+ export interface EzComponentConfig {
47
+ controller?: string | null;
48
+ bind?: string | BindConfig;
49
+ text?: string | number;
50
+ cls?: string | string[];
51
+ style?: Partial<CSSStyleDeclaration>;
52
+ tooltip?: string | TooltipConfig;
53
+ props?: Record<string, unknown>;
54
+ css?: string | unknown;
55
+ _styleModule?: StyleModule;
56
+ onChange?: string | ((value: unknown) => void);
57
+ itemRender?: (item: unknown, index: number, meta: ItemRenderMeta) => EzComponentConfig | null;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ // Alias for component configs
62
+ export type EzBaseComponentConfig = EzComponentConfig;
63
+
64
+ interface ItemRenderMeta {
65
+ isFirst: boolean;
66
+ isLast: boolean;
67
+ isEven: boolean;
68
+ }
69
+
70
+ interface ResolvedBinding {
71
+ controller: EzController;
72
+ path: string[];
73
+ }
74
+
75
+ type EffectCleanup = () => void;
76
+ type DomListenerCleanup = () => void;
77
+
78
+ // ==========================================================
79
+ // EzBaseComponent Class
80
+ // ==========================================================
81
+
82
+ export class EzBaseComponent {
83
+ config: EzComponentConfig;
84
+ props: Record<string, unknown>;
85
+ el: HTMLElement | null = null;
86
+
87
+ protected _effects?: EffectCleanup[];
88
+ protected _domListeners?: DomListenerCleanup[];
89
+ protected _children?: EzBaseComponent[];
90
+ protected _tooltip?: HTMLDivElement;
91
+ protected _tooltipCleanup?: () => void;
92
+
93
+ constructor(config: EzComponentConfig = {}) {
94
+ this.config = config;
95
+ this.props = config.props || {};
96
+ }
97
+
98
+ /**
99
+ * Resolve binding configuration to get controller and path.
100
+ * Supports:
101
+ * - String: "form.username" or "ControllerName:form.username"
102
+ * - Object: { value: "form.username" }
103
+ */
104
+ protected _resolveBinding(): ResolvedBinding | null {
105
+ const bind = this.config.bind;
106
+ if (!bind) return null;
107
+
108
+ const bindStr = typeof bind === 'string' ? bind : bind.value;
109
+ if (!bindStr) return null;
110
+
111
+ let controllerName: string | undefined;
112
+ let pathStr: string;
113
+
114
+ if (bindStr.includes(':')) {
115
+ [controllerName, pathStr] = bindStr.split(':');
116
+ } else {
117
+ controllerName = this.config.controller ?? undefined;
118
+ pathStr = bindStr;
119
+ }
120
+
121
+ if (!controllerName) return null;
122
+
123
+ const controller = ez.getController(controllerName);
124
+ if (!controller?.state) return null;
125
+
126
+ return {
127
+ controller,
128
+ path: pathStr.split('.')
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Create onChange handler from config or binding.
134
+ * Priority: function > string method name > auto from binding
135
+ */
136
+ protected _createOnChangeHandler(): ((value: unknown) => void) | null {
137
+ if (typeof this.config.onChange === 'function') {
138
+ return this.config.onChange;
139
+ }
140
+
141
+ if (typeof this.config.onChange === 'string') {
142
+ const controller = ez.getController(this.config.controller ?? '');
143
+ const method = controller?.[this.config.onChange];
144
+ if (typeof method === 'function') {
145
+ return (method as (value: unknown) => void).bind(controller);
146
+ }
147
+ return null;
148
+ }
149
+
150
+ const binding = this._resolveBinding();
151
+ if (binding) {
152
+ return (value: unknown) => {
153
+ ez.setDeepValue(binding.controller.state, binding.path, value);
154
+ };
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ applyCommonBindings(el: HTMLElement): void {
161
+ this._effects ??= [];
162
+
163
+ const bind = this.config.bind;
164
+ let ctrl: string | undefined = this.config.controller ?? undefined;
165
+
166
+ // 1️⃣ Default text
167
+ if (this.config.text !== undefined && !bind) {
168
+ el.textContent = String(this.config.text);
169
+ }
170
+
171
+ if (!this.config.bind) return;
172
+
173
+ const isInput =
174
+ el.tagName === 'INPUT' ||
175
+ el.tagName === 'TEXTAREA' ||
176
+ el.tagName === 'SELECT';
177
+
178
+ // 2️⃣ String bind: 'AppController:prop'
179
+ if (typeof bind === 'string') {
180
+ let props: string[] | undefined;
181
+ if (bind.includes(':')) {
182
+ const [controller, path] = bind.split(':');
183
+ props = path?.split('.');
184
+
185
+ if (!controller) {
186
+ console.warn(`[ez] Invalid bind format: ${bind}`);
187
+ return;
188
+ }
189
+
190
+ ctrl = controller;
191
+ } else {
192
+ props = bind.split('.');
193
+ }
194
+
195
+ const stop = effect(() => {
196
+ const next =
197
+ props?.length === 1
198
+ ? ez._controllers[ctrl!]?.state[props[0]]
199
+ : ez.getDeepValue(ez._controllers[ctrl!]?.state, props!);
200
+
201
+ const nextStr = (next as string) ?? '';
202
+ if (isInput) {
203
+ if ((el as HTMLInputElement).value !== nextStr) {
204
+ (el as HTMLInputElement).value = nextStr;
205
+ }
206
+ } else {
207
+ if (el.textContent !== nextStr) {
208
+ el.textContent = nextStr;
209
+ }
210
+ }
211
+ });
212
+ this._effects.push(stop);
213
+ }
214
+
215
+ // 3️⃣ Object bind
216
+ if (typeof bind === 'object') {
217
+ this._applyValueBind(el, bind, ctrl, isInput);
218
+ this._applyDataBind(el, bind, ctrl);
219
+ this._applyVisibleBind(el, bind);
220
+ this._applyClsBind(el, bind);
221
+ this._applyHtmlBind(el, bind);
222
+ }
223
+ }
224
+
225
+ private _applyValueBind(
226
+ el: HTMLElement,
227
+ bind: BindConfig,
228
+ ctrl: string | undefined,
229
+ isInput: boolean
230
+ ): void {
231
+ if (!bind.value) return;
232
+
233
+ let props: string[] | undefined;
234
+ let activeCtrl = ctrl;
235
+
236
+ if (bind.value.includes(':')) {
237
+ const [controller, path] = bind.value.split(':');
238
+ props = path?.split('.');
239
+
240
+ if (!controller) {
241
+ console.warn(`[ez] Invalid bind format: ${bind.value}`);
242
+ return;
243
+ }
244
+
245
+ activeCtrl = controller;
246
+ } else {
247
+ props = bind.value.split('.');
248
+ }
249
+
250
+ const stop = effect(() => {
251
+ const next =
252
+ props?.length === 1
253
+ ? ez._controllers[activeCtrl!]?.state[props[0]]
254
+ : ez.getDeepValue(ez._controllers[activeCtrl!]?.state, props!);
255
+
256
+ const nextStr = (next as string) ?? '';
257
+ if (isInput) {
258
+ if ((el as HTMLInputElement).value !== nextStr) {
259
+ (el as HTMLInputElement).value = nextStr;
260
+ }
261
+ } else {
262
+ if (el.textContent !== nextStr) {
263
+ el.textContent = nextStr;
264
+ }
265
+ }
266
+ });
267
+ this._effects!.push(stop);
268
+ }
269
+
270
+ private _applyDataBind(el: HTMLElement, bind: BindConfig, ctrl: string | undefined): void {
271
+ if (!bind.data) return;
272
+
273
+ let props: string[] | undefined;
274
+ let activeCtrl = ctrl;
275
+
276
+ if (bind.data.includes(':')) {
277
+ const [controller, path] = bind.data.split(':');
278
+ props = path?.split('.');
279
+
280
+ if (!controller) {
281
+ console.warn(`[ez] Invalid bind format: ${bind.data}`);
282
+ return;
283
+ }
284
+
285
+ activeCtrl = controller;
286
+ } else {
287
+ props = bind.data.split('.');
288
+ }
289
+
290
+ const stop = effect(() => {
291
+ (async () => {
292
+ const itemFn = this.config.itemRender;
293
+ let data = ez.getDeepValue(ez.getController(activeCtrl!)?.state, props!) as unknown[] | unknown;
294
+
295
+ if (data == null) {
296
+ return;
297
+ }
298
+
299
+ if (!Array.isArray(data)) {
300
+ data = [data];
301
+ }
302
+
303
+ el.innerHTML = '';
304
+
305
+ if (Array.isArray(data)) {
306
+ (el as HTMLElement & { _ezListConfig: unknown })._ezListConfig = { ctrl: activeCtrl, props, itemFn };
307
+ (el as HTMLElement & { _ezListRendered: boolean })._ezListRendered = true;
308
+ el.setAttribute("data-ez-list", "true");
309
+
310
+ el.innerHTML = '';
311
+ for (let i = 0; i < data.length; i++) {
312
+ if (data[i] == null) {
313
+ if (import.meta.env.DEV) {
314
+ console.warn(`[Ez] bind.data: Item at index ${i} is null/undefined. Controller: "${activeCtrl}", path: "${props!.join('.')}". Skipping.`);
315
+ }
316
+ continue;
317
+ }
318
+
319
+ const isLast = i === data.length - 1;
320
+ const isFirst = i === 0;
321
+ const isEven = i % 2 === 0;
322
+
323
+ const childCfg = itemFn?.(data[i], i, { isFirst, isLast, isEven });
324
+
325
+ if (!childCfg) continue;
326
+
327
+ if (!childCfg.controller) {
328
+ childCfg.controller = activeCtrl;
329
+ }
330
+ if (!childCfg.css && this.config.css) {
331
+ childCfg.css = this.config.css;
332
+ childCfg._styleModule = this.config._styleModule;
333
+ }
334
+ const childEl = await ez._createElement(childCfg);
335
+ el.appendChild(childEl);
336
+ }
337
+ }
338
+ })();
339
+ });
340
+ this._effects!.push(stop);
341
+ }
342
+
343
+ private _applyVisibleBind(el: HTMLElement, bind: BindConfig): void {
344
+ if (!bind.visible) return;
345
+
346
+ const expr = bind.visible;
347
+ el.setAttribute('data-ez-visible', expr);
348
+
349
+ interface ParsedVisibility {
350
+ controllerName: string;
351
+ props: string[];
352
+ operator: string;
353
+ rightExpr: string;
354
+ }
355
+
356
+ function parseVisibilityExpression(expr: string): ParsedVisibility | null {
357
+ const operatorMatch = expr.match(/(>=|<=|==|!=|>|<)/);
358
+ if (!operatorMatch) return null;
359
+
360
+ const operator = operatorMatch[0];
361
+ const [leftExpr, rightExpr] = expr.split(operator).map(s => s.trim());
362
+
363
+ const [controllerName, propertyPath] = leftExpr.split(':');
364
+ const props = propertyPath.split('.');
365
+
366
+ return { controllerName, props, operator, rightExpr };
367
+ }
368
+
369
+ const parsed = parseVisibilityExpression(expr);
370
+
371
+ if (parsed) {
372
+ const { controllerName, props, operator, rightExpr } = parsed;
373
+
374
+ const stop = effect(() => {
375
+ const state = ez._controllers[controllerName]?.state;
376
+ const left = ez.getDeepValue(state, props) as number | string;
377
+
378
+ const right = isNaN(Number(rightExpr)) ? rightExpr : Number(rightExpr);
379
+
380
+ let result = true;
381
+ switch (operator) {
382
+ case '>': result = left > right; break;
383
+ case '<': result = left < right; break;
384
+ case '>=': result = left >= right; break;
385
+ case '<=': result = left <= right; break;
386
+ case '!=': result = left != right; break;
387
+ case '==': result = left == right; break;
388
+ }
389
+
390
+ if (result) {
391
+ el.style.removeProperty('display');
392
+ } else {
393
+ el.style.setProperty('display', 'none', 'important');
394
+ }
395
+ });
396
+ this._effects!.push(stop);
397
+ } else if (expr.includes(':')) {
398
+ const [controllerName, path] = expr.split(':');
399
+ const props = path?.split('.');
400
+
401
+ if (!controllerName || !props) {
402
+ console.warn(`[ez] Invalid visible expression format: ${expr}`);
403
+ return;
404
+ }
405
+
406
+ const stop = effect(() => {
407
+ const result = !!ez.getDeepValue(ez._controllers[controllerName]?.state, props);
408
+ el.style.display = result ? '' : 'none';
409
+ });
410
+ this._effects!.push(stop);
411
+
412
+ } else {
413
+ console.warn(`[ez] Unsupported visibility expression: ${expr}`);
414
+ }
415
+ }
416
+
417
+ private _applyClsBind(el: HTMLElement, bind: BindConfig): void {
418
+ if (!bind.cls) return;
419
+
420
+ let prevClasses: string[] = [];
421
+ const styleModule = this.config._styleModule;
422
+
423
+ const stop = effect(() => {
424
+ let next = bind.cls;
425
+
426
+ if (typeof next === 'function') {
427
+ next = next();
428
+ }
429
+
430
+ if (typeof next !== 'string') {
431
+ next = '';
432
+ }
433
+
434
+ const classes = next.split(' ').filter(Boolean).map(c => {
435
+ if (styleModule && styleModule[c]) {
436
+ return styleModule[c];
437
+ }
438
+ return c;
439
+ });
440
+
441
+ if (prevClasses.length) {
442
+ el.classList.remove(...prevClasses);
443
+ }
444
+
445
+ if (classes.length) {
446
+ el.classList.add(...classes);
447
+ }
448
+
449
+ prevClasses = classes;
450
+ });
451
+
452
+ this._effects!.push(stop);
453
+ }
454
+
455
+ private _applyHtmlBind(el: HTMLElement, bind: BindConfig): void {
456
+ if (!bind.html) return;
457
+
458
+ const stop = effect(() => {
459
+ let next = bind.html;
460
+
461
+ if (typeof next === 'function') {
462
+ next = next();
463
+ }
464
+
465
+ if (typeof next !== 'string') {
466
+ next = '';
467
+ }
468
+
469
+ el.innerHTML = next;
470
+ });
471
+
472
+ this._effects!.push(stop);
473
+ }
474
+
475
+ applyStyles(el: HTMLElement): void {
476
+ if (this.config.style && typeof this.config.style === 'object') {
477
+ Object.assign(el.style, this.config.style);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Apply static cls with CSS module resolution
483
+ */
484
+ applyCls(el: HTMLElement): void {
485
+ const cls = this.config.cls;
486
+ if (!cls) return;
487
+
488
+ const styleModule = this.config._styleModule;
489
+
490
+ if (Array.isArray(cls)) {
491
+ const resolvedClasses = cls.map(c => {
492
+ if (styleModule && typeof c === 'string' && styleModule[c]) {
493
+ return styleModule[c];
494
+ }
495
+ return c;
496
+ }).filter(Boolean) as string[];
497
+ if (resolvedClasses.length) {
498
+ el.classList.add(...resolvedClasses);
499
+ }
500
+ } else if (typeof cls === 'string') {
501
+ const classes = cls.split(' ').filter(Boolean).map(c => {
502
+ if (styleModule && styleModule[c]) {
503
+ return styleModule[c];
504
+ }
505
+ return c;
506
+ });
507
+ if (classes.length) {
508
+ el.classList.add(...classes);
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Apply tooltip if configured
515
+ */
516
+ applyTooltip(el: HTMLElement): void {
517
+ const tipConfig = this.config.tooltip;
518
+ if (!tipConfig) return;
519
+
520
+ const text = typeof tipConfig === 'string' ? tipConfig : tipConfig.text;
521
+ const position: TooltipPosition = (typeof tipConfig === 'object' ? tipConfig.position : null) || 'top';
522
+ const variant: TooltipVariant = (typeof tipConfig === 'object' ? tipConfig.variant : null) || 'dark';
523
+ const delay = (typeof tipConfig === 'object' ? tipConfig.delay : null) ?? 200;
524
+
525
+ if (!text) return;
526
+
527
+ const tooltip = document.createElement('div');
528
+ tooltip.className = tooltipCls('tooltip', variant);
529
+ tooltip.textContent = text;
530
+ tooltip.style.opacity = '0';
531
+ tooltip.style.visibility = 'hidden';
532
+
533
+ const arrow = document.createElement('div');
534
+ arrow.className = tooltipCls('arrow');
535
+ tooltip.appendChild(arrow);
536
+
537
+ document.body.appendChild(tooltip);
538
+
539
+ let showTimeout: ReturnType<typeof setTimeout> | null = null;
540
+ let hideTimeout: ReturnType<typeof setTimeout> | null = null;
541
+
542
+ const showTooltip = (): void => {
543
+ if (hideTimeout) clearTimeout(hideTimeout);
544
+ showTimeout = setTimeout(() => {
545
+ const rect = el.getBoundingClientRect();
546
+ const tooltipRect = tooltip.getBoundingClientRect();
547
+
548
+ let top: number;
549
+ let left: number;
550
+
551
+ switch (position) {
552
+ case 'top':
553
+ top = rect.top - tooltipRect.height - 8;
554
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
555
+ break;
556
+ case 'bottom':
557
+ top = rect.bottom + 8;
558
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
559
+ break;
560
+ case 'left':
561
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
562
+ left = rect.left - tooltipRect.width - 8;
563
+ break;
564
+ case 'right':
565
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
566
+ left = rect.right + 8;
567
+ break;
568
+ default:
569
+ top = rect.top - tooltipRect.height - 8;
570
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
571
+ }
572
+
573
+ const padding = 8;
574
+ if (left < padding) left = padding;
575
+ if (left + tooltipRect.width > window.innerWidth - padding) {
576
+ left = window.innerWidth - tooltipRect.width - padding;
577
+ }
578
+ if (top < padding) top = padding;
579
+ if (top + tooltipRect.height > window.innerHeight - padding) {
580
+ top = window.innerHeight - tooltipRect.height - padding;
581
+ }
582
+
583
+ tooltip.style.top = `${top + window.scrollY}px`;
584
+ tooltip.style.left = `${left + window.scrollX}px`;
585
+ tooltip.className = tooltipCls('tooltip', variant, position);
586
+ tooltip.style.opacity = '1';
587
+ tooltip.style.visibility = 'visible';
588
+ }, delay);
589
+ };
590
+
591
+ const hideTooltip = (): void => {
592
+ if (showTimeout) clearTimeout(showTimeout);
593
+ hideTimeout = setTimeout(() => {
594
+ tooltip.style.opacity = '0';
595
+ tooltip.style.visibility = 'hidden';
596
+ }, 0);
597
+ };
598
+
599
+ el.addEventListener('mouseenter', showTooltip);
600
+ el.addEventListener('mouseleave', hideTooltip);
601
+ el.addEventListener('focus', showTooltip);
602
+ el.addEventListener('blur', hideTooltip);
603
+
604
+ this._tooltip = tooltip;
605
+ this._tooltipCleanup = () => {
606
+ if (showTimeout) clearTimeout(showTimeout);
607
+ if (hideTimeout) clearTimeout(hideTimeout);
608
+ el.removeEventListener('mouseenter', showTooltip);
609
+ el.removeEventListener('mouseleave', hideTooltip);
610
+ el.removeEventListener('focus', showTooltip);
611
+ el.removeEventListener('blur', hideTooltip);
612
+ if (tooltip.parentNode) {
613
+ tooltip.parentNode.removeChild(tooltip);
614
+ }
615
+ };
616
+ }
617
+
618
+ destroy(): void {
619
+ // Destroy children first
620
+ if (this._children) {
621
+ for (const child of this._children) {
622
+ if (child && typeof child.destroy === 'function') {
623
+ child.destroy();
624
+ }
625
+ }
626
+ this._children = [];
627
+ }
628
+
629
+ if (this._effects) {
630
+ for (const stop of this._effects) stop();
631
+ this._effects = [];
632
+ }
633
+
634
+ if (this._domListeners) {
635
+ this._domListeners.forEach(off => off());
636
+ this._domListeners = [];
637
+ }
638
+
639
+ if (this._tooltipCleanup) {
640
+ this._tooltipCleanup();
641
+ }
642
+
643
+ if (this.el) {
644
+ (this.el as HTMLElement & { __ezInstance: unknown }).__ezInstance = null;
645
+ this.el = null;
646
+ }
647
+ }
648
+ }