ezfw-core 1.0.39 → 1.0.41

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 (95) hide show
  1. package/components/EzBaseComponent.ts +26 -4
  2. package/components/EzComponent.ts +4 -2
  3. package/components/EzIcon.ts +2 -1
  4. package/components/HtmlWrapper.ts +3 -2
  5. package/components/drawer/EzDrawer.module.scss +2 -2
  6. package/components/tabs/EzTabPanel.ts +2 -2
  7. package/components/tree/EzTree.ts +9 -3
  8. package/core/ez.ts +39 -3
  9. package/core/loader.ts +143 -23
  10. package/core/renderer.ts +133 -3
  11. package/core/router.ts +10 -2
  12. package/islands/StaticHtmlRenderer.d.ts +3 -3
  13. package/islands/StaticHtmlRenderer.js +342 -339
  14. package/islands/ViteIslandsPlugin.js +779 -780
  15. package/package.json +1 -1
  16. package/components/EzBaseComponent.js +0 -845
  17. package/components/EzComponent.js +0 -52
  18. package/components/EzIcon.js +0 -64
  19. package/components/EzLabel.js +0 -51
  20. package/components/EzOutlet.js +0 -121
  21. package/components/HtmlWrapper.js +0 -163
  22. package/components/avatar/EzAvatar.js +0 -163
  23. package/components/badge/EzBadge.js +0 -57
  24. package/components/button/EzButton.js +0 -141
  25. package/components/button/EzButtonGroup.js +0 -25
  26. package/components/card/EzCard.js +0 -84
  27. package/components/chart/EzBarChart.js +0 -24
  28. package/components/chart/EzChart.js +0 -155
  29. package/components/chart/EzDoughnutChart.js +0 -32
  30. package/components/chart/EzLineChart.js +0 -27
  31. package/components/checkbox/EzCheckbox.js +0 -96
  32. package/components/components/EzBaseComponent.d.ts +0 -177
  33. package/components/components/EzBaseComponent.js +0 -841
  34. package/components/core/styleShortcuts.d.ts +0 -64
  35. package/components/core/styleShortcuts.js +0 -316
  36. package/components/dataview/EzDataView.js +0 -311
  37. package/components/dataview/modes/EzDataViewCards.js +0 -549
  38. package/components/dataview/modes/EzDataViewGrid.js +0 -41
  39. package/components/datepicker/EzDatePicker.js +0 -435
  40. package/components/dialog/EzDialog.js +0 -144
  41. package/components/drawer/EzDrawer.js +0 -52
  42. package/components/dropdown/EzDropdown.js +0 -197
  43. package/components/feed/EzActivityFeed.js +0 -42
  44. package/components/form/EzForm.js +0 -327
  45. package/components/form/EzValidators.js +0 -171
  46. package/components/grid/EzGrid.js +0 -1097
  47. package/components/grid/EzGridContainer.js +0 -58
  48. package/components/grid/body/EzGridBody.js +0 -421
  49. package/components/grid/body/EzGridCell.js +0 -154
  50. package/components/grid/body/EzGridRow.js +0 -122
  51. package/components/grid/filter/EzGridFilters.js +0 -205
  52. package/components/grid/footer/EzGridFooter.js +0 -419
  53. package/components/grid/header/EzGridHeader.js +0 -323
  54. package/components/grid/query/EzGridQuery.js +0 -51
  55. package/components/grid/state/EzGridColumns.js +0 -106
  56. package/components/grid/state/EzGridController.js +0 -327
  57. package/components/grid/state/EzGridLifecycle.js +0 -103
  58. package/components/grid/state/EzGridNormalizers.js +0 -122
  59. package/components/grid/state/EzGridParts.js +0 -146
  60. package/components/grid/state/EzGridPersistence.js +0 -90
  61. package/components/grid/state/EzGridRemote.js +0 -219
  62. package/components/grid/state/EzGridSelection.js +0 -178
  63. package/components/grid/state/EzGridSort.js +0 -209
  64. package/components/grid/title/EzGridActionBar.js +0 -56
  65. package/components/grid/title/EzGridTitle.js +0 -68
  66. package/components/grid/title/EzGridTitleBar.js +0 -53
  67. package/components/grid/types.js +0 -7
  68. package/components/input/EzInput.js +0 -149
  69. package/components/kanban/EzKanban.js +0 -152
  70. package/components/kanban/EzKanbanTypes.js +0 -1
  71. package/components/kanban/board/EzKanbanBoard.js +0 -83
  72. package/components/kanban/card/EzKanbanCard.js +0 -220
  73. package/components/kanban/card/EzKanbanCardEditor.js +0 -176
  74. package/components/kanban/column/EzKanbanColumn.js +0 -195
  75. package/components/kanban/state/EzKanbanController.js +0 -301
  76. package/components/kanban/state/EzKanbanDragDrop.js +0 -185
  77. package/components/layout/EzLayout.js +0 -291
  78. package/components/mask/EzMask.js +0 -32
  79. package/components/orgchart/EzOrgChart.js +0 -919
  80. package/components/panel/EzPanel.js +0 -91
  81. package/components/paper/EzPaper.js +0 -24
  82. package/components/picker/EzPicker.js +0 -85
  83. package/components/radio/EzRadio.js +0 -112
  84. package/components/searchfilter/EzSearchFilter.js +0 -417
  85. package/components/select/EzSelect.js +0 -547
  86. package/components/skeleton/EzSkeleton.js +0 -42
  87. package/components/store/EzStore.js +0 -298
  88. package/components/switch/EzSwitch.js +0 -87
  89. package/components/tabs/EzTabPanel.js +0 -361
  90. package/components/textarea/EzTextarea.js +0 -132
  91. package/components/timepicker/EzTimePicker.js +0 -462
  92. package/components/tooltip/EzTooltip.js +0 -140
  93. package/components/tree/EzTree.js +0 -506
  94. package/core/EzError.js +0 -39
  95. package/core/styleShortcuts.js +0 -316
