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,181 @@
1
+ // ==========================================================
2
+ // EzTabPanel - CSS Module
3
+ // ==========================================================
4
+
5
+ .tabPanel {
6
+ display: flex;
7
+ flex-direction: column;
8
+ flex: 1;
9
+ overflow: hidden;
10
+ }
11
+
12
+ // ----------------------------------------------------------
13
+ // Tab Header
14
+ // ----------------------------------------------------------
15
+ .tabHeader {
16
+ display: flex;
17
+ gap: 0;
18
+ background: var(--ez-surface-tertiary, #f8fafc);
19
+ border-bottom: 1px solid var(--ez-border-primary, #e2e8f0);
20
+ padding: 0 8px;
21
+ }
22
+
23
+ .tab {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ gap: 8px;
27
+ padding: 12px 16px;
28
+ font-family: inherit;
29
+ font-size: 13px;
30
+ font-weight: 500;
31
+ color: var(--ez-text-secondary, #64748b);
32
+ background: transparent;
33
+ border: none;
34
+ border-bottom: 2px solid transparent;
35
+ margin-bottom: -1px;
36
+ cursor: pointer;
37
+ transition: all 0.15s ease;
38
+ white-space: nowrap;
39
+
40
+ i {
41
+ font-size: 14px;
42
+ }
43
+
44
+ &:hover {
45
+ color: var(--ez-text-primary, #1e293b);
46
+ background: var(--ez-surface-primary, #f1f5f9);
47
+ }
48
+ }
49
+
50
+ .tabClose {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ width: 18px;
55
+ height: 18px;
56
+ margin-left: 4px;
57
+ border-radius: 4px;
58
+ color: var(--ez-text-tertiary, #94a3b8);
59
+ transition: all 0.15s ease;
60
+
61
+ i {
62
+ font-size: 10px;
63
+ }
64
+
65
+ &:hover {
66
+ background: var(--ez-danger-light, rgba(239, 68, 68, 0.1));
67
+ color: var(--ez-danger, #dc2626);
68
+ }
69
+ }
70
+
71
+ .tabActive {
72
+ color: var(--ez-primary, #2563eb);
73
+ border-bottom-color: var(--ez-primary, #2563eb);
74
+ background: transparent;
75
+
76
+ &:hover {
77
+ background: transparent;
78
+ }
79
+ }
80
+
81
+ // ----------------------------------------------------------
82
+ // Tab Content
83
+ // ----------------------------------------------------------
84
+ .tabContent {
85
+ flex: 1;
86
+ display: flex;
87
+ flex-direction: column;
88
+ overflow: hidden;
89
+ position: relative;
90
+ }
91
+
92
+ .tabPane {
93
+ display: none;
94
+ flex: 1;
95
+ flex-direction: column;
96
+ overflow: auto;
97
+ }
98
+
99
+ .tabPaneActive {
100
+ display: flex;
101
+ }
102
+
103
+ // ----------------------------------------------------------
104
+ // Variants
105
+ // ----------------------------------------------------------
106
+
107
+ // Boxed variant - tabs with background
108
+ .boxed {
109
+ .tabHeader {
110
+ background: var(--ez-surface-secondary, #ffffff);
111
+ padding: 8px 8px 0;
112
+ gap: 4px;
113
+ }
114
+
115
+ .tab {
116
+ background: var(--ez-surface-primary, #f1f5f9);
117
+ border-radius: var(--ez-radius-md, 6px) var(--ez-radius-md, 6px) 0 0;
118
+ border-bottom: none;
119
+ margin-bottom: 0;
120
+ }
121
+
122
+ .tabActive {
123
+ background: var(--ez-surface-secondary, #ffffff);
124
+ border: 1px solid var(--ez-border-primary, #e2e8f0);
125
+ border-bottom-color: var(--ez-surface-secondary, #ffffff);
126
+ margin-bottom: -1px;
127
+ }
128
+
129
+ .tabContent {
130
+ background: var(--ez-surface-secondary, #ffffff);
131
+ border: 1px solid var(--ez-border-primary, #e2e8f0);
132
+ border-top: none;
133
+ }
134
+ }
135
+
136
+ // Pill variant - rounded pill tabs
137
+ .pills {
138
+ .tabHeader {
139
+ background: transparent;
140
+ border-bottom: none;
141
+ padding: 8px;
142
+ gap: 8px;
143
+ }
144
+
145
+ .tab {
146
+ background: var(--ez-surface-primary, #f1f5f9);
147
+ border-radius: var(--ez-radius-full, 20px);
148
+ border-bottom: none;
149
+ margin-bottom: 0;
150
+ padding: 8px 16px;
151
+ }
152
+
153
+ .tabActive {
154
+ background: var(--ez-primary, #2563eb);
155
+ color: var(--ez-text-inverse, #ffffff);
156
+ }
157
+ }
158
+
159
+ // Minimal variant - just underline
160
+ .minimal {
161
+ .tabHeader {
162
+ background: transparent;
163
+ padding: 0;
164
+ }
165
+
166
+ .tab {
167
+ padding: 8px 12px;
168
+ }
169
+ }
170
+
171
+ // Compact variant - smaller tabs
172
+ .compact {
173
+ .tab {
174
+ padding: 8px 12px;
175
+ font-size: 12px;
176
+
177
+ i {
178
+ font-size: 12px;
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,402 @@
1
+ import styles from './EzTabPanel.module.scss';
2
+ import { cx } from '../../utils/cssModules.js';
3
+ import { EzBaseComponent, EzBaseComponentConfig } from '../EzBaseComponent.js';
4
+
5
+ declare const ez: {
6
+ _createElement(config: unknown, controller?: string | null, parent?: unknown): Promise<HTMLElement>;
7
+ };
8
+
9
+ const cls = cx(styles);
10
+
11
+ interface TabConfig {
12
+ id?: string | number;
13
+ title?: string;
14
+ icon?: string;
15
+ closable?: boolean;
16
+ items?: unknown[];
17
+ restoreData?: unknown;
18
+ }
19
+
20
+ interface SavedTabState {
21
+ id: string | number;
22
+ title?: string;
23
+ icon?: string;
24
+ closable?: boolean;
25
+ restoreData?: unknown;
26
+ }
27
+
28
+ interface SavedState {
29
+ activeTab: string | number | null;
30
+ dynamicTabs: SavedTabState[];
31
+ }
32
+
33
+ export interface EzTabPanelConfig extends EzBaseComponentConfig {
34
+ id?: string;
35
+ tabs?: TabConfig[];
36
+ activeTab?: string | number;
37
+ variant?: string;
38
+ stateful?: boolean;
39
+ statefulPersist?: boolean;
40
+ onTabChange?: (tabId: string | number) => void;
41
+ onTabAdd?: (tabId: string | number, config: TabConfig) => void;
42
+ onTabClose?: (tabId: string | number) => void;
43
+ onTabRestore?: (tabData: SavedTabState, tabPanel: EzTabPanel) => Promise<void>;
44
+ }
45
+
46
+ export class EzTabPanel extends EzBaseComponent {
47
+ declare config: EzTabPanelConfig;
48
+ declare el: HTMLElement;
49
+
50
+ private _activeTabId: string | number | null = null;
51
+ private _tabElements: Map<string | number, HTMLElement> = new Map();
52
+ private _contentElements: Map<string | number, HTMLElement> = new Map();
53
+ private _tabConfigs: Map<string | number, TabConfig> = new Map();
54
+ private _headerEl: HTMLElement | null = null;
55
+ private _contentEl: HTMLElement | null = null;
56
+ private _dynamicTabs: SavedTabState[] = [];
57
+ private _isRestoring: boolean = false;
58
+
59
+ stateful: boolean;
60
+ statefulPersist: boolean;
61
+
62
+ constructor(config: EzTabPanelConfig = {}) {
63
+ super(config);
64
+ this.config = config;
65
+
66
+ this.stateful = config.stateful || false;
67
+ this.statefulPersist = config.statefulPersist || false;
68
+ }
69
+
70
+ private _getStorageKey(): string | null {
71
+ if (!this.config.id) return null;
72
+ return `ez-tabpanel-${this.config.id}`;
73
+ }
74
+
75
+ private _saveState(): void {
76
+ if (!this.stateful || this._isRestoring) return;
77
+
78
+ const key = this._getStorageKey();
79
+ if (!key) return;
80
+
81
+ const state: SavedState = {
82
+ activeTab: this._activeTabId,
83
+ dynamicTabs: this._dynamicTabs.map(tab => ({
84
+ id: tab.id,
85
+ title: tab.title,
86
+ icon: tab.icon,
87
+ closable: tab.closable,
88
+ restoreData: tab.restoreData || null
89
+ }))
90
+ };
91
+
92
+ const storage = this.statefulPersist ? localStorage : sessionStorage;
93
+ storage.setItem(key, JSON.stringify(state));
94
+ }
95
+
96
+ private _loadState(): SavedState | null {
97
+ if (!this.stateful) return null;
98
+
99
+ const key = this._getStorageKey();
100
+ if (!key) return null;
101
+
102
+ const storage = this.statefulPersist ? localStorage : sessionStorage;
103
+ const data = storage.getItem(key);
104
+
105
+ if (!data) return null;
106
+
107
+ try {
108
+ return JSON.parse(data);
109
+ } catch {
110
+ console.warn('[EzTabPanel] Failed to parse saved state');
111
+ return null;
112
+ }
113
+ }
114
+
115
+ private async _restoreState(state: SavedState): Promise<void> {
116
+ if (!state?.dynamicTabs?.length) return;
117
+
118
+ this._isRestoring = true;
119
+
120
+ for (const tabData of state.dynamicTabs) {
121
+ if (this._tabConfigs.has(tabData.id)) continue;
122
+
123
+ if (this.config.onTabRestore) {
124
+ await this.config.onTabRestore(tabData, this);
125
+ }
126
+ }
127
+
128
+ this._isRestoring = false;
129
+
130
+ if (state.activeTab && this._tabConfigs.has(state.activeTab)) {
131
+ this._setActiveTabInternal(state.activeTab);
132
+ }
133
+ }
134
+
135
+ async render(): Promise<HTMLElement> {
136
+ const cfg = this.config;
137
+ const el = document.createElement('div');
138
+
139
+ el.classList.add(cls('tabPanel'));
140
+
141
+ if (cfg.variant) {
142
+ el.classList.add(cls(cfg.variant));
143
+ }
144
+
145
+ this._headerEl = document.createElement('div');
146
+ this._headerEl.classList.add(cls('tabHeader'));
147
+
148
+ this._contentEl = document.createElement('div');
149
+ this._contentEl.classList.add(cls('tabContent'));
150
+
151
+ const tabs = cfg.tabs || [];
152
+
153
+ let activeTabId: string | number | undefined = cfg.activeTab;
154
+ if (activeTabId === undefined && tabs.length > 0) {
155
+ activeTabId = tabs[0].id ?? 0;
156
+ }
157
+
158
+ for (let i = 0; i < tabs.length; i++) {
159
+ const tab = tabs[i];
160
+ await this._createTab(tab, i);
161
+ }
162
+
163
+ el.appendChild(this._headerEl);
164
+ el.appendChild(this._contentEl);
165
+
166
+ this.applyStyles(el);
167
+ this.el = el;
168
+
169
+ if (activeTabId != null) {
170
+ this._setActiveTabInternal(activeTabId);
171
+ }
172
+
173
+ const savedState = this._loadState();
174
+ if (savedState) {
175
+ await this._restoreState(savedState);
176
+ }
177
+
178
+ return el;
179
+ }
180
+
181
+ private async _createTab(tab: TabConfig, index: number): Promise<string | number> {
182
+ const tabId = tab.id ?? index;
183
+
184
+ this._tabConfigs.set(tabId, tab);
185
+
186
+ const tabBtn = document.createElement('button');
187
+ tabBtn.classList.add(cls('tab'));
188
+ tabBtn.setAttribute('data-tab-id', String(tabId));
189
+
190
+ if (tab.icon) {
191
+ const icon = document.createElement('i');
192
+ icon.className = tab.icon;
193
+ tabBtn.appendChild(icon);
194
+ }
195
+
196
+ if (tab.title) {
197
+ const span = document.createElement('span');
198
+ span.textContent = tab.title;
199
+ tabBtn.appendChild(span);
200
+ }
201
+
202
+ if (tab.closable) {
203
+ const closeBtn = document.createElement('span');
204
+ closeBtn.classList.add(cls('tabClose'));
205
+ closeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
206
+ closeBtn.addEventListener('click', (e) => {
207
+ e.stopPropagation();
208
+ this.removeTab(tabId);
209
+ });
210
+ tabBtn.appendChild(closeBtn);
211
+ }
212
+
213
+ tabBtn.addEventListener('click', () => this.setActiveTab(tabId));
214
+ this._headerEl!.appendChild(tabBtn);
215
+ this._tabElements.set(tabId, tabBtn);
216
+
217
+ const pane = document.createElement('div');
218
+ pane.classList.add(cls('tabPane'));
219
+ pane.setAttribute('data-tab-id', String(tabId));
220
+
221
+ if (Array.isArray(tab.items)) {
222
+ for (const item of tab.items) {
223
+ const child = await ez._createElement(item, this.config.controller || null, this);
224
+ pane.appendChild(child);
225
+ }
226
+ }
227
+
228
+ this._contentEl!.appendChild(pane);
229
+ this._contentElements.set(tabId, pane);
230
+
231
+ return tabId;
232
+ }
233
+
234
+ async addTab(tabConfig: TabConfig, activate: boolean = true): Promise<string | number> {
235
+ const index = this._tabConfigs.size;
236
+ const tabId = tabConfig.id ?? `tab-${Date.now()}`;
237
+
238
+ if (this._tabConfigs.has(tabId)) {
239
+ console.warn(`[EzTabPanel] Tab with id "${tabId}" already exists`);
240
+ if (activate) {
241
+ this.setActiveTab(tabId);
242
+ }
243
+ return tabId;
244
+ }
245
+
246
+ await this._createTab({ ...tabConfig, id: tabId }, index);
247
+
248
+ if (tabConfig.closable) {
249
+ this._dynamicTabs.push({
250
+ id: tabId,
251
+ title: tabConfig.title,
252
+ icon: tabConfig.icon,
253
+ closable: true,
254
+ restoreData: tabConfig.restoreData || null
255
+ });
256
+ this._saveState();
257
+ }
258
+
259
+ if (activate) {
260
+ this.setActiveTab(tabId);
261
+ }
262
+
263
+ if (this.config.onTabAdd) {
264
+ this.config.onTabAdd(tabId, tabConfig);
265
+ }
266
+
267
+ return tabId;
268
+ }
269
+
270
+ removeTab(tabId: string | number): boolean {
271
+ const tabBtn = this._tabElements.get(tabId);
272
+ const pane = this._contentElements.get(tabId);
273
+
274
+ if (!tabBtn || !pane) {
275
+ console.warn(`[EzTabPanel] Tab "${tabId}" not found`);
276
+ return false;
277
+ }
278
+
279
+ if (this._activeTabId === tabId) {
280
+ const nextTabId = this._findNextTab(tabId);
281
+
282
+ if (nextTabId != null) {
283
+ this.setActiveTab(nextTabId);
284
+ } else {
285
+ this._activeTabId = null;
286
+ }
287
+ }
288
+
289
+ tabBtn.remove();
290
+ pane.remove();
291
+
292
+ this._tabElements.delete(tabId);
293
+ this._contentElements.delete(tabId);
294
+ this._tabConfigs.delete(tabId);
295
+
296
+ const dynamicIndex = this._dynamicTabs.findIndex(t => t.id === tabId);
297
+ if (dynamicIndex !== -1) {
298
+ this._dynamicTabs.splice(dynamicIndex, 1);
299
+ this._saveState();
300
+ }
301
+
302
+ if (this.config.onTabClose) {
303
+ this.config.onTabClose(tabId);
304
+ }
305
+
306
+ return true;
307
+ }
308
+
309
+ hasTab(tabId: string | number): boolean {
310
+ return this._tabConfigs.has(tabId);
311
+ }
312
+
313
+ getTab(tabId: string | number): TabConfig | null {
314
+ return this._tabConfigs.get(tabId) || null;
315
+ }
316
+
317
+ getTabIds(): (string | number)[] {
318
+ return Array.from(this._tabConfigs.keys());
319
+ }
320
+
321
+ setActiveTab(tabId: string | number): void {
322
+ this._setActiveTabInternal(tabId);
323
+ this._saveState();
324
+
325
+ if (this.config.onTabChange) {
326
+ this.config.onTabChange(tabId);
327
+ }
328
+ }
329
+
330
+ getActiveTab(): string | number | null {
331
+ return this._activeTabId;
332
+ }
333
+
334
+ private _findNextTab(closingTabId: string | number): string | number | null {
335
+ const tabIds = Array.from(this._tabConfigs.keys());
336
+ const currentIndex = tabIds.indexOf(closingTabId);
337
+
338
+ if (tabIds.length <= 1) return null;
339
+
340
+ interface Candidate {
341
+ id: string | number;
342
+ index: number;
343
+ isClosable: boolean;
344
+ }
345
+
346
+ const candidates: Candidate[] = [];
347
+ for (const id of tabIds) {
348
+ if (id === closingTabId) continue;
349
+ const config = this._tabConfigs.get(id);
350
+ const isClosable = config?.closable === true;
351
+ const index = tabIds.indexOf(id);
352
+ candidates.push({ id, index, isClosable });
353
+ }
354
+
355
+ if (candidates.length === 0) return null;
356
+
357
+ candidates.sort((a, b) => {
358
+ if (a.isClosable !== b.isClosable) {
359
+ return a.isClosable ? 1 : -1;
360
+ }
361
+
362
+ const distA = Math.abs(a.index - currentIndex);
363
+ const distB = Math.abs(b.index - currentIndex);
364
+ if (distA !== distB) return distA - distB;
365
+
366
+ return a.index - b.index;
367
+ });
368
+
369
+ return candidates[0].id;
370
+ }
371
+
372
+ private _setActiveTabInternal(tabId: string | number): void {
373
+ if (this._activeTabId != null) {
374
+ const prevTab = this._tabElements.get(this._activeTabId);
375
+ const prevPane = this._contentElements.get(this._activeTabId);
376
+ prevTab?.classList.remove(cls('tabActive'));
377
+ prevPane?.classList.remove(cls('tabPaneActive'));
378
+ }
379
+
380
+ const newTab = this._tabElements.get(tabId);
381
+ const newPane = this._contentElements.get(tabId);
382
+ newTab?.classList.add(cls('tabActive'));
383
+ newPane?.classList.add(cls('tabPaneActive'));
384
+
385
+ this._activeTabId = tabId;
386
+ }
387
+
388
+ setTabTitle(tabId: string | number, title: string): void {
389
+ const tabBtn = this._tabElements.get(tabId);
390
+ if (tabBtn) {
391
+ const span = tabBtn.querySelector(`span:not(.${cls('tabClose')})`);
392
+ if (span) {
393
+ span.textContent = title;
394
+ }
395
+ }
396
+
397
+ const config = this._tabConfigs.get(tabId);
398
+ if (config) {
399
+ config.title = title;
400
+ }
401
+ }
402
+ }
@@ -0,0 +1,131 @@
1
+ .textareaGroup {
2
+ position: relative;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 4px;
6
+ min-width: 0;
7
+ }
8
+
9
+ .textarea {
10
+ box-sizing: border-box;
11
+ width: 100%;
12
+ min-height: 80px;
13
+ padding: 10px 12px;
14
+
15
+ font-family: inherit;
16
+ font-size: 14px;
17
+ line-height: 1.5;
18
+ color: var(--ez-input-text, #1e293b);
19
+
20
+ background: var(--ez-input-bg, #ffffff);
21
+ border: 1px solid var(--ez-input-border, #e2e8f0);
22
+ border-radius: var(--ez-radius-md, 6px);
23
+
24
+ resize: vertical;
25
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
26
+
27
+ &::placeholder {
28
+ color: var(--ez-input-placeholder, #94a3b8);
29
+ }
30
+
31
+ &:hover:not(:disabled) {
32
+ border-color: var(--ez-text-tertiary, #94a3b8);
33
+ }
34
+
35
+ &:focus {
36
+ outline: none;
37
+ border-color: var(--ez-primary, #2563eb);
38
+ box-shadow: 0 0 0 2px var(--ez-primary-light, rgba(37, 99, 235, 0.15));
39
+ }
40
+ }
41
+
42
+ .hasRows .textarea {
43
+ min-height: auto;
44
+ }
45
+
46
+ .noResize {
47
+ .textarea {
48
+ resize: none;
49
+ }
50
+ }
51
+
52
+ .counter {
53
+ align-self: flex-end;
54
+ font-size: 12px;
55
+ color: var(--ez-text-tertiary, #94a3b8);
56
+ }
57
+
58
+ .counterWarning {
59
+ color: var(--ez-warning, #f59e0b);
60
+ }
61
+
62
+ .counterError {
63
+ color: var(--ez-danger, #dc2626);
64
+ }
65
+
66
+ // Disabled state
67
+ .disabled {
68
+ .textarea {
69
+ background: var(--ez-surface-primary, #f8fafc);
70
+ color: var(--ez-text-tertiary, #94a3b8);
71
+ cursor: not-allowed;
72
+ resize: none;
73
+
74
+ &:hover {
75
+ border-color: var(--ez-input-border, #e2e8f0);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Sizes
81
+ .sm {
82
+ .textarea {
83
+ min-height: 60px;
84
+ padding: 8px 10px;
85
+ font-size: 13px;
86
+ }
87
+
88
+ .counter {
89
+ font-size: 11px;
90
+ }
91
+ }
92
+
93
+ .lg {
94
+ .textarea {
95
+ min-height: 120px;
96
+ padding: 12px 14px;
97
+ font-size: 15px;
98
+ }
99
+
100
+ .counter {
101
+ font-size: 13px;
102
+ }
103
+ }
104
+
105
+ // Override min-height for sizes when rows is set
106
+ .hasRows.sm .textarea,
107
+ .hasRows.lg .textarea {
108
+ min-height: auto;
109
+ }
110
+
111
+ // Error state
112
+ .hasError {
113
+ .textarea {
114
+ border-color: var(--ez-danger, #dc2626);
115
+
116
+ &:focus {
117
+ border-color: var(--ez-danger, #dc2626);
118
+ box-shadow: 0 0 0 2px var(--ez-danger-light, rgba(220, 38, 38, 0.15));
119
+ }
120
+ }
121
+ }
122
+
123
+ .fieldError {
124
+ display: none;
125
+ font-size: 12px;
126
+ color: var(--ez-danger, #dc2626);
127
+ }
128
+
129
+ .fieldErrorVisible {
130
+ display: block;
131
+ }