@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for managing the splash screen that loads before JavaScript
|
|
3
|
+
*/
|
|
4
|
+
export class SplashScreen {
|
|
5
|
+
private static readonly SPLASH_ID = 'splash-screen';
|
|
6
|
+
private static dismissed = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dismiss the splash screen with a fade-out animation
|
|
10
|
+
* Safe to call multiple times - only dismisses once
|
|
11
|
+
*/
|
|
12
|
+
static dismiss(): void {
|
|
13
|
+
if (this.dismissed) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
18
|
+
if (!splash) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.dismissed = true;
|
|
23
|
+
|
|
24
|
+
// Fade out
|
|
25
|
+
splash.style.opacity = '0';
|
|
26
|
+
|
|
27
|
+
// Remove from DOM after animation completes
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
splash.remove();
|
|
30
|
+
}, 300); // Match transition-opacity duration-300 from Tailwind
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Update the splash screen message (if it hasn't been dismissed yet)
|
|
35
|
+
*/
|
|
36
|
+
static updateMessage(title?: string, message?: string): void {
|
|
37
|
+
if (this.dismissed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
42
|
+
if (!splash) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (title) {
|
|
47
|
+
const titleEl = splash.querySelector('[data-splash-title]');
|
|
48
|
+
if (titleEl) {
|
|
49
|
+
titleEl.textContent = title;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (message) {
|
|
54
|
+
const messageEl = splash.querySelector('[data-splash-message]');
|
|
55
|
+
if (messageEl) {
|
|
56
|
+
messageEl.textContent = message;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update the splash screen progress bar
|
|
63
|
+
* @param percent - Progress percentage (0-100)
|
|
64
|
+
*/
|
|
65
|
+
static updateProgress(percent: number): void {
|
|
66
|
+
if (this.dismissed) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
71
|
+
if (!splash) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Clamp between 0-100
|
|
76
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
77
|
+
|
|
78
|
+
const progressBar = splash.querySelector('[data-splash-progress-bar]') as HTMLElement;
|
|
79
|
+
if (progressBar) {
|
|
80
|
+
progressBar.style.width = `${clampedPercent}%`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const progressText = splash.querySelector('[data-splash-progress-text]');
|
|
84
|
+
if (progressText) {
|
|
85
|
+
progressText.textContent = `${Math.round(clampedPercent)}%`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Update both message and progress at once
|
|
91
|
+
*/
|
|
92
|
+
static update(options: {
|
|
93
|
+
title?: string;
|
|
94
|
+
message?: string;
|
|
95
|
+
progress?: number;
|
|
96
|
+
}): void {
|
|
97
|
+
if (options.title !== undefined || options.message !== undefined) {
|
|
98
|
+
this.updateMessage(options.title, options.message);
|
|
99
|
+
}
|
|
100
|
+
if (options.progress !== undefined) {
|
|
101
|
+
this.updateProgress(options.progress);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if splash screen is still visible
|
|
107
|
+
*/
|
|
108
|
+
static isVisible(): boolean {
|
|
109
|
+
return !this.dismissed && !!document.getElementById(this.SPLASH_ID);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
// Helper to manage our templates
|
|
3
|
+
export class TemplateLoader {
|
|
4
|
+
constructor(public registryName = "template-registry") {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Finds the specified template wrapper element in the registry.
|
|
10
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
11
|
+
* @returns The wrapper HTMLElement or null if not found.
|
|
12
|
+
*/
|
|
13
|
+
private _findTemplateWrapper(templateId: string): HTMLElement | null {
|
|
14
|
+
const templateRegistry = document.getElementById(this.registryName);
|
|
15
|
+
if (!templateRegistry) {
|
|
16
|
+
console.error(`Template registry '#${this.registryName}' not found!`);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const templateWrapper = templateRegistry.querySelector<HTMLElement>(`[data-template-id="${templateId}"]`);
|
|
20
|
+
if (!templateWrapper) {
|
|
21
|
+
console.error(`Template with ID "${templateId}" not found in registry '#${this.registryName}'.`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return templateWrapper;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Loads and returns the inner HTML of a template definition.
|
|
29
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
30
|
+
* @returns The innerHTML content as string if it exists otherwise null.
|
|
31
|
+
*/
|
|
32
|
+
loadHtml(templateId: string): string | null {
|
|
33
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
34
|
+
if (!templateWrapper) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return templateWrapper.innerHTML
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loads and clones the child elements of a template definition.
|
|
42
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
43
|
+
* @returns An array of cloned HTMLElement children, or an empty array if not found or has no children.
|
|
44
|
+
*/
|
|
45
|
+
load(templateId: string): HTMLElement[] {
|
|
46
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
47
|
+
if (!templateWrapper) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Using hidden div: Clone the first child element which is the actual template root
|
|
52
|
+
const templateRootElement = templateWrapper.cloneNode(true) as HTMLElement | null;
|
|
53
|
+
if (!templateRootElement) {
|
|
54
|
+
console.error(`Template content is empty for: ${templateId}`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
// deep clone AND return th children
|
|
58
|
+
return Array.from(templateRootElement.children) as HTMLElement[];
|
|
59
|
+
|
|
60
|
+
// clond the children instead
|
|
61
|
+
// Clone each child element individually and return as an array
|
|
62
|
+
// const childElements = Array.from(templateWrapper.children) as HTMLElement[];
|
|
63
|
+
// return childElements.map(child => child.cloneNode(true) as HTMLElement);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loads a template's content, clears the target element, and appends the cloned content into it.
|
|
68
|
+
* @param templateId The data-template-id of the wrapper element to load.
|
|
69
|
+
* @param targetElement The HTMLElement where the cloned content should be placed.
|
|
70
|
+
* @returns True if the operation was successful (template found and content appended, even if content was empty), false otherwise.
|
|
71
|
+
*/
|
|
72
|
+
public loadInto(templateId: string, targetElement: HTMLElement | null): boolean {
|
|
73
|
+
if (!targetElement) {
|
|
74
|
+
console.error(`Cannot load template "${templateId}": Target element is null.`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
79
|
+
if (!templateWrapper) {
|
|
80
|
+
// Error logged in _findTemplateWrapper
|
|
81
|
+
targetElement.innerHTML = `<div class="p-4 text-red-500">Error loading template '${templateId}' (Not Found)</div>`; // Provide feedback in target
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Clear the target element first
|
|
86
|
+
targetElement.innerHTML = '';
|
|
87
|
+
|
|
88
|
+
// Clone and append children
|
|
89
|
+
const childElements = Array.from(templateWrapper.children) as HTMLElement[];
|
|
90
|
+
if (childElements.length === 0) {
|
|
91
|
+
console.warn(`Template "${templateId}" has no child elements to load.`);
|
|
92
|
+
// Still considered successful, just loaded nothing.
|
|
93
|
+
} else {
|
|
94
|
+
childElements.forEach(child => {
|
|
95
|
+
targetElement.appendChild(child.cloneNode(true) as HTMLElement);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true; // Operation successful
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme management for applications
|
|
3
|
+
*/
|
|
4
|
+
export class ThemeManager {
|
|
5
|
+
// Theme options
|
|
6
|
+
static LIGHT = 'light';
|
|
7
|
+
static DARK = 'dark';
|
|
8
|
+
static SYSTEM = 'system';
|
|
9
|
+
|
|
10
|
+
// Icons for each theme state
|
|
11
|
+
static readonly LIGHT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /></svg>`; // Sun icon
|
|
12
|
+
static readonly DARK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /></svg>`; // Moon icon
|
|
13
|
+
static readonly SYSTEM_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /></svg>`; // Computer icon
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize theme based on saved preference or system default
|
|
17
|
+
*/
|
|
18
|
+
static initialize(): void {
|
|
19
|
+
const savedTheme = localStorage.getItem('theme');
|
|
20
|
+
|
|
21
|
+
if (savedTheme === ThemeManager.DARK ||
|
|
22
|
+
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
23
|
+
document.documentElement.classList.add('dark');
|
|
24
|
+
} else {
|
|
25
|
+
document.documentElement.classList.remove('dark');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set theme and save preference
|
|
31
|
+
*/
|
|
32
|
+
static setTheme(theme: string): void {
|
|
33
|
+
if (theme === ThemeManager.SYSTEM) {
|
|
34
|
+
localStorage.removeItem('theme');
|
|
35
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
36
|
+
document.documentElement.classList.add('dark');
|
|
37
|
+
} else {
|
|
38
|
+
document.documentElement.classList.remove('dark');
|
|
39
|
+
}
|
|
40
|
+
} else if (theme === ThemeManager.DARK) {
|
|
41
|
+
localStorage.setItem('theme', ThemeManager.DARK);
|
|
42
|
+
document.documentElement.classList.add('dark');
|
|
43
|
+
} else { // LIGHT
|
|
44
|
+
localStorage.setItem('theme', ThemeManager.LIGHT);
|
|
45
|
+
document.documentElement.classList.remove('dark');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current theme setting (light, dark, or system)
|
|
51
|
+
*/
|
|
52
|
+
static getCurrentThemeSetting(): string {
|
|
53
|
+
return localStorage.getItem('theme') || ThemeManager.SYSTEM;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets the *next* theme in the cycle: Light -> Dark -> System -> Light ...
|
|
58
|
+
*/
|
|
59
|
+
static getNextTheme(currentSetting: string): string {
|
|
60
|
+
if (currentSetting === ThemeManager.LIGHT) {
|
|
61
|
+
return ThemeManager.DARK;
|
|
62
|
+
} else if (currentSetting === ThemeManager.DARK) {
|
|
63
|
+
return ThemeManager.SYSTEM;
|
|
64
|
+
} else { // SYSTEM or unknown goes to LIGHT
|
|
65
|
+
return ThemeManager.LIGHT;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the appropriate SVG icon string for a given theme setting.
|
|
71
|
+
*/
|
|
72
|
+
static getIconSVG(themeSetting: string): string {
|
|
73
|
+
switch (themeSetting) {
|
|
74
|
+
case ThemeManager.LIGHT: return ThemeManager.LIGHT_ICON_SVG;
|
|
75
|
+
case ThemeManager.DARK: return ThemeManager.DARK_ICON_SVG;
|
|
76
|
+
case ThemeManager.SYSTEM:
|
|
77
|
+
default: return ThemeManager.SYSTEM_ICON_SVG;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets a user-friendly label for the theme setting.
|
|
83
|
+
*/
|
|
84
|
+
static getThemeLabel(themeSetting: string): string {
|
|
85
|
+
switch (themeSetting) {
|
|
86
|
+
case ThemeManager.LIGHT: return "Light Mode";
|
|
87
|
+
case ThemeManager.DARK: return "Dark Mode";
|
|
88
|
+
case ThemeManager.SYSTEM:
|
|
89
|
+
default: return "System Default";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Initialize the ThemeManager (no instance needed for static methods)
|
|
96
|
+
*/
|
|
97
|
+
public static init(): void {
|
|
98
|
+
ThemeManager.initialize();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast types for styling
|
|
3
|
+
*/
|
|
4
|
+
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages toast notifications
|
|
8
|
+
*/
|
|
9
|
+
export class ToastManager {
|
|
10
|
+
private static instance: ToastManager | null = null;
|
|
11
|
+
private container: HTMLElement | null;
|
|
12
|
+
private template: HTMLElement | null;
|
|
13
|
+
private toasts: Map<string, HTMLElement> = new Map();
|
|
14
|
+
private counter: number = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Private constructor for singleton pattern
|
|
18
|
+
*/
|
|
19
|
+
private constructor() {
|
|
20
|
+
// Select the container using its ID from index.html
|
|
21
|
+
this.container = document.getElementById('toast-container');
|
|
22
|
+
this.template = document.getElementById('toast-template');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the ToastManager instance (singleton)
|
|
27
|
+
*/
|
|
28
|
+
public static getInstance(): ToastManager {
|
|
29
|
+
if (!ToastManager.instance) {
|
|
30
|
+
ToastManager.instance = new ToastManager();
|
|
31
|
+
}
|
|
32
|
+
return ToastManager.instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Show a toast notification
|
|
37
|
+
* @param title Toast title
|
|
38
|
+
* @param message Toast message
|
|
39
|
+
* @param type Toast type for styling
|
|
40
|
+
* @param duration Duration in ms (default: 4000)
|
|
41
|
+
*/
|
|
42
|
+
public showToast(title: string, message: string, type: ToastType = 'info', duration: number = 4000): string {
|
|
43
|
+
if (!this.container || !this.template) return '';
|
|
44
|
+
|
|
45
|
+
// Create a unique ID for this toast
|
|
46
|
+
const id = `toast-${Date.now()}-${this.counter++}`;
|
|
47
|
+
|
|
48
|
+
// Clone the template
|
|
49
|
+
const toast = this.template.cloneNode(true) as HTMLElement;
|
|
50
|
+
toast.id = id;
|
|
51
|
+
toast.classList.remove('hidden');
|
|
52
|
+
|
|
53
|
+
// Set content
|
|
54
|
+
const titleElement = toast.querySelector('.toast-title');
|
|
55
|
+
const messageElement = toast.querySelector('.toast-message');
|
|
56
|
+
if (titleElement) titleElement.textContent = title;
|
|
57
|
+
if (messageElement) messageElement.textContent = message;
|
|
58
|
+
|
|
59
|
+
// Set style based on type
|
|
60
|
+
const iconContainer = toast.querySelector('.flex-shrink-0');
|
|
61
|
+
if (iconContainer) {
|
|
62
|
+
// Clear existing icon
|
|
63
|
+
iconContainer.innerHTML = '';
|
|
64
|
+
|
|
65
|
+
// Add appropriate icon based on type
|
|
66
|
+
let icon: string;
|
|
67
|
+
let borderColor: string;
|
|
68
|
+
|
|
69
|
+
switch (type) {
|
|
70
|
+
case 'success':
|
|
71
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>';
|
|
72
|
+
borderColor = 'border-green-500';
|
|
73
|
+
break;
|
|
74
|
+
case 'error':
|
|
75
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /></svg>';
|
|
76
|
+
borderColor = 'border-red-500';
|
|
77
|
+
break;
|
|
78
|
+
case 'warning':
|
|
79
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
|
|
80
|
+
borderColor = 'border-yellow-500';
|
|
81
|
+
break;
|
|
82
|
+
case 'info':
|
|
83
|
+
default:
|
|
84
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg>';
|
|
85
|
+
borderColor = 'border-blue-500';
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add icon
|
|
90
|
+
iconContainer.innerHTML = icon;
|
|
91
|
+
|
|
92
|
+
// Set border color
|
|
93
|
+
const borderElement = toast.querySelector('.border-l-4');
|
|
94
|
+
if (borderElement) {
|
|
95
|
+
borderElement.className = borderElement.className.replace(/border-[a-z]+-500/g, borderColor);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add close button handler
|
|
100
|
+
const closeButton = toast.querySelector('.toast-close');
|
|
101
|
+
if (closeButton) {
|
|
102
|
+
closeButton.addEventListener('click', () => {
|
|
103
|
+
this.hideToast(id);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Add to container
|
|
108
|
+
this.container.appendChild(toast);
|
|
109
|
+
this.toasts.set(id, toast);
|
|
110
|
+
|
|
111
|
+
// Show toast with animation
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
toast.classList.remove('scale-95', 'opacity-0');
|
|
114
|
+
toast.classList.add('scale-100', 'opacity-100');
|
|
115
|
+
}, 10);
|
|
116
|
+
|
|
117
|
+
// Auto-hide after duration
|
|
118
|
+
if (duration > 0) {
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
this.hideToast(id);
|
|
121
|
+
}, duration);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hide a toast notification
|
|
129
|
+
* @param id Toast ID
|
|
130
|
+
*/
|
|
131
|
+
public hideToast(id: string): void {
|
|
132
|
+
const toast = this.toasts.get(id);
|
|
133
|
+
if (!toast) return;
|
|
134
|
+
|
|
135
|
+
// Hide with animation
|
|
136
|
+
toast.classList.remove('scale-100', 'opacity-100');
|
|
137
|
+
toast.classList.add('scale-95', 'opacity-0');
|
|
138
|
+
|
|
139
|
+
// Remove after animation
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
toast.remove();
|
|
142
|
+
this.toasts.delete(id);
|
|
143
|
+
}, 300);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Hide all toast notifications
|
|
148
|
+
*/
|
|
149
|
+
public hideAllToasts(): void {
|
|
150
|
+
this.toasts.forEach((_, id) => {
|
|
151
|
+
this.hideToast(id);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Initialize the component
|
|
157
|
+
*/
|
|
158
|
+
public static init(): ToastManager {
|
|
159
|
+
return ToastManager.getInstance();
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/events.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Core component system
|
|
2
|
+
export type { Component } from './Component';
|
|
3
|
+
export { BaseComponent } from './Component';
|
|
4
|
+
export type { LCMComponent, LCMComponentConfig, LCMComponentEvent } from './LCMComponent';
|
|
5
|
+
export { LifecycleController } from './LifecycleController';
|
|
6
|
+
|
|
7
|
+
// Event system
|
|
8
|
+
export {
|
|
9
|
+
EventBus,
|
|
10
|
+
ComponentEventTypes,
|
|
11
|
+
LifecycleEventTypes,
|
|
12
|
+
} from './EventBus';
|
|
13
|
+
export type {
|
|
14
|
+
EventHandler,
|
|
15
|
+
EventSubscriber,
|
|
16
|
+
LifecycleEventType,
|
|
17
|
+
ComponentEventType
|
|
18
|
+
} from './EventBus';
|
|
19
|
+
export type { ComponentLifecycleEvent } from './events';
|
|
20
|
+
|
|
21
|
+
// UI Components
|
|
22
|
+
export { BasePage } from './BasePage';
|
|
23
|
+
export { Modal } from './Modal';
|
|
24
|
+
export { ToastManager } from './ToastManager';
|
|
25
|
+
export { ThemeManager } from './ThemeManager';
|
|
26
|
+
export { TemplateLoader } from './TemplateLoader';
|
|
27
|
+
export { SplashScreen } from './SplashScreen';
|
|
28
|
+
export { MobileBottomDrawer } from './MobileBottomDrawer';
|
|
29
|
+
|
|
30
|
+
// Utilities
|
|
31
|
+
export { isInInputContext, hasModifierKeys, shouldIgnoreShortcut } from './DOMUtils';
|
|
32
|
+
export {
|
|
33
|
+
KeyboardShortcutManager,
|
|
34
|
+
KeyboardState,
|
|
35
|
+
} from './KeyboardShortcutManager';
|
|
36
|
+
export type {
|
|
37
|
+
ShortcutConfig,
|
|
38
|
+
ShortcutManagerConfig
|
|
39
|
+
} from './KeyboardShortcutManager';
|