@@ -11,6 +11,14 @@ const tooltipCls = cx(tooltipStyles);
11
11
 
12
12
  declare const ez: {
13
13
  _controllers: Record<string, EzController | undefined>;
14
+ _internal: {
15
+ state: { breakpoint: string };
16
+ variants: Record<string, string[]>;
17
+ };
18
+ _registry: Record<string, unknown>;
19
+ _loader: {
20
+ hasVariants(name: string): boolean;
21
+ };
14
22
  getController(name: string): Promise<EzController | null>;
15
23
  getControllerSync(name: string): EzController | null;
16
24
  getDeepValue(obj: unknown, path: string[]): unknown;
@@ -349,6 +357,16 @@ export class EzBaseComponent {
349
357
  }
350
358
  }
351
359
 
360
+ /**
361
+ * Setup repaint effect for components with breakpoint variants.
362
+ * Note: This is now handled by the renderer for all components.
363
+ * Kept for potential manual use by class-based components.
364
+ */
365
+ protected _setupRepaintEffect(_el: HTMLElement): void {
366
+ // Repaint is now handled by the renderer in _setupRepaintEffect
367
+ // This method is kept for backward compatibility
368
+ }
369
+
352
370
  private _applyValueBind(
353
371
  el: HTMLElement,
354
372
  bind: BindConfig,
@@ -499,6 +517,10 @@ export class EzBaseComponent {
499
517
  childCfg.css = this.config.css;
500
518
  childCfg._styleModule = this.config._styleModule;
501
519
  }
520
+ // Propagate _isRepaint to skip onLoad in children during breakpoint repaint
521
+ if ((this.config as { _isRepaint?: boolean })._isRepaint) {
522
+ (childCfg as { _isRepaint?: boolean })._isRepaint = true;
523
+ }
502
524
  // Pass 'this' as inheritedState so children are added to _children
503
525
  const childEl = await ez._createElement(childCfg, null, this);
504
526
 
@@ -568,10 +590,10 @@ export class EzBaseComponent {
568
590
 
569
591
  let result = true;
570
592
  switch (operator) {
571
- case '>': result = left > right; break;
572
- case '<': result = left < right; break;
573
- case '>=': result = left >= right; break;
574
- case '<=': result = left <= right; break;
593
+ case '>': result = (left as number) > (right as number); break;
594
+ case '<': result = (left as number) < (right as number); break;
595
+ case '>=': result = (left as number) >= (right as number); break;
596
+ case '<=': result = (left as number) <= (right as number); break;
575
597
  case '!=': result = left != right; break;
576
598
  case '==': result = left == right; break;
577
599
  }
@@ -5,7 +5,8 @@ declare const ez: {
5
5
  items: EzComponentConfig[],
6
6
  controller: string | null,
7
7
  state: unknown,
8
- css: string | null
8
+ css: string | null,
9
+ isRepaint?: boolean
9
10
  ): Promise<Node[]>;
10
11
  };
11
12
 
@@ -75,7 +76,8 @@ export class EzComponent extends EzBaseComponent {
75
76
  items,
76
77
  this.config.controller || null,
77
78
  null,
78
- (typeof this.config.css === 'string' ? this.config.css : null) as string | null
79
+ (typeof this.config.css === 'string' ? this.config.css : null) as string | null,
80
+ (this.config as { _isRepaint?: boolean })._isRepaint || false
79
81
  );
80
82
 
81
83
  for (const child of children) {
@@ -1,7 +1,7 @@
1
1
  import { EzBaseComponent, EzBaseComponentConfig } from './EzBaseComponent.js';
2
2
 
3
3
  type IconType = 'solid' | 'regular' | 'brands' | 'light' | 'thin' | 'duotone';
4
- type IconSize = 'xs' | 'sm' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x';
4
+ type IconSize = 'xxs' | 'xs' | 'sm' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x';
5
5
 
6
6
  const SEMANTIC_COLORS: Record<string, string> = {
7
7
  primary: 'var(--ez-primary)',
@@ -16,6 +16,7 @@ const SEMANTIC_COLORS: Record<string, string> = {
16
16
  };
17
17
 
18
18
  const SIZE_MAP: Record<string, string> = {
19
+ xxs: '0.65em',
19
20
  xs: '0.75em',
20
21
  sm: '0.875em',
21
22
  lg: '1.25em',
@@ -1,7 +1,7 @@
1
1
  import { EzBaseComponent, EzBaseComponentConfig } from "./EzBaseComponent.js";
2
2
 
3
3
  declare const ez: {
4
- _createChildElements(items: unknown[], controller: string | null, parent: unknown, css?: string | null): Promise<Node[]>;
4
+ _createChildElements(items: unknown[], controller: string | null, parent: unknown, css?: string | null, isRepaint?: boolean): Promise<Node[]>;
5
5
  };
6
6
 
7
7
  type DOMEventHandler<E extends Event = Event> = (e: E) => void;
@@ -295,7 +295,8 @@ export class HtmlWrapper extends EzBaseComponent {
295
295
  cfg.items,
296
296
  cfg.controller || null,
297
297
  this,
298
- (cfg.css as string) || null
298
+ (cfg.css as string) || null,
299
+ (cfg as { _isRepaint?: boolean })._isRepaint || false
299
300
  );
300
301
  children.forEach(child => el.appendChild(child));
301
302
  }
@@ -10,7 +10,7 @@
10
10
  visibility: hidden;
11
11
  transition: opacity 0.3s ease, visibility 0.3s ease;
12
12
 
13
- &.open {
13
+ &:global(.open) {
14
14
  opacity: 1;
15
15
  visibility: visible;
16
16
  }
@@ -54,7 +54,7 @@
54
54
  }
55
55
 
56
56
  // Open state
57
- &.open {
57
+ &:global(.open) {
58
58
  &.left,
59
59
  &.right {
60
60
  transform: translateX(0);
@@ -5,7 +5,7 @@ import { EzComponent } from '../EzComponent.js';
5
5
 
6
6
  declare const ez: {
7
7
  _createElement(config: unknown, controller?: string | null, parent?: unknown): Promise<HTMLElement>;
8
- _createChildElements(items: unknown[], controller: string | null, parent: unknown): Promise<Node[]>;
8
+ _createChildElements(items: unknown[], controller: string | null, parent: unknown, css?: string | null, isRepaint?: boolean): Promise<Node[]>;
9
9
  };
10
10
 
11
11
  const cls = cx(styles);
@@ -280,7 +280,7 @@ export class EzTabPanel extends EzComponent {
280
280
  return item;
281
281
  });
282
282
 
283
- const children = await ez._createChildElements(items, this.config.controller || null, this);
283
+ const children = await ez._createChildElements(items, this.config.controller || null, this, null, (this.config as { _isRepaint?: boolean })._isRepaint || false);
284
284
  for (const child of children) {
285
285
  pane.appendChild(child);
286
286
  }
@@ -38,6 +38,7 @@ export interface EzTreeConfig extends EzComponentExtendedConfig {
38
38
  defaultExpandLevel?: number;
39
39
 
40
40
  selectable?: boolean;
41
+ autoSelectFirst?: boolean;
41
42
  showIcon?: boolean;
42
43
  showLine?: boolean;
43
44
 
@@ -90,6 +91,7 @@ export class EzTree extends EzComponent {
90
91
  childrenKey: 'children',
91
92
  labelKey: 'label',
92
93
  selectable: true,
94
+ autoSelectFirst: true,
93
95
  showIcon: true,
94
96
  showLine: false,
95
97
  defaultExpandAll: false,
@@ -130,7 +132,11 @@ export class EzTree extends EzComponent {
130
132
 
131
133
  const existingBind = (this.config.bind && typeof this.config.bind === 'object') ? this.config.bind : {};
132
134
  this.config.bind = { ...existingBind, data: this._dataStateKey };
133
- void this._loadData();
135
+
136
+ // Skip loading during repaint - data already exists in controller state
137
+ if (!(this.config as { _isRepaint?: boolean })._isRepaint) {
138
+ void this._loadData();
139
+ }
134
140
  }
135
141
  }
136
142
 
@@ -330,8 +336,8 @@ export class EzTree extends EzComponent {
330
336
  const data = await this._store.load();
331
337
  state[this._dataStateKey] = data;
332
338
 
333
- // Select first item by default
334
- if (Array.isArray(data) && data.length > 0) {
339
+ // Select first item by default (if enabled)
340
+ if (this.config.autoSelectFirst && Array.isArray(data) && data.length > 0) {
335
341
  const firstNode = data[0] as EzTreeNode;
336
342
  const key = this._getNodeKey(firstNode);
337
343
  this._select(key, firstNode);
package/core/ez.ts CHANGED
@@ -6,6 +6,7 @@ import { EzState, ControllerDefinition, GridControllerDefinition, GridBehaviorDe
6
6
  import { EzModel, ModelDefinition, Model } from './EzModel.js';
7
7
  import { EzServices } from './services.js';
8
8
  import { EzEventBus, EventHandler } from './eventBus.js';
9
+ import { deepSignal } from 'deepsignal';
9
10
  import * as EzGridQuery from '../components/grid/query/EzGridQuery.js';
10
11
  import dayjs from 'dayjs';
11
12
  import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -42,6 +43,15 @@ interface EzContext {
42
43
  controller: string | null;
43
44
  }
44
45
 
46
+ interface EzInternalState {
47
+ breakpoint: string;
48
+ }
49
+
50
+ interface EzInternal {
51
+ state: EzInternalState;
52
+ variants: Record<string, string[]>; // { 'Department': ['desktop', 'mobile'] }
53
+ }
54
+
45
55
  export interface EzFramework {
46
56
  _registry: Record<string, unknown>;
47
57
  _controllers: Record<string, ControllerDefinition>;
@@ -50,6 +60,7 @@ export interface EzFramework {
50
60
  _refs: Record<string, unknown>;
51
61
  _registryMeta: Record<string, RegistryMeta>;
52
62
  _context: EzContext;
63
+ _internal: EzInternal;
53
64
  _gridControllerDefinitions: Record<string, GridControllerDefinition>;
54
65
  _gridStateRegistry: Record<string, unknown>;
55
66
  _gridBehaviorDefinitions: Record<string, GridBehaviorDefinition>;
@@ -171,7 +182,7 @@ export interface EzFramework {
171
182
  getController(name: string): Promise<ControllerDefinition> | ControllerDefinition;
172
183
  getControllerSync(name: string): ControllerDefinition;
173
184
  _createElement(config: ComponentConfig, controllerName?: string | null, inheritedState?: unknown): Promise<Node>;
174
- _createChildElements(items: ComponentConfig[], controllerName: string | null, inheritedState: unknown, parentCss?: string | null): Promise<Node[]>;
185
+ _createChildElements(items: ComponentConfig[], controllerName: string | null, inheritedState: unknown, parentCss?: string | null, isRepaint?: boolean): Promise<Node[]>;
175
186
  routes(routeDefs: RouteDefinition[]): void;
176
187
  go(path: string): void;
177
188
  navigate(viewName: string, route?: RouteInfo | null): Promise<void>;
@@ -230,6 +241,7 @@ export interface EzFramework {
230
241
  isTablet(): boolean;
231
242
  isDesktop(): boolean;
232
243
  _initBreakpoints(): void;
244
+ _hasAnyResponsiveModules(): boolean;
233
245
  _updateBreakpoint(): void;
234
246
  format(value: unknown, type?: FormatType, options?: FormatOptions): string;
235
247
  capitalize(str: string): string;
@@ -251,6 +263,18 @@ const ez: EzFramework = {
251
263
  view: null,
252
264
  controller: null
253
265
  },
266
+ _internal: {
267
+ state: deepSignal({
268
+ breakpoint: (() => {
269
+ if (typeof window === 'undefined') return 'desktop';
270
+ const width = window.innerWidth;
271
+ if (width < 768) return 'mobile';
272
+ if (width < 1024) return 'tablet';
273
+ return 'desktop';
274
+ })()
275
+ }),
276
+ variants: {}
277
+ },
254
278
  _gridControllerDefinitions: {},
255
279
  _gridStateRegistry: {},
256
280
  _gridBehaviorDefinitions: {},
@@ -433,8 +457,8 @@ const ez: EzFramework = {
433
457
  return this._renderer!.createElement(config, controllerName, inheritedState);
434
458
  },
435
459
 
436
- async _createChildElements(items: ComponentConfig[], controllerName: string | null, inheritedState: unknown, parentCss: string | null = null): Promise<Node[]> {
437
- return this._renderer!.createChildElements(items, controllerName || '', inheritedState, parentCss);
460
+ async _createChildElements(items: ComponentConfig[], controllerName: string | null, inheritedState: unknown, parentCss: string | null = null, isRepaint: boolean = false): Promise<Node[]> {
461
+ return this._renderer!.createChildElements(items, controllerName || '', inheritedState, parentCss, isRepaint);
438
462
  },
439
463
 
440
464
  routes(routeDefs: RouteDefinition[]): void {
@@ -665,6 +689,14 @@ const ez: EzFramework = {
665
689
  return this._currentBreakpoint === 'desktop';
666
690
  },
667
691
 
692
+ _hasAnyResponsiveModules(): boolean {
693
+ if (!this._loader) return false;
694
+ const modules = this._loader.getModules();
695
+ return Object.keys(modules).some(p =>
696
+ p.includes('.mobile.') || p.includes('.tablet.')
697
+ );
698
+ },
699
+
668
700
  _updateBreakpoint(): void {
669
701
  const width = window.innerWidth;
670
702
  const { mobile, tablet } = this._breakpoints;
@@ -682,6 +714,10 @@ const ez: EzFramework = {
682
714
  const oldBreakpoint = this._currentBreakpoint;
683
715
  this._currentBreakpoint = newBreakpoint;
684
716
  document.documentElement.setAttribute('data-ez-breakpoint', newBreakpoint);
717
+
718
+ // Update reactive state - this triggers repaint on components with variants
719
+ this._internal.state.breakpoint = newBreakpoint;
720
+
685
721
  this._eventBus?.emit('breakpoint:change', {
686
722
  breakpoint: newBreakpoint,
687
723
  previous: oldBreakpoint
package/core/loader.ts CHANGED
@@ -7,6 +7,13 @@ export interface ModuleMeta {
7
7
 
8
8
  export type ModuleMap = Record<string, () => Promise<unknown>>;
9
9
 
10
+ export interface BreakpointModules {
11
+ framework: ModuleMap;
12
+ desktop: ModuleMap;
13
+ mobile: ModuleMap;
14
+ tablet: ModuleMap;
15
+ }
16
+
10
17
  export type LoadPriority = 'high' | 'low' | 'idle';
11
18
 
12
19
  export type NetworkStrategy = 'fast' | 'medium' | 'slow' | 'offline';
@@ -63,6 +70,10 @@ interface EzInstance {
63
70
  route?: string | null;
64
71
  view?: string | null;
65
72
  };
73
+ _internal: {
74
+ state: { breakpoint: string };
75
+ variants: Record<string, string[]>;
76
+ };
66
77
  _router?: EzRouter;
67
78
  _currentBreakpoint: string;
68
79
  registerComponent(name: string, component: unknown): void;
@@ -73,7 +84,13 @@ interface EzInstance {
73
84
 
74
85
  export class EzLoader {
75
86
  private ez: EzInstance;
76
- private _modules: ModuleMap = {};
87
+ private _modules: ModuleMap = {}; // Combined flat map for backward compat
88
+ private _frameworkModules: ModuleMap = {};
89
+ private _desktopModules: ModuleMap = {};
90
+ private _mobileModules: ModuleMap = {};
91
+ private _tabletModules: ModuleMap = {};
92
+ // Cache for variant definitions (ES modules only execute once)
93
+ private _variantCache: Record<string, unknown> = {}; // 'Department@mobile' -> definition
77
94
  private _moduleIndex: Record<string, string[]> = {};
78
95
  private _moduleMeta: Record<string, ModuleMeta> = {};
79
96
  private _currentModulePath: string | null = null;
@@ -196,38 +213,100 @@ export class EzLoader {
196
213
  };
197
214
  }
198
215
 
199
- installModules(mods: ModuleMap): void {
200
- this._modules = mods;
216
+ installModules(mods: BreakpointModules | ModuleMap): void {
217
+ // Support both old flat format and new breakpoint-separated format
218
+ if ('framework' in mods && 'desktop' in mods) {
219
+ const breakpointMods = mods as BreakpointModules;
220
+ this._frameworkModules = breakpointMods.framework;
221
+ this._desktopModules = breakpointMods.desktop;
222
+ this._mobileModules = breakpointMods.mobile || {};
223
+ this._tabletModules = breakpointMods.tablet || {};
224
+
225
+ // Combined flat map for backward compatibility
226
+ this._modules = {
227
+ ...breakpointMods.framework,
228
+ ...breakpointMods.desktop,
229
+ ...breakpointMods.mobile,
230
+ ...breakpointMods.tablet
231
+ };
232
+
233
+ // Build variants registry
234
+ this._buildVariantsRegistry();
235
+ } else {
236
+ // Legacy flat format
237
+ this._modules = mods as ModuleMap;
238
+ }
239
+
201
240
  this._moduleIndex = {};
202
241
 
203
- for (const path of Object.keys(mods)) {
242
+ for (const path of Object.keys(this._modules)) {
204
243
  this._moduleMeta[path] = {
205
244
  path,
206
245
  defines: []
207
246
  };
208
247
  const file = path.split('/').pop();
209
248
  if (!file || (!file.endsWith('.js') && !file.endsWith('.ts'))) continue;
249
+ // Skip TypeScript declaration files
250
+ if (file.endsWith('.d.ts')) continue;
210
251
 
211
252
  const eztype = file.replace(/\.(js|ts)$/, '');
212
253
 
213
254
  this._moduleIndex[eztype] ??= [];
214
255
  this._moduleIndex[eztype].push(path);
215
256
  }
257
+ }
216
258
 
217
- for (const eztype in this._moduleIndex) {
218
- const paths = this._moduleIndex[eztype];
219
- if (paths.length > 1) {
220
- throw new EzError({
221
- code: 'EZ_MODULE_DUPLICATE',
222
- source: 'module',
223
- message: `Multiple modules resolve to eztype "${eztype}"`,
224
- details: {
225
- eztype,
226
- modules: paths
227
- }
228
- });
259
+ private _buildVariantsRegistry(): void {
260
+ const variants: Record<string, string[]> = {};
261
+
262
+
263
+ // Extract component names from desktop modules
264
+ for (const path of Object.keys(this._desktopModules)) {
265
+ const file = path.split('/').pop();
266
+ if (!file || (!file.endsWith('.js') && !file.endsWith('.ts'))) continue;
267
+ if (file.endsWith('.d.ts')) continue;
268
+
269
+ const componentName = file.replace(/\.(js|ts)$/, '');
270
+ variants[componentName] = ['desktop'];
271
+ }
272
+
273
+ // Check for mobile variants
274
+ for (const path of Object.keys(this._mobileModules)) {
275
+ const file = path.split('/').pop();
276
+ if (!file) continue;
277
+
278
+ // Department.mobile.js -> Department
279
+ const match = file.match(/^(.+)\.mobile\.(js|ts)$/);
280
+ if (match) {
281
+ const componentName = match[1];
282
+ if (variants[componentName]) {
283
+ variants[componentName].push('mobile');
284
+ }
229
285
  }
230
286
  }
287
+
288
+ // Check for tablet variants
289
+ for (const path of Object.keys(this._tabletModules)) {
290
+ const file = path.split('/').pop();
291
+ if (!file) continue;
292
+
293
+ const match = file.match(/^(.+)\.tablet\.(js|ts)$/);
294
+ if (match) {
295
+ const componentName = match[1];
296
+ if (variants[componentName]) {
297
+ variants[componentName].push('tablet');
298
+ }
299
+ }
300
+ }
301
+
302
+ // Only keep components with multiple variants
303
+ for (const name in variants) {
304
+ if (variants[name].length === 1) {
305
+ delete variants[name];
306
+ }
307
+ }
308
+
309
+ this.ez._internal.variants = variants;
231
310
  }
232
311
 
233
312
  async loadModule(path: string): Promise<unknown> {
@@ -253,21 +332,38 @@ export class EzLoader {
253
332
  if (this.ez._registry[eztype]) return;
254
333
 
255
334
  const lowerEztype = eztype.toLowerCase();
256
- const keys = Object.keys(this._modules);
257
335
  const breakpoint = this.ez._currentBreakpoint;
258
336
 
259
- // Try responsive variant first (e.g., Department.mobile.js)
260
337
  let path: string | undefined;
261
338
 
262
- if (breakpoint !== 'desktop') {
263
- path = keys.find(p => {
339
+ // Use breakpoint-specific registry if available
340
+ if (breakpoint === 'mobile' && Object.keys(this._mobileModules).length > 0) {
341
+ path = Object.keys(this._mobileModules).find(p => {
264
342
  const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
265
- return filename === `${lowerEztype}.${breakpoint}.js` || filename === `${lowerEztype}.${breakpoint}.ts`;
343
+ return filename === `${lowerEztype}.mobile.js` || filename === `${lowerEztype}.mobile.ts`;
344
+ });
345
+ } else if (breakpoint === 'tablet' && Object.keys(this._tabletModules).length > 0) {
346
+ path = Object.keys(this._tabletModules).find(p => {
347
+ const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
348
+ return filename === `${lowerEztype}.tablet.js` || filename === `${lowerEztype}.tablet.ts`;
266
349
  });
267
350
  }
268
351
 
269
- // Fallback to default file (e.g., Department.js)
352
+ // Fallback to desktop/framework modules
270
353
  if (!path) {
354
+ const desktopKeys = Object.keys(this._desktopModules);
355
+ const frameworkKeys = Object.keys(this._frameworkModules);
356
+ const allKeys = [...desktopKeys, ...frameworkKeys];
357
+
358
+ path = allKeys.find(p => {
359
+ const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
360
+ return filename === `${lowerEztype}.js` || filename === `${lowerEztype}.ts`;
361
+ });
362
+ }
363
+
364
+ // Final fallback to combined modules (legacy support)
365
+ if (!path) {
366
+ const keys = Object.keys(this._modules);
271
367
  path = keys.find(p => {
272
368
  const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
273
369
  return filename === `${lowerEztype}.js` || filename === `${lowerEztype}.ts`;
@@ -278,6 +374,17 @@ export class EzLoader {
278
374
  return;
279
375
  }
280
376
 
377
+ // Determine which variant we're loading
378
+ const isVariant = path.includes('.mobile.') || path.includes('.tablet.');
379
+ const variantType = path.includes('.mobile.') ? 'mobile' : path.includes('.tablet.') ? 'tablet' : 'desktop';
380
+ const cacheKey = `${eztype}@${variantType}`;
381
+
382
+ // Check variant cache first (ES modules only execute once, so ez.define won't re-run)
383
+ if (this._variantCache[cacheKey]) {
384
+ this.ez.registerComponent(eztype, this._variantCache[cacheKey]);
385
+ return;
386
+ }
387
+
281
388
  const module = await this.loadModuleWithRetry(path) as Record<string, unknown>;
282
389
 
283
390
  let exported = module.default || Object.values(module).find(v => typeof v === 'function');
@@ -287,7 +394,12 @@ export class EzLoader {
287
394
  }
288
395
 
289
396
  if (!exported) {
290
- throw new Error(`[ez] Module loaded for ${eztype} but exports no class/function or ez.define`);
397
+ throw new Error(`[ez] Module loaded for ${eztype} but exports no class/function or ez.define. Path: ${path}`);
398
+ }
399
+
400
+ // Cache the variant definition for future use
401
+ if (isVariant || this.hasVariants(eztype)) {
402
+ this._variantCache[cacheKey] = exported;
291
403
  }
292
404
 
293
405
  this.ez.registerComponent(eztype, exported);
@@ -298,6 +410,14 @@ export class EzLoader {
298
410
  }
299
411
  }
300
412
 
413
+ hasVariants(componentName: string): boolean {
414
+ return componentName in this.ez._internal.variants;
415
+ }
416
+
417
+ getVariants(componentName: string): string[] {
418
+ return this.ez._internal.variants[componentName] || [];
419
+ }
420
+
301
421
  async resolveController(name: string | object | null): Promise<unknown> {
302
422
  if (!name) return null;
303
423