@panyam/tsappkit 0.0.3
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.
- package/README.md +98 -0
- package/dist/index.d.mts +866 -0
- package/dist/index.d.ts +866 -0
- package/dist/index.js +1736 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1718 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/src/BasePage.ts +270 -0
- package/src/Component.ts +178 -0
- package/src/DOMUtils.ts +72 -0
- package/src/EventBus.ts +272 -0
- package/src/KeyboardShortcutManager.ts +435 -0
- package/src/LCMComponent.ts +140 -0
- package/src/LifecycleController.ts +213 -0
- package/src/MobileBottomDrawer.ts +236 -0
- package/src/Modal.ts +233 -0
- package/src/SplashScreen.ts +111 -0
- package/src/TemplateLoader.ts +101 -0
- package/src/ThemeManager.ts +100 -0
- package/src/ToastManager.ts +161 -0
- package/src/events.ts +5 -0
- package/src/index.ts +39 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic keyboard shortcut manager for handling multi-key commands
|
|
3
|
+
* across all application pages
|
|
4
|
+
*
|
|
5
|
+
* Current Behavior (default):
|
|
6
|
+
* - User types 'c3' → visual indicator → Enter/timeout → execute
|
|
7
|
+
* - Requires explicit confirmation for execution
|
|
8
|
+
*
|
|
9
|
+
* Future Immediate Mode (when enabled):
|
|
10
|
+
* - User types 'c3' → immediate preview → Escape to cancel
|
|
11
|
+
* - Provides instant feedback with option to cancel
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isInInputContext } from './DOMUtils';
|
|
15
|
+
|
|
16
|
+
export interface ShortcutConfig {
|
|
17
|
+
key: string;
|
|
18
|
+
handler: (args?: string) => void;
|
|
19
|
+
description: string;
|
|
20
|
+
category?: string;
|
|
21
|
+
requiresArgs?: boolean;
|
|
22
|
+
argType?: 'number' | 'string';
|
|
23
|
+
contextFilter?: (event: KeyboardEvent) => boolean;
|
|
24
|
+
|
|
25
|
+
// Future: Preview handlers for immediate execution mode
|
|
26
|
+
previewHandler?: (args?: string) => void; // Called immediately as user types
|
|
27
|
+
cancelHandler?: () => void; // Called when user presses Escape
|
|
28
|
+
executeImmediately?: boolean; // Override global immediateExecution setting
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ShortcutManagerConfig {
|
|
32
|
+
shortcuts: ShortcutConfig[];
|
|
33
|
+
helpContainer?: string;
|
|
34
|
+
timeout?: number; // ms to return to normal state (current: 3000ms, immediate mode: 300ms)
|
|
35
|
+
immediateExecution?: boolean; // Enable immediate execution with preview
|
|
36
|
+
previewDelay?: number; // ms delay before preview execution (default: 300ms)
|
|
37
|
+
onStateChange?: (state: KeyboardState, command?: string) => void; // Callback when state changes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export enum KeyboardState {
|
|
41
|
+
NORMAL = 'normal',
|
|
42
|
+
AWAITING_ARGS = 'awaiting_args'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class KeyboardShortcutManager {
|
|
46
|
+
private shortcuts: Map<string, ShortcutConfig> = new Map();
|
|
47
|
+
private state: KeyboardState = KeyboardState.NORMAL;
|
|
48
|
+
private currentCommand: string = '';
|
|
49
|
+
private currentArgs: string = '';
|
|
50
|
+
private helpContainer: string | null = null;
|
|
51
|
+
private timeout: number = 3000; // Default 3 second timeout
|
|
52
|
+
private timeoutId: number | null = null;
|
|
53
|
+
private helpOverlay: HTMLElement | null = null;
|
|
54
|
+
private immediateExecution: boolean = false; // Enable immediate execution mode
|
|
55
|
+
private previewDelay: number = 300; // Default 300ms preview delay
|
|
56
|
+
private previewTimeoutId: number | null = null; // Separate timeout for preview
|
|
57
|
+
private isPreviewActive: boolean = false; // Track if preview is currently active
|
|
58
|
+
private onStateChange: ((state: KeyboardState, command?: string) => void) | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(config: ShortcutManagerConfig) {
|
|
61
|
+
this.helpContainer = config.helpContainer || null;
|
|
62
|
+
this.timeout = config.timeout || 3000;
|
|
63
|
+
this.immediateExecution = config.immediateExecution || false;
|
|
64
|
+
this.previewDelay = config.previewDelay || 300;
|
|
65
|
+
this.onStateChange = config.onStateChange || null;
|
|
66
|
+
|
|
67
|
+
// Register shortcuts
|
|
68
|
+
config.shortcuts.forEach(shortcut => {
|
|
69
|
+
this.shortcuts.set(shortcut.key, shortcut);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.initialize();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private initialize(): void {
|
|
76
|
+
// Global keydown listener
|
|
77
|
+
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
|
78
|
+
|
|
79
|
+
// Show initial state indicator
|
|
80
|
+
this.updateStateIndicator();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private handleKeydown(event: KeyboardEvent): void {
|
|
84
|
+
const target = event.target as HTMLElement;
|
|
85
|
+
|
|
86
|
+
// Skip if in input field, textarea, or contenteditable
|
|
87
|
+
if (isInInputContext(target)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle help key
|
|
92
|
+
if (event.key === '?' && this.state === KeyboardState.NORMAL) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
this.showHelp();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle escape key
|
|
99
|
+
if (event.key === 'Escape') {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
|
|
102
|
+
// Cancel preview if active in immediate execution mode
|
|
103
|
+
if (this.immediateExecution && this.isPreviewActive) {
|
|
104
|
+
this.cancelCurrentPreview();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.resetState();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle state machine
|
|
112
|
+
if (this.state === KeyboardState.NORMAL) {
|
|
113
|
+
this.handleNormalState(event);
|
|
114
|
+
} else if (this.state === KeyboardState.AWAITING_ARGS) {
|
|
115
|
+
this.handleAwaitingArgsState(event);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private handleNormalState(event: KeyboardEvent): void {
|
|
120
|
+
const key = event.key.toLowerCase();
|
|
121
|
+
const shortcut = this.shortcuts.get(key);
|
|
122
|
+
|
|
123
|
+
if (shortcut) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
|
|
126
|
+
// Check context filter
|
|
127
|
+
if (shortcut.contextFilter && !shortcut.contextFilter(event)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (shortcut.requiresArgs) {
|
|
132
|
+
// Enter args waiting state
|
|
133
|
+
this.state = KeyboardState.AWAITING_ARGS;
|
|
134
|
+
this.currentCommand = key;
|
|
135
|
+
this.currentArgs = '';
|
|
136
|
+
this.updateStateIndicator();
|
|
137
|
+
this.startTimeout();
|
|
138
|
+
|
|
139
|
+
// Notify state change
|
|
140
|
+
if (this.onStateChange) {
|
|
141
|
+
this.onStateChange(this.state, key);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Execute immediately
|
|
145
|
+
this.executeShortcut(shortcut);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private handleAwaitingArgsState(event: KeyboardEvent): void {
|
|
151
|
+
const key = event.key;
|
|
152
|
+
|
|
153
|
+
if (key >= '0' && key <= '9') {
|
|
154
|
+
// Add digit to args
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
this.currentArgs += key;
|
|
157
|
+
this.updateStateIndicator();
|
|
158
|
+
this.resetTimeout();
|
|
159
|
+
|
|
160
|
+
// Handle immediate execution mode
|
|
161
|
+
if (this.immediateExecution) {
|
|
162
|
+
this.schedulePreviewExecution();
|
|
163
|
+
}
|
|
164
|
+
} else if (key === 'Enter' || key === ' ') {
|
|
165
|
+
// Execute command with args
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
this.executeCurrentCommand();
|
|
168
|
+
} else if (key === 'Backspace') {
|
|
169
|
+
// Remove last digit
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
this.currentArgs = this.currentArgs.slice(0, -1);
|
|
172
|
+
this.updateStateIndicator();
|
|
173
|
+
this.resetTimeout();
|
|
174
|
+
|
|
175
|
+
// Handle immediate execution mode
|
|
176
|
+
if (this.immediateExecution) {
|
|
177
|
+
this.cancelPreviewExecution();
|
|
178
|
+
if (this.currentArgs.length > 0) {
|
|
179
|
+
this.schedulePreviewExecution();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private executeCurrentCommand(): void {
|
|
186
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
187
|
+
if (shortcut && this.currentArgs) {
|
|
188
|
+
this.executeShortcut(shortcut, this.currentArgs);
|
|
189
|
+
}
|
|
190
|
+
this.resetState();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private executeShortcut(shortcut: ShortcutConfig, args?: string): void {
|
|
194
|
+
try {
|
|
195
|
+
shortcut.handler(args);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('Error executing shortcut:', error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private schedulePreviewExecution(): void {
|
|
202
|
+
// Cancel any existing preview timeout
|
|
203
|
+
this.cancelPreviewExecution();
|
|
204
|
+
|
|
205
|
+
// Schedule preview execution
|
|
206
|
+
this.previewTimeoutId = window.setTimeout(() => {
|
|
207
|
+
this.executePreview();
|
|
208
|
+
}, this.previewDelay);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private cancelPreviewExecution(): void {
|
|
212
|
+
if (this.previewTimeoutId) {
|
|
213
|
+
window.clearTimeout(this.previewTimeoutId);
|
|
214
|
+
this.previewTimeoutId = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Cancel current preview if active
|
|
218
|
+
if (this.isPreviewActive) {
|
|
219
|
+
this.cancelCurrentPreview();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private executePreview(): void {
|
|
224
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
225
|
+
if (shortcut && this.currentArgs) {
|
|
226
|
+
try {
|
|
227
|
+
// Call preview handler if available
|
|
228
|
+
if (shortcut.previewHandler) {
|
|
229
|
+
shortcut.previewHandler(this.currentArgs);
|
|
230
|
+
this.isPreviewActive = true;
|
|
231
|
+
} else {
|
|
232
|
+
// Fallback to regular handler for immediate execution
|
|
233
|
+
shortcut.handler(this.currentArgs);
|
|
234
|
+
this.isPreviewActive = true;
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('Error executing preview:', error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private cancelCurrentPreview(): void {
|
|
243
|
+
if (this.isPreviewActive) {
|
|
244
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
245
|
+
if (shortcut && shortcut.cancelHandler) {
|
|
246
|
+
try {
|
|
247
|
+
shortcut.cancelHandler();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('Error canceling preview:', error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.isPreviewActive = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
private resetState(): void {
|
|
258
|
+
this.state = KeyboardState.NORMAL;
|
|
259
|
+
this.currentCommand = '';
|
|
260
|
+
this.currentArgs = '';
|
|
261
|
+
this.clearTimeout();
|
|
262
|
+
this.cancelPreviewExecution();
|
|
263
|
+
this.updateStateIndicator();
|
|
264
|
+
this.hideHelp();
|
|
265
|
+
|
|
266
|
+
// Notify state change
|
|
267
|
+
if (this.onStateChange) {
|
|
268
|
+
this.onStateChange(this.state);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private startTimeout(): void {
|
|
273
|
+
this.timeoutId = window.setTimeout(() => {
|
|
274
|
+
this.resetState();
|
|
275
|
+
}, this.timeout);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private resetTimeout(): void {
|
|
279
|
+
this.clearTimeout();
|
|
280
|
+
this.startTimeout();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private clearTimeout(): void {
|
|
284
|
+
if (this.timeoutId) {
|
|
285
|
+
window.clearTimeout(this.timeoutId);
|
|
286
|
+
this.timeoutId = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private updateStateIndicator(): void {
|
|
291
|
+
// Remove existing indicator
|
|
292
|
+
const existingIndicator = document.getElementById('keyboard-state-indicator');
|
|
293
|
+
if (existingIndicator) {
|
|
294
|
+
existingIndicator.remove();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Only show indicator when not in normal state
|
|
298
|
+
if (this.state === KeyboardState.NORMAL) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Create new indicator
|
|
303
|
+
const indicator = document.createElement('div');
|
|
304
|
+
indicator.id = 'keyboard-state-indicator';
|
|
305
|
+
indicator.className = 'fixed top-4 right-4 bg-blue-600 text-white px-3 py-2 rounded-lg shadow-lg z-50 font-mono text-sm';
|
|
306
|
+
|
|
307
|
+
if (this.state === KeyboardState.AWAITING_ARGS) {
|
|
308
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
309
|
+
const description = shortcut ? shortcut.description : 'Unknown command';
|
|
310
|
+
indicator.innerHTML = `
|
|
311
|
+
<div class="flex items-center space-x-2">
|
|
312
|
+
<span>${this.currentCommand.toUpperCase()}</span>
|
|
313
|
+
<span class="text-blue-200">${this.currentArgs || '_'}</span>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="text-xs text-blue-200 mt-1">${description}</div>
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
document.body.appendChild(indicator);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private showHelp(): void {
|
|
323
|
+
if (this.helpOverlay) {
|
|
324
|
+
this.hideHelp();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.helpOverlay = document.createElement('div');
|
|
329
|
+
this.helpOverlay.id = 'keyboard-help-overlay';
|
|
330
|
+
this.helpOverlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
|
331
|
+
|
|
332
|
+
const helpContent = document.createElement('div');
|
|
333
|
+
helpContent.className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl max-h-[80vh] overflow-y-auto p-6';
|
|
334
|
+
|
|
335
|
+
helpContent.innerHTML = this.generateHelpContent();
|
|
336
|
+
|
|
337
|
+
this.helpOverlay.appendChild(helpContent);
|
|
338
|
+
document.body.appendChild(this.helpOverlay);
|
|
339
|
+
|
|
340
|
+
// Close on click outside or escape
|
|
341
|
+
this.helpOverlay.addEventListener('click', (e) => {
|
|
342
|
+
if (e.target === this.helpOverlay) {
|
|
343
|
+
this.hideHelp();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private hideHelp(): void {
|
|
349
|
+
if (this.helpOverlay) {
|
|
350
|
+
this.helpOverlay.remove();
|
|
351
|
+
this.helpOverlay = null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private generateHelpContent(): string {
|
|
356
|
+
const categories = new Map<string, ShortcutConfig[]>();
|
|
357
|
+
|
|
358
|
+
// Group shortcuts by category
|
|
359
|
+
this.shortcuts.forEach(shortcut => {
|
|
360
|
+
const category = shortcut.category || 'General';
|
|
361
|
+
if (!categories.has(category)) {
|
|
362
|
+
categories.set(category, []);
|
|
363
|
+
}
|
|
364
|
+
categories.get(category)!.push(shortcut);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
let html = `
|
|
368
|
+
<div class="flex items-center justify-between mb-4">
|
|
369
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Keyboard Shortcuts</h2>
|
|
370
|
+
<button class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" onclick="this.closest('#keyboard-help-overlay').remove()">
|
|
371
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
372
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
373
|
+
</svg>
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
`;
|
|
377
|
+
|
|
378
|
+
categories.forEach((shortcuts, category) => {
|
|
379
|
+
html += `
|
|
380
|
+
<div class="mb-6">
|
|
381
|
+
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">${category}</h3>
|
|
382
|
+
<div class="space-y-2">
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
shortcuts.forEach(shortcut => {
|
|
386
|
+
const keyDisplay = shortcut.requiresArgs
|
|
387
|
+
? `${shortcut.key.toUpperCase()}<span class="text-blue-600 dark:text-blue-400"><number></span>`
|
|
388
|
+
: shortcut.key.toUpperCase();
|
|
389
|
+
|
|
390
|
+
html += `
|
|
391
|
+
<div class="flex items-center justify-between py-2 px-3 bg-gray-50 dark:bg-gray-700 rounded">
|
|
392
|
+
<span class="text-sm text-gray-700 dark:text-gray-300">${shortcut.description}</span>
|
|
393
|
+
<kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded">${keyDisplay}</kbd>
|
|
394
|
+
</div>
|
|
395
|
+
`;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
html += `
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
`;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
html += `
|
|
406
|
+
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
407
|
+
<p class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
408
|
+
Press <kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 rounded">ESC</kbd> to cancel any command or
|
|
409
|
+
<kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 rounded">?</kbd> to close this help
|
|
410
|
+
</p>
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
|
|
414
|
+
return html;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public destroy(): void {
|
|
418
|
+
document.removeEventListener('keydown', this.handleKeydown.bind(this));
|
|
419
|
+
this.cancelPreviewExecution();
|
|
420
|
+
this.resetState();
|
|
421
|
+
this.hideHelp();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public getState(): KeyboardState {
|
|
425
|
+
return this.state;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public getCurrentCommand(): string {
|
|
429
|
+
return this.currentCommand;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public getCurrentArgs(): string {
|
|
433
|
+
return this.currentArgs;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LCMComponent Interface - Defines multi-phase component initialization
|
|
3
|
+
*
|
|
4
|
+
* This interface implements a breadth-first lifecycle pattern that eliminates
|
|
5
|
+
* initialization order dependencies and race conditions through synchronization barriers.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle Phases:
|
|
8
|
+
* 1. Construction - Components are created but not initialized
|
|
9
|
+
* 2. bindToDOM() - Basic DOM setup, discover child components
|
|
10
|
+
* 3. injectDependencies() - Receive references to other components
|
|
11
|
+
* 4. activate() - Final setup when all dependencies are ready
|
|
12
|
+
*
|
|
13
|
+
* Key Benefits:
|
|
14
|
+
* - Order Independence: Components can be created in any sequence
|
|
15
|
+
* - Async Safety: Each phase waits for all components before proceeding
|
|
16
|
+
* - Clear Dependencies: Explicit injection points prevent race conditions
|
|
17
|
+
* - Error Isolation: Component failures don't cascade to others
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* LCM Component - Short for LifeCycle Managed Component
|
|
22
|
+
* enable components to be declared first and their loading be managed by a
|
|
23
|
+
* LifecycleController so that we have layered creation, dependency injection and setup
|
|
24
|
+
* in a breadth first way.
|
|
25
|
+
*
|
|
26
|
+
* A key constraint on LCMComponents are that they should not perform any initialization
|
|
27
|
+
* in the constructor. This is because a LifecycleController should be used to load/setup
|
|
28
|
+
* these components and they will follow a layered approach. Performing these actions in
|
|
29
|
+
* the constructor could violate the idempotency guarantees.
|
|
30
|
+
*/
|
|
31
|
+
export interface LCMComponent {
|
|
32
|
+
/**
|
|
33
|
+
* Phase 1: The "local" initialization of the component.
|
|
34
|
+
*
|
|
35
|
+
* In this phase the component initializes itself and returns any children it might
|
|
36
|
+
* want initialized as part of the lifecycled loading.
|
|
37
|
+
*
|
|
38
|
+
* This phase should:
|
|
39
|
+
* - Set up basic DOM elements and event listeners
|
|
40
|
+
* - Create child components (but don't initialize them) and return them
|
|
41
|
+
* - Return array of child components for lifecycle controller discovery
|
|
42
|
+
*
|
|
43
|
+
* This phase must be synchronous and should not:
|
|
44
|
+
* - Access other components or external dependencies
|
|
45
|
+
* - Perform async operations
|
|
46
|
+
* - Emit events or notifications
|
|
47
|
+
*
|
|
48
|
+
* @returns Array of child components to be managed by lifecycle controller
|
|
49
|
+
*/
|
|
50
|
+
performLocalInit(): Promise<LCMComponent[]> | LCMComponent[];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Phase 2: Inject dependencies from parent/siblings
|
|
54
|
+
*
|
|
55
|
+
* This phase should:
|
|
56
|
+
* - Receive and store references to required dependencies
|
|
57
|
+
* - Validate that required dependencies are provided
|
|
58
|
+
* - Set up internal state based on dependencies
|
|
59
|
+
*
|
|
60
|
+
* This phase can be async and may:
|
|
61
|
+
* - Load external data or resources
|
|
62
|
+
* - Perform validation or setup operations
|
|
63
|
+
* - Initialize internal components that depend on injected references
|
|
64
|
+
*
|
|
65
|
+
* @param deps Record of dependency name to dependency instance
|
|
66
|
+
* @returns Promise<void> or void - can be async
|
|
67
|
+
*/
|
|
68
|
+
setupDependencies(): Promise<void> | void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Phase 3: Activate component when all dependencies are ready
|
|
72
|
+
*
|
|
73
|
+
* This phase should:
|
|
74
|
+
* - Complete final initialization
|
|
75
|
+
* - Enable component functionality
|
|
76
|
+
* - Start listening for external events
|
|
77
|
+
* - Begin normal operation
|
|
78
|
+
*
|
|
79
|
+
* This phase can be async and may:
|
|
80
|
+
* - Connect to external services
|
|
81
|
+
* - Load initial data
|
|
82
|
+
* - Emit ready notifications
|
|
83
|
+
*
|
|
84
|
+
* @returns Promise<void> or void - can be async
|
|
85
|
+
*/
|
|
86
|
+
activate(): Promise<void> | void;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup phase: Deactivate component and clean up resources
|
|
90
|
+
*
|
|
91
|
+
* This should:
|
|
92
|
+
* - Stop all ongoing operations
|
|
93
|
+
* - Remove event listeners
|
|
94
|
+
* - Clean up external connections
|
|
95
|
+
* - Dispose of child components
|
|
96
|
+
*
|
|
97
|
+
* @returns Promise<void> or void - can be async
|
|
98
|
+
*/
|
|
99
|
+
deactivate(): Promise<void> | void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Configuration for component lifecycle behavior
|
|
104
|
+
*/
|
|
105
|
+
export interface LCMComponentConfig {
|
|
106
|
+
/**
|
|
107
|
+
* Maximum time to wait for a lifecycle phase to complete (ms)
|
|
108
|
+
* Default: 10000 (10 seconds)
|
|
109
|
+
*/
|
|
110
|
+
phaseTimeoutMs?: number;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether to continue if individual components fail during a phase
|
|
114
|
+
* Default: false (fail fast)
|
|
115
|
+
*/
|
|
116
|
+
continueOnError?: boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether to validate dependencies against declared requirements
|
|
120
|
+
* Default: true
|
|
121
|
+
*/
|
|
122
|
+
validateDependencies?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Enable debug logging for lifecycle phases
|
|
126
|
+
* Default: false
|
|
127
|
+
*/
|
|
128
|
+
enableDebugLogging?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Event emitted during component lifecycle transitions
|
|
133
|
+
*/
|
|
134
|
+
export interface LCMComponentEvent {
|
|
135
|
+
type: 'phase-start' | 'phase-complete' | 'phase-error' | 'component-ready';
|
|
136
|
+
componentName: string;
|
|
137
|
+
timestamp: number;
|
|
138
|
+
error?: Error;
|
|
139
|
+
metadata?: Record<string, any>;
|
|
140
|
+
}
|