@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.
@@ -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
@@ -0,0 +1,5 @@
1
+
2
+ export interface ComponentLifecycleEvent {
3
+ success: boolean;
4
+ error: Error | null;
5
+ }
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';