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,344 @@
1
+ /**
2
+ * EzStore - Abstract data store for components
3
+ *
4
+ * Handles remote data loading, pagination, filtering, and sorting.
5
+ * Can be shared between multiple components (EzGrid, EzDataView, etc.)
6
+ */
7
+
8
+ // ============================================================
9
+ // Types
10
+ // ============================================================
11
+
12
+ export interface EzStoreRemoteConfig {
13
+ api: string;
14
+ source?: {
15
+ dataPath?: string;
16
+ countPath?: string;
17
+ };
18
+ }
19
+
20
+ export interface EzStoreConfig {
21
+ remote?: string | EzStoreRemoteConfig;
22
+ model?: string;
23
+ pageSize?: number;
24
+ autoLoad?: boolean;
25
+ }
26
+
27
+ export interface EzStoreState {
28
+ data: unknown[];
29
+ loading: boolean;
30
+ error: Error | null;
31
+ page: number;
32
+ pageSize: number;
33
+ total: number;
34
+ filters: Record<string, unknown>;
35
+ sort: { field: string; order: 'asc' | 'desc' } | null;
36
+ }
37
+
38
+ export interface EzStorePageChangeEvent {
39
+ page: number;
40
+ pageSize: number;
41
+ }
42
+
43
+ export type EzStoreEventMap = {
44
+ datachange: unknown[];
45
+ loadingchange: boolean;
46
+ pagechange: EzStorePageChangeEvent;
47
+ filterchange: Record<string, unknown>;
48
+ sortchange: { field: string; order: 'asc' | 'desc' } | null;
49
+ error: Error;
50
+ };
51
+
52
+ export type EzStoreEventName = keyof EzStoreEventMap;
53
+
54
+ export interface EzStore {
55
+ readonly _isEzStore: true;
56
+ readonly config: EzStoreConfig;
57
+ readonly state: EzStoreState;
58
+
59
+ // State accessors
60
+ getState(): EzStoreState;
61
+ getData(): unknown[];
62
+ getTotalPages(): number;
63
+ isFirstPage(): boolean;
64
+ isLastPage(): boolean;
65
+
66
+ // Events
67
+ on<K extends EzStoreEventName>(event: K, fn: (payload: EzStoreEventMap[K]) => void): () => void;
68
+ off<K extends EzStoreEventName>(event: K, fn: (payload: EzStoreEventMap[K]) => void): void;
69
+ emit<K extends EzStoreEventName>(event: K, payload: EzStoreEventMap[K]): void;
70
+
71
+ // Data operations
72
+ load(params?: Record<string, unknown>): Promise<unknown[]>;
73
+ reload(): Promise<unknown[]>;
74
+ setData(data: unknown[], meta?: { total?: number }): void;
75
+
76
+ // Pagination
77
+ setPage(page: number): Promise<unknown[]> | undefined;
78
+ setPageSize(size: number): Promise<unknown[]> | undefined;
79
+ goToFirstPage(): Promise<unknown[]> | undefined;
80
+ goToLastPage(): Promise<unknown[]> | undefined;
81
+ goToPrevPage(): Promise<unknown[]> | undefined;
82
+ goToNextPage(): Promise<unknown[]> | undefined;
83
+
84
+ // Filtering & Sorting
85
+ setFilters(filters: Record<string, unknown>): Promise<unknown[]>;
86
+ setSort(field: string | null, order?: 'asc' | 'desc'): Promise<unknown[]>;
87
+ }
88
+
89
+ // ============================================================
90
+ // Declare global ez object
91
+ // ============================================================
92
+
93
+ declare const ez: {
94
+ _api: {
95
+ request(api: string, options?: { params?: Record<string, unknown> }): Promise<unknown>;
96
+ };
97
+ _loader: {
98
+ resolveModel(name: string): Promise<void>;
99
+ };
100
+ getModelSync(name: string): {
101
+ processAll(data: unknown[]): unknown[];
102
+ } | null;
103
+ };
104
+
105
+ // ============================================================
106
+ // Implementation
107
+ // ============================================================
108
+
109
+ type EventCallback = (payload: unknown) => void;
110
+ type ListenerMap = Record<string, EventCallback[]>;
111
+
112
+ function getNestedValue(obj: unknown, path: string): unknown {
113
+ if (!path) return obj;
114
+ return path.split('.').reduce((acc: unknown, key: string) => {
115
+ if (acc && typeof acc === 'object' && key in acc) {
116
+ return (acc as Record<string, unknown>)[key];
117
+ }
118
+ return undefined;
119
+ }, obj);
120
+ }
121
+
122
+ export function createEzStore(config: EzStoreConfig = {}): EzStore {
123
+ const listeners: ListenerMap = Object.create(null);
124
+
125
+ const state: EzStoreState = {
126
+ data: [],
127
+ loading: false,
128
+ error: null,
129
+ page: 1,
130
+ pageSize: config.pageSize || 25,
131
+ total: 0,
132
+ filters: {},
133
+ sort: null
134
+ };
135
+
136
+ function on<K extends EzStoreEventName>(
137
+ event: K,
138
+ fn: (payload: EzStoreEventMap[K]) => void
139
+ ): () => void {
140
+ (listeners[event] ??= []).push(fn as EventCallback);
141
+ return () => off(event, fn);
142
+ }
143
+
144
+ function off<K extends EzStoreEventName>(
145
+ event: K,
146
+ fn: (payload: EzStoreEventMap[K]) => void
147
+ ): void {
148
+ const eventListeners = listeners[event];
149
+ if (!eventListeners) return;
150
+ listeners[event] = eventListeners.filter(l => l !== fn);
151
+ }
152
+
153
+ function emit<K extends EzStoreEventName>(
154
+ event: K,
155
+ payload: EzStoreEventMap[K]
156
+ ): void {
157
+ const eventListeners = listeners[event];
158
+ if (!eventListeners) return;
159
+ for (const fn of eventListeners) {
160
+ fn(payload);
161
+ }
162
+ }
163
+
164
+ async function load(params: Record<string, unknown> = {}): Promise<unknown[]> {
165
+ const remote = config.remote;
166
+ if (!remote) {
167
+ console.warn('[EzStore] No remote config provided');
168
+ return [];
169
+ }
170
+
171
+ const api = typeof remote === 'string' ? remote : remote.api;
172
+ if (!api) {
173
+ console.warn('[EzStore] No API endpoint provided');
174
+ return [];
175
+ }
176
+
177
+ state.loading = true;
178
+ state.error = null;
179
+ emit('loadingchange', true);
180
+
181
+ try {
182
+ const requestParams: Record<string, unknown> = {
183
+ page: state.page,
184
+ pageSize: state.pageSize,
185
+ ...state.filters,
186
+ ...params
187
+ };
188
+
189
+ if (state.sort) {
190
+ requestParams.sort = state.sort.field;
191
+ requestParams.order = state.sort.order;
192
+ }
193
+
194
+ const result = await ez._api.request(api, { params: requestParams });
195
+
196
+ const source = typeof remote === 'object' ? remote.source : undefined;
197
+ const dataPath = source?.dataPath || 'data';
198
+ const countPath = source?.countPath || 'total';
199
+
200
+ let data = (getNestedValue(result, dataPath) as unknown[]) || [];
201
+ const total = getNestedValue(result, countPath);
202
+ state.total = typeof total === 'number' ? total : data.length;
203
+
204
+ if (config.model) {
205
+ await ez._loader.resolveModel(config.model);
206
+ const model = ez.getModelSync(config.model);
207
+ if (model) {
208
+ data = model.processAll(data);
209
+ }
210
+ }
211
+
212
+ state.data = data;
213
+ emit('datachange', state.data);
214
+
215
+ return state.data;
216
+ } catch (err) {
217
+ const error = err instanceof Error ? err : new Error(String(err));
218
+ state.error = error;
219
+ emit('error', error);
220
+ console.error('[EzStore] Load error:', error);
221
+ throw error;
222
+ } finally {
223
+ state.loading = false;
224
+ emit('loadingchange', false);
225
+ }
226
+ }
227
+
228
+ function reload(): Promise<unknown[]> {
229
+ return load();
230
+ }
231
+
232
+ function setPage(page: number): Promise<unknown[]> | undefined {
233
+ const next = Number(page);
234
+ if (!Number.isInteger(next) || next < 1) return;
235
+ if (state.page === next) return;
236
+
237
+ state.page = next;
238
+ emit('pagechange', { page: next, pageSize: state.pageSize });
239
+ return load();
240
+ }
241
+
242
+ function setPageSize(size: number): Promise<unknown[]> | undefined {
243
+ const next = Number(size);
244
+ if (!Number.isInteger(next) || next < 1) return;
245
+ if (state.pageSize === next) return;
246
+
247
+ state.pageSize = next;
248
+ state.page = 1;
249
+ emit('pagechange', { page: 1, pageSize: next });
250
+ return load();
251
+ }
252
+
253
+ function setFilters(filters: Record<string, unknown>): Promise<unknown[]> {
254
+ state.filters = filters || {};
255
+ state.page = 1;
256
+ emit('filterchange', state.filters);
257
+ return load();
258
+ }
259
+
260
+ function setSort(field: string | null, order: 'asc' | 'desc' = 'asc'): Promise<unknown[]> {
261
+ if (field === null) {
262
+ state.sort = null;
263
+ } else {
264
+ state.sort = { field, order };
265
+ }
266
+ emit('sortchange', state.sort);
267
+ return load();
268
+ }
269
+
270
+ function setData(data: unknown[], meta: { total?: number } = {}): void {
271
+ state.data = Array.isArray(data) ? data : [];
272
+ state.total = meta.total ?? state.data.length;
273
+ emit('datachange', state.data);
274
+ }
275
+
276
+ function getData(): unknown[] {
277
+ return state.data;
278
+ }
279
+
280
+ function getState(): EzStoreState {
281
+ return { ...state };
282
+ }
283
+
284
+ function getTotalPages(): number {
285
+ return Math.max(1, Math.ceil(state.total / state.pageSize));
286
+ }
287
+
288
+ function isFirstPage(): boolean {
289
+ return state.page <= 1;
290
+ }
291
+
292
+ function isLastPage(): boolean {
293
+ return state.page >= getTotalPages();
294
+ }
295
+
296
+ function goToFirstPage(): Promise<unknown[]> | undefined {
297
+ return setPage(1);
298
+ }
299
+
300
+ function goToLastPage(): Promise<unknown[]> | undefined {
301
+ return setPage(getTotalPages());
302
+ }
303
+
304
+ function goToPrevPage(): Promise<unknown[]> | undefined {
305
+ return setPage(state.page - 1);
306
+ }
307
+
308
+ function goToNextPage(): Promise<unknown[]> | undefined {
309
+ return setPage(state.page + 1);
310
+ }
311
+
312
+ const store: EzStore = {
313
+ _isEzStore: true,
314
+ config,
315
+ state,
316
+ getState,
317
+ getData,
318
+ getTotalPages,
319
+ isFirstPage,
320
+ isLastPage,
321
+ on,
322
+ off,
323
+ emit,
324
+ load,
325
+ reload,
326
+ setData,
327
+ setPage,
328
+ setPageSize,
329
+ goToFirstPage,
330
+ goToLastPage,
331
+ goToPrevPage,
332
+ goToNextPage,
333
+ setFilters,
334
+ setSort
335
+ };
336
+
337
+ if (config.autoLoad !== false && config.remote) {
338
+ queueMicrotask(() => { void load(); });
339
+ }
340
+
341
+ return store;
342
+ }
343
+
344
+ export default createEzStore;
@@ -0,0 +1,164 @@
1
+ .switch {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 10px;
5
+ cursor: pointer;
6
+ user-select: none;
7
+ }
8
+
9
+ .labelLeft {
10
+ flex-direction: row-reverse;
11
+ }
12
+
13
+ .input {
14
+ position: absolute;
15
+ opacity: 0;
16
+ width: 0;
17
+ height: 0;
18
+ pointer-events: none;
19
+ }
20
+
21
+ .track {
22
+ position: relative;
23
+ display: inline-flex;
24
+ align-items: center;
25
+ box-sizing: border-box;
26
+
27
+ width: 44px;
28
+ height: 24px;
29
+ padding: 2px;
30
+
31
+ background: var(--ez-border, #cbd5e1);
32
+ border-radius: 12px;
33
+
34
+ transition: background 0.2s ease;
35
+ }
36
+
37
+ .thumb {
38
+ position: absolute;
39
+ left: 2px;
40
+
41
+ width: 20px;
42
+ height: 20px;
43
+
44
+ background: #ffffff;
45
+ border-radius: 50%;
46
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
47
+
48
+ transition: transform 0.2s ease;
49
+ }
50
+
51
+ // Checked state
52
+ .input:checked + .track {
53
+ background: var(--ez-primary, #2563eb);
54
+
55
+ .thumb {
56
+ transform: translateX(20px);
57
+ }
58
+ }
59
+
60
+ // Focus state
61
+ .input:focus-visible + .track {
62
+ box-shadow: 0 0 0 2px var(--ez-primary-light, rgba(37, 99, 235, 0.3));
63
+ }
64
+
65
+ // Hover state
66
+ .switch:hover:not(.disabled) .track {
67
+ background: var(--ez-text-tertiary, #94a3b8);
68
+ }
69
+
70
+ .switch:hover:not(.disabled) .input:checked + .track {
71
+ background: var(--ez-primary-dark, #1d4ed8);
72
+ }
73
+
74
+ .label {
75
+ font-size: 14px;
76
+ font-weight: 500;
77
+ color: var(--ez-text-primary, #1e293b);
78
+ }
79
+
80
+ // Disabled state
81
+ .disabled {
82
+ cursor: not-allowed;
83
+ pointer-events: none;
84
+ opacity: 0.5;
85
+
86
+ .track {
87
+ background: var(--ez-surface-primary, #e2e8f0);
88
+ }
89
+
90
+ .input:checked + .track {
91
+ background: var(--ez-primary, #2563eb);
92
+ opacity: 0.5;
93
+ }
94
+
95
+ .label {
96
+ color: var(--ez-text-tertiary, #94a3b8);
97
+ }
98
+ }
99
+
100
+ // Sizes
101
+ .sm {
102
+ .track {
103
+ width: 36px;
104
+ height: 20px;
105
+ border-radius: 10px;
106
+ }
107
+
108
+ .thumb {
109
+ width: 16px;
110
+ height: 16px;
111
+ }
112
+
113
+ .input:checked + .track .thumb {
114
+ transform: translateX(16px);
115
+ }
116
+
117
+ .label {
118
+ font-size: 13px;
119
+ }
120
+ }
121
+
122
+ .lg {
123
+ .track {
124
+ width: 52px;
125
+ height: 28px;
126
+ border-radius: 14px;
127
+ padding: 3px;
128
+ }
129
+
130
+ .thumb {
131
+ width: 22px;
132
+ height: 22px;
133
+ }
134
+
135
+ .input:checked + .track .thumb {
136
+ transform: translateX(24px);
137
+ }
138
+
139
+ .label {
140
+ font-size: 15px;
141
+ }
142
+ }
143
+
144
+ // Error state
145
+ .hasError {
146
+ .track {
147
+ background: var(--ez-danger-light, rgba(220, 38, 38, 0.2));
148
+ }
149
+
150
+ .input:checked + .track {
151
+ background: var(--ez-danger, #dc2626);
152
+ }
153
+ }
154
+
155
+ .fieldError {
156
+ display: none;
157
+ font-size: 12px;
158
+ color: var(--ez-danger, #dc2626);
159
+ margin-left: auto;
160
+ }
161
+
162
+ .fieldErrorVisible {
163
+ display: block;
164
+ }
@@ -0,0 +1,117 @@
1
+ import styles from './EzSwitch.module.scss';
2
+ import { cx } from '../../utils/cssModules.js';
3
+ import { EzBaseComponent, EzBaseComponentConfig } from '../EzBaseComponent.js';
4
+
5
+ const cls = cx(styles);
6
+
7
+ export interface EzSwitchConfig extends EzBaseComponentConfig {
8
+ size?: 'sm' | 'lg';
9
+ disabled?: boolean;
10
+ value?: boolean;
11
+ name?: string;
12
+ formData?: string;
13
+ label?: string;
14
+ labelPosition?: 'left' | 'right';
15
+ }
16
+
17
+ export class EzSwitch extends EzBaseComponent {
18
+ declare config: EzSwitchConfig;
19
+
20
+ private _wrapper: HTMLLabelElement | null = null;
21
+ private _input: HTMLInputElement | null = null;
22
+ private _error: HTMLDivElement | null = null;
23
+
24
+ render(): HTMLLabelElement {
25
+ const cfg = this.config;
26
+
27
+ const wrapper = document.createElement('label');
28
+ wrapper.className = cls(
29
+ 'switch',
30
+ cfg.size,
31
+ cfg.disabled && 'disabled',
32
+ cfg.labelPosition === 'left' && 'labelLeft'
33
+ );
34
+
35
+ const input = document.createElement('input');
36
+ input.type = 'checkbox';
37
+ input.className = cls('input');
38
+ input.checked = !!cfg.value;
39
+
40
+ if (cfg.disabled) input.disabled = true;
41
+ if (cfg.name) input.name = cfg.name;
42
+
43
+ if (cfg.name && cfg.formData) {
44
+ this.config.bind = `${cfg.formData}.${cfg.name}`;
45
+ }
46
+
47
+ const track = document.createElement('span');
48
+ track.className = cls('track');
49
+
50
+ const thumb = document.createElement('span');
51
+ thumb.className = cls('thumb');
52
+ track.appendChild(thumb);
53
+
54
+ this.applyCommonBindings(input);
55
+
56
+ const onChange = this._createOnChangeHandler();
57
+
58
+ input.addEventListener('change', e => {
59
+ if (onChange) {
60
+ onChange((e.target as HTMLInputElement).checked);
61
+ }
62
+ });
63
+
64
+ wrapper.appendChild(input);
65
+ wrapper.appendChild(track);
66
+
67
+ if (cfg.label) {
68
+ const label = document.createElement('span');
69
+ label.className = cls('label');
70
+ label.textContent = cfg.label;
71
+ wrapper.appendChild(label);
72
+ }
73
+
74
+ const error = document.createElement('div');
75
+ error.className = cls('fieldError');
76
+ wrapper.appendChild(error);
77
+
78
+ this._wrapper = wrapper;
79
+ this._input = input;
80
+ this._error = error;
81
+
82
+ return wrapper;
83
+ }
84
+
85
+ getValue(): boolean {
86
+ return this._input?.checked ?? false;
87
+ }
88
+
89
+ setValue(value: boolean): void {
90
+ if (this._input) {
91
+ this._input.checked = !!value;
92
+ }
93
+ }
94
+
95
+ toggle(): void {
96
+ if (this._input && !this._input.disabled) {
97
+ this._input.checked = !this._input.checked;
98
+ this._input.dispatchEvent(new Event('change', { bubbles: true }));
99
+ }
100
+ }
101
+
102
+ showError(message: string): void {
103
+ if (this._wrapper && this._error) {
104
+ this._wrapper.classList.add(cls('hasError'));
105
+ this._error.classList.add(cls('fieldErrorVisible'));
106
+ this._error.textContent = message;
107
+ }
108
+ }
109
+
110
+ clearError(): void {
111
+ if (this._wrapper && this._error) {
112
+ this._wrapper.classList.remove(cls('hasError'));
113
+ this._error.classList.remove(cls('fieldErrorVisible'));
114
+ this._error.textContent = '';
115
+ }
116
+ }
117
+ }