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,63 @@
1
+ export interface EzErrorConfig {
2
+ code: string;
3
+ message: string;
4
+ source?: string;
5
+ route?: string | null;
6
+ view?: string | null;
7
+ details?: unknown;
8
+ cause?: Error | null;
9
+ }
10
+
11
+ export class EzError extends Error {
12
+ name: string = 'EzError';
13
+ code: string;
14
+ source: string;
15
+ route: string | null;
16
+ view: string | null;
17
+ details: unknown;
18
+ cause: Error | null;
19
+
20
+ constructor({
21
+ code,
22
+ message,
23
+ source = 'framework',
24
+ route = null,
25
+ view = null,
26
+ details = null,
27
+ cause = null
28
+ }: EzErrorConfig) {
29
+ super(message);
30
+
31
+ this.code = code;
32
+ this.source = source;
33
+ this.route = route;
34
+ this.view = view;
35
+ this.details = details;
36
+ this.cause = cause;
37
+
38
+ this.handleFrameworkError(this);
39
+ }
40
+
41
+ handleFrameworkError(error: EzError): void {
42
+ console.group(`🧱 EZ Framework Error — ${error.code}`);
43
+ console.error(error.message);
44
+
45
+ console.info('Source:', error.source);
46
+ if (error.route) console.info('Route:', error.route);
47
+ if (error.view) console.info('View:', error.view);
48
+
49
+ if (error.details) {
50
+ console.group('Details');
51
+ console.dir(error.details);
52
+ console.groupEnd();
53
+ }
54
+
55
+ if (error.cause) {
56
+ console.group('Cause');
57
+ console.error(error.cause);
58
+ console.groupEnd();
59
+ }
60
+
61
+ console.groupEnd();
62
+ }
63
+ }
@@ -0,0 +1,268 @@
1
+ import { EzError } from './EzError.js';
2
+
3
+ type TypeCoercer = (value: unknown) => unknown;
4
+
5
+ export interface FieldDefinition {
6
+ name: string;
7
+ type?: string;
8
+ default?: unknown | (() => unknown);
9
+ primaryKey?: boolean;
10
+ compute?: (record: Record<string, unknown>) => unknown;
11
+ nullable?: boolean;
12
+ }
13
+
14
+ export interface ModelDefinition {
15
+ fields: FieldDefinition[];
16
+ }
17
+
18
+ export interface NormalizedField {
19
+ name: string;
20
+ type: string;
21
+ default: unknown | (() => unknown) | undefined;
22
+ primaryKey: boolean;
23
+ compute: ((record: Record<string, unknown>) => unknown) | null;
24
+ nullable: boolean;
25
+ }
26
+
27
+ export interface ValidationError {
28
+ field: string;
29
+ error: string;
30
+ message: string;
31
+ received?: string;
32
+ }
33
+
34
+ export interface ValidationResult {
35
+ valid: boolean;
36
+ errors: ValidationError[];
37
+ }
38
+
39
+ export interface Model {
40
+ name: string;
41
+ fields: NormalizedField[];
42
+ fieldMap: Record<string, NormalizedField>;
43
+ primaryKey: string | null;
44
+ coerce: (record: Record<string, unknown>) => Record<string, unknown>;
45
+ validate: (record: Record<string, unknown>) => ValidationResult;
46
+ applyDefaults: (record: Record<string, unknown>) => Record<string, unknown>;
47
+ applyComputed: (record: Record<string, unknown>) => Record<string, unknown>;
48
+ process: (record: Record<string, unknown>) => Record<string, unknown>;
49
+ processAll: (records: Record<string, unknown>[]) => Record<string, unknown>[];
50
+ }
51
+
52
+ interface EzInstance {
53
+ _models: Record<string, Model>;
54
+ }
55
+
56
+ export class EzModel {
57
+ private ez: EzInstance;
58
+
59
+ static TYPES: Record<string, TypeCoercer> = {
60
+ int: (v) => v == null ? null : parseInt(String(v), 10),
61
+ float: (v) => v == null ? null : parseFloat(String(v)),
62
+ string: (v) => v == null ? null : String(v),
63
+ bool: (v) => {
64
+ if (v == null) return null;
65
+ if (typeof v === 'string') {
66
+ return v.toLowerCase() === 'true' || v === '1';
67
+ }
68
+ return Boolean(v);
69
+ },
70
+ date: (v) => {
71
+ if (v == null) return null;
72
+ if (v instanceof Date) return v;
73
+ const d = new Date(v as string | number);
74
+ return isNaN(d.getTime()) ? null : d;
75
+ },
76
+ array: (v) => {
77
+ if (v == null) return [];
78
+ return Array.isArray(v) ? v : [v];
79
+ },
80
+ object: (v) => v == null ? {} : (typeof v === 'object' ? v : {}),
81
+ any: (v) => v
82
+ };
83
+
84
+ constructor(ez: EzInstance) {
85
+ this.ez = ez;
86
+ }
87
+
88
+ defineModel(name: string, definition: ModelDefinition): void {
89
+ if (this.ez._models[name]) {
90
+ throw new EzError({
91
+ code: 'EZ_MODEL_DUPLICATE',
92
+ source: 'model',
93
+ message: `Model "${name}" is already defined`
94
+ });
95
+ }
96
+
97
+ const fields = (definition.fields || []).map(f => this._normalizeField(f));
98
+
99
+ const model: Model = {
100
+ name,
101
+ fields,
102
+ fieldMap: Object.fromEntries(fields.map(f => [f.name, f])),
103
+ primaryKey: fields.find(f => f.primaryKey)?.name || null,
104
+
105
+ coerce: (record) => this._coerceRecord(fields, record),
106
+ validate: (record) => this._validateRecord(fields, record),
107
+ applyDefaults: (record) => this._applyDefaults(fields, record),
108
+ applyComputed: (record) => this._applyComputed(fields, record),
109
+ process: (record) => this._processRecord(fields, record),
110
+ processAll: (records) => this._processAll(fields, records)
111
+ };
112
+
113
+ this.ez._models[name] = model;
114
+ }
115
+
116
+ private _normalizeField(field: FieldDefinition): NormalizedField {
117
+ return {
118
+ name: field.name,
119
+ type: field.type || 'any',
120
+ default: field.default,
121
+ primaryKey: field.primaryKey || false,
122
+ compute: field.compute || null,
123
+ nullable: field.nullable !== false
124
+ };
125
+ }
126
+
127
+ getModel(name: string): Model | null {
128
+ return this.ez._models[name] || null;
129
+ }
130
+
131
+ getModelSync(name: string): Model {
132
+ const model = this.ez._models[name];
133
+ if (!model) {
134
+ throw new EzError({
135
+ code: 'EZ_MODEL_001',
136
+ source: 'model',
137
+ message: `Model "${name}" not found`,
138
+ details: {
139
+ available: Object.keys(this.ez._models)
140
+ }
141
+ });
142
+ }
143
+ return model;
144
+ }
145
+
146
+ private _processRecord(fields: NormalizedField[], record: Record<string, unknown>): Record<string, unknown> {
147
+ if (!record || typeof record !== 'object') {
148
+ return record;
149
+ }
150
+
151
+ let result = { ...record };
152
+ result = this._applyDefaults(fields, result);
153
+ result = this._coerceRecord(fields, result);
154
+ result = this._applyComputed(fields, result);
155
+ return result;
156
+ }
157
+
158
+ private _processAll(fields: NormalizedField[], records: Record<string, unknown>[]): Record<string, unknown>[] {
159
+ if (!Array.isArray(records)) {
160
+ return [];
161
+ }
162
+ return records.map(r => this._processRecord(fields, r));
163
+ }
164
+
165
+ private _applyDefaults(fields: NormalizedField[], record: Record<string, unknown>): Record<string, unknown> {
166
+ const result = { ...record };
167
+
168
+ for (const field of fields) {
169
+ if (result[field.name] === undefined && field.default !== undefined) {
170
+ result[field.name] = typeof field.default === 'function'
171
+ ? field.default()
172
+ : field.default;
173
+ }
174
+ }
175
+
176
+ return result;
177
+ }
178
+
179
+ private _coerceRecord(fields: NormalizedField[], record: Record<string, unknown>): Record<string, unknown> {
180
+ const result = { ...record };
181
+
182
+ for (const field of fields) {
183
+ if (field.compute) continue;
184
+
185
+ if (result[field.name] !== undefined) {
186
+ const coercer = EzModel.TYPES[field.type] || EzModel.TYPES.any;
187
+ result[field.name] = coercer(result[field.name]);
188
+ }
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ private _applyComputed(fields: NormalizedField[], record: Record<string, unknown>): Record<string, unknown> {
195
+ const result = { ...record };
196
+
197
+ for (const field of fields) {
198
+ if (field.compute && typeof field.compute === 'function') {
199
+ try {
200
+ result[field.name] = field.compute(result);
201
+ } catch (err) {
202
+ console.warn(`[EzModel] Error computing field "${field.name}":`, err);
203
+ result[field.name] = null;
204
+ }
205
+ }
206
+ }
207
+
208
+ return result;
209
+ }
210
+
211
+ private _validateRecord(fields: NormalizedField[], record: Record<string, unknown>): ValidationResult {
212
+ const errors: ValidationError[] = [];
213
+
214
+ for (const field of fields) {
215
+ const value = record[field.name];
216
+
217
+ if (field.compute) continue;
218
+
219
+ if (value == null && !field.nullable) {
220
+ errors.push({
221
+ field: field.name,
222
+ error: 'required',
223
+ message: `Field "${field.name}" is required`
224
+ });
225
+ continue;
226
+ }
227
+
228
+ if (value != null) {
229
+ const isValid = this._validateType(field.type, value);
230
+ if (!isValid) {
231
+ errors.push({
232
+ field: field.name,
233
+ error: 'type',
234
+ message: `Field "${field.name}" must be of type ${field.type}`,
235
+ received: typeof value
236
+ });
237
+ }
238
+ }
239
+ }
240
+
241
+ return {
242
+ valid: errors.length === 0,
243
+ errors
244
+ };
245
+ }
246
+
247
+ private _validateType(type: string, value: unknown): boolean {
248
+ switch (type) {
249
+ case 'int':
250
+ return Number.isInteger(value) || (typeof value === 'string' && /^-?\d+$/.test(value));
251
+ case 'float':
252
+ return typeof value === 'number' || (typeof value === 'string' && !isNaN(parseFloat(value)));
253
+ case 'string':
254
+ return typeof value === 'string';
255
+ case 'bool':
256
+ return typeof value === 'boolean' || value === 'true' || value === 'false' || value === 0 || value === 1;
257
+ case 'date':
258
+ return value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)));
259
+ case 'array':
260
+ return Array.isArray(value);
261
+ case 'object':
262
+ return typeof value === 'object' && !Array.isArray(value);
263
+ case 'any':
264
+ default:
265
+ return true;
266
+ }
267
+ }
268
+ }
@@ -0,0 +1,328 @@
1
+ import type { EzItemConfig } from './EzComponentTypes.js';
2
+
3
+ /**
4
+ * Configuration for component items (children)
5
+ * For component-specific autocomplete, use EzItemConfig instead
6
+ */
7
+ export interface ItemConfig {
8
+ /**
9
+ * Component type to render
10
+ * @example eztype: 'EzButton'
11
+ * @example eztype: 'div' // native HTML element
12
+ */
13
+ eztype?: string;
14
+
15
+ /**
16
+ * CSS class(es) to apply to the element
17
+ * @example cls: 'header'
18
+ * @example cls: ['card', 'primary', 'shadow']
19
+ */
20
+ cls?: string | string[];
21
+
22
+ /**
23
+ * CSS module name - framework auto-loads {css}.module.scss
24
+ * @example css: 'UserCard' // loads UserCard.module.scss
25
+ */
26
+ css?: string;
27
+
28
+ /**
29
+ * Text content for the element
30
+ * @example text: 'Hello World'
31
+ * @example text: user.name
32
+ */
33
+ text?: string;
34
+
35
+ /**
36
+ * Raw HTML content (use with caution - XSS risk)
37
+ * @example html: '<strong>Bold</strong>'
38
+ */
39
+ html?: string;
40
+
41
+ /**
42
+ * Child items to render inside this element
43
+ * @example items: [{ eztype: 'span', text: 'Child 1' }]
44
+ */
45
+ items?: ItemConfig[];
46
+
47
+ /**
48
+ * Flex grow value for flexbox layouts
49
+ * @example flex: 1 // takes remaining space
50
+ * @example flex: 2 // takes 2x space relative to flex: 1
51
+ */
52
+ flex?: number;
53
+
54
+ /**
55
+ * Inline CSS styles
56
+ * @example style: { padding: '16px', gap: '8px' }
57
+ */
58
+ style?: Record<string, string | number>;
59
+
60
+ /**
61
+ * Props passed to template-based components
62
+ * @example props: { user: userData, showAvatar: true }
63
+ */
64
+ props?: Record<string, unknown>;
65
+
66
+ /**
67
+ * Data bindings for reactive updates
68
+ * @example bind: { text: 'userName', visible: 'isLoggedIn' }
69
+ */
70
+ bind?: BindConfig;
71
+
72
+ /**
73
+ * Reference name for accessing via ez._refs
74
+ * @example ref: 'submitBtn' // access as ez._refs.submitBtn
75
+ */
76
+ ref?: string;
77
+
78
+ /**
79
+ * Click event handler - method name (string) or function
80
+ * @example onClick: 'handleClick' // calls controller.handleClick()
81
+ * @example onClick: (e) => console.log('clicked')
82
+ */
83
+ onClick?: string | ((e: Event) => void);
84
+
85
+ /**
86
+ * Change event handler (for inputs)
87
+ * @example onChange: 'onSelectChange'
88
+ */
89
+ onChange?: string | ((e: Event) => void);
90
+
91
+ /**
92
+ * Input event handler (fires on every keystroke)
93
+ * @example onInput: 'onSearchInput'
94
+ */
95
+ onInput?: string | ((e: Event) => void);
96
+
97
+ /** Any additional properties */
98
+ [key: string]: unknown;
99
+ }
100
+
101
+ /**
102
+ * Data binding configuration for reactive updates
103
+ */
104
+ export interface BindConfig {
105
+ /**
106
+ * Bind to array data for list rendering with itemRender
107
+ * @example
108
+ * bind: { data: 'users' },
109
+ * itemRender: (user) => ({ eztype: 'UserCard', props: { user } })
110
+ */
111
+ data?: string;
112
+
113
+ /**
114
+ * Bind text content to a state property
115
+ * @example bind: { text: 'userName' } // displays state.userName
116
+ */
117
+ text?: string;
118
+
119
+ /**
120
+ * Conditionally show/hide element based on state
121
+ * @example bind: { visible: 'isLoading' }
122
+ * @example bind: { visible: () => ctrl.state.items.length > 0 }
123
+ */
124
+ visible?: string | (() => boolean);
125
+
126
+ /**
127
+ * Dynamically set CSS classes based on state
128
+ * @example bind: { cls: 'activeClass' } // uses state.activeClass
129
+ * @example bind: { cls: () => ctrl.state.isActive ? 'active' : 'inactive' }
130
+ */
131
+ cls?: string | (() => string | string[]);
132
+
133
+ /**
134
+ * Bind input value to state (two-way binding)
135
+ * @example bind: { value: 'formData.email' }
136
+ */
137
+ value?: string;
138
+
139
+ /**
140
+ * Bind disabled state
141
+ * @example bind: { disabled: 'isSubmitting' }
142
+ */
143
+ disabled?: string | (() => boolean);
144
+
145
+ /** Any additional attribute bindings */
146
+ [key: string]: unknown;
147
+ }
148
+
149
+ /**
150
+ * Component definition for ez.define()
151
+ */
152
+ export interface ComponentDefinition {
153
+ /**
154
+ * Base component type to extend
155
+ * @example eztype: 'EzComponent'
156
+ * @example eztype: 'EzForm'
157
+ */
158
+ eztype?: string;
159
+
160
+ /**
161
+ * CSS module name for automatic style loading
162
+ * The framework will load `{css}.module.scss` automatically
163
+ * @example css: 'StatCard' // loads StatCard.module.scss
164
+ */
165
+ css?: string;
166
+
167
+ /**
168
+ * Controller name for state management
169
+ * @example controller: 'UserDetail'
170
+ */
171
+ controller?: string;
172
+
173
+ /**
174
+ * CSS class(es) to apply to the root element
175
+ * @example cls: 'myComponent'
176
+ * @example cls: ['card', 'primary']
177
+ */
178
+ cls?: string | string[];
179
+
180
+ /**
181
+ * Layout direction for child items
182
+ * @example layout: 'hbox' // horizontal
183
+ * @example layout: 'vbox' // vertical
184
+ */
185
+ layout?: 'hbox' | 'vbox';
186
+
187
+ /**
188
+ * Inline styles for the root element
189
+ */
190
+ style?: Record<string, string | number>;
191
+
192
+ /**
193
+ * Static child items
194
+ * @example items: [{ eztype: 'EzButton', text: 'Click' }]
195
+ */
196
+ items?: ItemConfig[];
197
+
198
+ /**
199
+ * Template function for dynamic rendering
200
+ * Receives props and returns component configuration
201
+ * @example
202
+ * template({ name, age }) {
203
+ * return {
204
+ * eztype: 'div',
205
+ * items: [
206
+ * { eztype: 'span', text: name },
207
+ * { eztype: 'span', text: `Age: ${age}` }
208
+ * ]
209
+ * };
210
+ * }
211
+ */
212
+ template?: (props: Record<string, unknown>) => ItemConfig;
213
+
214
+ /**
215
+ * Data bindings for reactive updates
216
+ * @example bind: { data: 'users', visible: 'hasUsers' }
217
+ */
218
+ bind?: BindConfig;
219
+
220
+ /**
221
+ * Render function for each item when using bind.data
222
+ * @example
223
+ * itemRender: (user) => ({
224
+ * eztype: 'UserCard',
225
+ * props: { user }
226
+ * })
227
+ */
228
+ itemRender?: (item: unknown, index?: number) => ItemConfig;
229
+
230
+ /**
231
+ * Method name to call before rendering (async data loading)
232
+ * The method must exist on the controller
233
+ * @example onLoad: 'loadUserData'
234
+ */
235
+ onLoad?: string;
236
+
237
+ /**
238
+ * Component to show while onLoad is executing
239
+ * @example
240
+ * fallback: {
241
+ * eztype: 'EzSkeleton',
242
+ * variant: 'card'
243
+ * }
244
+ */
245
+ fallback?: ItemConfig;
246
+
247
+ /**
248
+ * Reference name for accessing this component via ez._refs
249
+ * @example ref: 'myGrid' // access as ez._refs.myGrid
250
+ */
251
+ ref?: string;
252
+
253
+ /**
254
+ * Flex grow value for the component
255
+ */
256
+ flex?: number;
257
+
258
+ /**
259
+ * Route parameters passed by the router
260
+ */
261
+ routeParams?: Record<string, string>;
262
+
263
+ /** Any additional properties */
264
+ [key: string]: unknown;
265
+ }
266
+
267
+ /**
268
+ * Controller definition for ez.defineController()
269
+ */
270
+ export interface ControllerDef {
271
+ /**
272
+ * Reactive state object - automatically wrapped with deepSignal
273
+ * Access in methods via this.state
274
+ * @example
275
+ * state: {
276
+ * users: [],
277
+ * loading: false,
278
+ * filters: { search: '', status: 'all' }
279
+ * }
280
+ */
281
+ state?: Record<string, unknown>;
282
+
283
+ /**
284
+ * Called once when controller is first initialized
285
+ * Use for initial data loading
286
+ * @example
287
+ * async onInit() {
288
+ * this.state.loading = true;
289
+ * this.state.users = await api.getUsers();
290
+ * this.state.loading = false;
291
+ * }
292
+ */
293
+ onInit?: () => void | Promise<void>;
294
+
295
+ /**
296
+ * Called when route changes (for view controllers)
297
+ * @param params.fullPath - Current route path
298
+ * @param params.chain - Remaining route chain for nested views
299
+ * @example
300
+ * async onRouteChange({ fullPath, chain }) {
301
+ * const userId = chain[0]?.params?.id;
302
+ * if (userId) await this.loadUser(userId);
303
+ * }
304
+ */
305
+ onRouteChange?: (params: { fullPath: string; chain: unknown[] }) => void | Promise<void>;
306
+
307
+ /**
308
+ * Called when controller is being destroyed
309
+ * Use for cleanup (event listeners, timers, etc.)
310
+ * @example
311
+ * onDestroy() {
312
+ * clearInterval(this.refreshTimer);
313
+ * }
314
+ */
315
+ onDestroy?: () => void;
316
+
317
+ /**
318
+ * Custom methods - access state via this.state
319
+ * @example
320
+ * async loadUsers() {
321
+ * this.state.users = await api.get('/users');
322
+ * },
323
+ * addUser(user) {
324
+ * this.state.users.push(user);
325
+ * }
326
+ */
327
+ [key: string]: unknown;
328
+ }