agentgui 1.0.273 → 1.0.275

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 (71) hide show
  1. package/CLAUDE.md +280 -280
  2. package/IPFS_DOWNLOADER.md +277 -277
  3. package/TASK_2C_COMPLETION.md +334 -334
  4. package/bin/gmgui.cjs +54 -54
  5. package/build-portable.js +111 -0
  6. package/database.js +1422 -1406
  7. package/lib/claude-runner.js +1130 -1130
  8. package/lib/ipfs-downloader.js +459 -459
  9. package/lib/speech.js +152 -152
  10. package/package.json +1 -1
  11. package/portable-entry.js +43 -0
  12. package/readme.md +76 -76
  13. package/scripts/inject-pe-section.py +185 -0
  14. package/server.js +3787 -3794
  15. package/setup-npm-token.sh +68 -68
  16. package/static/app.js +773 -773
  17. package/static/event-rendering-showcase.html +708 -708
  18. package/static/index.html +3178 -3180
  19. package/static/js/agent-auth.js +298 -298
  20. package/static/js/audio-recorder-processor.js +18 -18
  21. package/static/js/client.js +2656 -2656
  22. package/static/js/conversations.js +583 -583
  23. package/static/js/dialogs.js +267 -267
  24. package/static/js/event-consolidator.js +101 -101
  25. package/static/js/event-filter.js +311 -311
  26. package/static/js/event-processor.js +452 -452
  27. package/static/js/features.js +413 -413
  28. package/static/js/kalman-filter.js +67 -67
  29. package/static/js/progress-dialog.js +130 -130
  30. package/static/js/script-runner.js +219 -219
  31. package/static/js/streaming-renderer.js +2123 -2120
  32. package/static/js/syntax-highlighter.js +269 -269
  33. package/static/js/tts-websocket-handler.js +152 -152
  34. package/static/js/ui-components.js +431 -431
  35. package/static/js/voice.js +849 -849
  36. package/static/js/websocket-manager.js +596 -596
  37. package/static/templates/INDEX.html +465 -465
  38. package/static/templates/README.md +190 -190
  39. package/static/templates/agent-capabilities.html +56 -56
  40. package/static/templates/agent-metadata-panel.html +44 -44
  41. package/static/templates/agent-status-badge.html +30 -30
  42. package/static/templates/code-annotation-panel.html +155 -155
  43. package/static/templates/code-suggestion-panel.html +184 -184
  44. package/static/templates/command-header.html +77 -77
  45. package/static/templates/command-output-scrollable.html +118 -118
  46. package/static/templates/elapsed-time.html +54 -54
  47. package/static/templates/error-alert.html +106 -106
  48. package/static/templates/error-history-timeline.html +160 -160
  49. package/static/templates/error-recovery-options.html +109 -109
  50. package/static/templates/error-stack-trace.html +95 -95
  51. package/static/templates/error-summary.html +80 -80
  52. package/static/templates/event-counter.html +48 -48
  53. package/static/templates/execution-actions.html +97 -97
  54. package/static/templates/execution-progress-bar.html +80 -80
  55. package/static/templates/execution-stepper.html +120 -120
  56. package/static/templates/file-breadcrumb.html +118 -118
  57. package/static/templates/file-diff-viewer.html +121 -121
  58. package/static/templates/file-metadata.html +133 -133
  59. package/static/templates/file-read-panel.html +66 -66
  60. package/static/templates/file-write-panel.html +120 -120
  61. package/static/templates/git-branch-remote.html +107 -107
  62. package/static/templates/git-diff-list.html +101 -101
  63. package/static/templates/git-log-visualization.html +153 -153
  64. package/static/templates/git-status-panel.html +115 -115
  65. package/static/templates/quality-metrics-display.html +170 -170
  66. package/static/templates/terminal-output-panel.html +87 -87
  67. package/static/templates/test-results-display.html +144 -144
  68. package/static/theme.js +72 -72
  69. package/test-download-progress.js +223 -223
  70. package/test-websocket-broadcast.js +147 -147
  71. package/tests/ipfs-downloader.test.js +370 -370
@@ -1,431 +1,431 @@
1
- /**
2
- * UI Components
3
- * Reusable UI building blocks for modals, tabs, buttons, and more
4
- */
5
-
6
- class UIComponents {
7
- /**
8
- * Create a modal dialog
9
- */
10
- static createModal(config = {}) {
11
- const {
12
- title = 'Dialog',
13
- content = '',
14
- buttons = [],
15
- onClose = null,
16
- size = 'medium' // small, medium, large
17
- } = config;
18
-
19
- const modal = document.createElement('div');
20
- modal.className = 'modal-overlay';
21
- modal.dataset.modal = 'true';
22
-
23
- const sizeClasses = {
24
- 'small': 'max-w-sm',
25
- 'medium': 'max-w-md',
26
- 'large': 'max-w-2xl'
27
- };
28
-
29
- modal.innerHTML = `
30
- <div class="modal-content ${sizeClasses[size] || sizeClasses['medium']} bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6">
31
- <div class="modal-header flex justify-between items-center mb-4 pb-4 border-b">
32
- <h2 class="text-xl font-bold">${UIComponents.escapeHtml(title)}</h2>
33
- <button class="modal-close text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 text-2xl leading-none">&times;</button>
34
- </div>
35
- <div class="modal-body mb-4">
36
- ${typeof content === 'string' ? UIComponents.escapeHtml(content) : ''}
37
- </div>
38
- <div class="modal-footer flex gap-2 justify-end">
39
- ${buttons.map(btn => `
40
- <button class="btn btn-${btn.variant || 'secondary'}" data-action="${btn.action || 'close'}">
41
- ${UIComponents.escapeHtml(btn.label)}
42
- </button>
43
- `).join('')}
44
- </div>
45
- </div>
46
- `;
47
-
48
- // Add close handler
49
- const closeBtn = modal.querySelector('.modal-close');
50
- closeBtn.addEventListener('click', () => {
51
- modal.remove();
52
- if (onClose) onClose();
53
- });
54
-
55
- // Add button handlers
56
- modal.querySelectorAll('[data-action]').forEach(btn => {
57
- btn.addEventListener('click', (e) => {
58
- const action = e.target.dataset.action;
59
- if (action === 'close') {
60
- modal.remove();
61
- if (onClose) onClose();
62
- }
63
- });
64
- });
65
-
66
- // Close on background click
67
- modal.addEventListener('click', (e) => {
68
- if (e.target === modal) {
69
- modal.remove();
70
- if (onClose) onClose();
71
- }
72
- });
73
-
74
- return modal;
75
- }
76
-
77
- /**
78
- * Create a tabbed interface
79
- */
80
- static createTabs(config = {}) {
81
- const {
82
- tabs = [],
83
- activeTab = 0,
84
- onChange = null
85
- } = config;
86
-
87
- const container = document.createElement('div');
88
- container.className = 'tabs';
89
-
90
- // Tab buttons
91
- const tabButtons = document.createElement('div');
92
- tabButtons.className = 'tab-buttons flex border-b';
93
-
94
- tabs.forEach((tab, index) => {
95
- const btn = document.createElement('button');
96
- btn.className = `tab-button px-4 py-2 font-medium transition-colors ${
97
- index === activeTab
98
- ? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
99
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
100
- }`;
101
- btn.textContent = tab.label;
102
- btn.dataset.tabIndex = index;
103
-
104
- btn.addEventListener('click', () => {
105
- // Update active button
106
- tabButtons.querySelectorAll('.tab-button').forEach((b, i) => {
107
- if (i === index) {
108
- b.classList.add('border-b-2', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
109
- b.classList.remove('text-gray-600', 'dark:text-gray-400');
110
- } else {
111
- b.classList.remove('border-b-2', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
112
- b.classList.add('text-gray-600', 'dark:text-gray-400');
113
- }
114
- });
115
-
116
- // Update tab content
117
- tabContent.querySelectorAll('.tab-pane').forEach((pane, i) => {
118
- pane.style.display = i === index ? 'block' : 'none';
119
- });
120
-
121
- if (onChange) onChange(index);
122
- });
123
-
124
- tabButtons.appendChild(btn);
125
- });
126
-
127
- container.appendChild(tabButtons);
128
-
129
- // Tab content
130
- const tabContent = document.createElement('div');
131
- tabContent.className = 'tab-content mt-4';
132
-
133
- tabs.forEach((tab, index) => {
134
- const pane = document.createElement('div');
135
- pane.className = 'tab-pane';
136
- pane.style.display = index === activeTab ? 'block' : 'none';
137
- pane.innerHTML = typeof tab.content === 'string' ? tab.content : '';
138
- tabContent.appendChild(pane);
139
- });
140
-
141
- container.appendChild(tabContent);
142
- return container;
143
- }
144
-
145
- /**
146
- * Create an alert/notification
147
- */
148
- static createAlert(config = {}) {
149
- const {
150
- message = '',
151
- type = 'info', // info, success, warning, error
152
- duration = 5000,
153
- dismissible = true
154
- } = config;
155
-
156
- const alert = document.createElement('div');
157
- const typeClasses = {
158
- 'info': 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-200',
159
- 'success': 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-200',
160
- 'warning': 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200',
161
- 'error': 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-200'
162
- };
163
-
164
- alert.className = `alert border-l-4 p-4 mb-4 rounded ${typeClasses[type] || typeClasses['info']}`;
165
- alert.innerHTML = `
166
- <div class="flex justify-between items-center">
167
- <span>${UIComponents.escapeHtml(message)}</span>
168
- ${dismissible ? '<button class="text-current hover:opacity-75">&times;</button>' : ''}
169
- </div>
170
- `;
171
-
172
- if (dismissible) {
173
- const closeBtn = alert.querySelector('button');
174
- closeBtn.addEventListener('click', () => alert.remove());
175
- }
176
-
177
- if (duration > 0) {
178
- setTimeout(() => alert.remove(), duration);
179
- }
180
-
181
- return alert;
182
- }
183
-
184
- /**
185
- * Create a loading spinner
186
- */
187
- static createSpinner(config = {}) {
188
- const {
189
- size = 'medium', // small, medium, large
190
- text = 'Loading...'
191
- } = config;
192
-
193
- const sizeClasses = {
194
- 'small': 'w-4 h-4',
195
- 'medium': 'w-8 h-8',
196
- 'large': 'w-12 h-12'
197
- };
198
-
199
- const container = document.createElement('div');
200
- container.className = 'flex items-center gap-3 justify-center p-4';
201
- container.innerHTML = `
202
- <svg class="animate-spin ${sizeClasses[size] || sizeClasses['medium']} text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
203
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" opacity="0.25"></circle>
204
- <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
205
- </svg>
206
- <span class="text-gray-700 dark:text-gray-300">${UIComponents.escapeHtml(text)}</span>
207
- `;
208
- return container;
209
- }
210
-
211
- /**
212
- * Create a progress bar
213
- */
214
- static createProgressBar(config = {}) {
215
- const {
216
- percentage = 0,
217
- label = '',
218
- showLabel = true
219
- } = config;
220
-
221
- const container = document.createElement('div');
222
- container.className = 'progress-container';
223
-
224
- let html = '';
225
- if (label && showLabel) {
226
- html += `<div class="flex justify-between mb-2 text-sm"><span>${UIComponents.escapeHtml(label)}</span><span>${Math.round(percentage)}%</span></div>`;
227
- }
228
-
229
- html += `
230
- <div class="progress-bar bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
231
- <div class="progress-fill bg-blue-500 h-full transition-all" style="width: ${Math.min(100, Math.max(0, percentage))}%"></div>
232
- </div>
233
- `;
234
-
235
- container.innerHTML = html;
236
- return container;
237
- }
238
-
239
- /**
240
- * Create a collapsible section
241
- */
242
- static createCollapsible(config = {}) {
243
- const {
244
- title = 'Details',
245
- content = '',
246
- isOpen = false
247
- } = config;
248
-
249
- const container = document.createElement('div');
250
- container.className = 'collapsible';
251
-
252
- container.innerHTML = `
253
- <details ${isOpen ? 'open' : ''}>
254
- <summary class="cursor-pointer font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1 rounded transition-colors">
255
- ${UIComponents.escapeHtml(title)}
256
- </summary>
257
- <div class="content mt-2 ml-4">
258
- ${typeof content === 'string' ? content : ''}
259
- </div>
260
- </details>
261
- `;
262
-
263
- return container;
264
- }
265
-
266
- /**
267
- * Create a form input
268
- */
269
- static createInput(config = {}) {
270
- const {
271
- type = 'text',
272
- name = '',
273
- label = '',
274
- placeholder = '',
275
- value = '',
276
- required = false
277
- } = config;
278
-
279
- const container = document.createElement('div');
280
- container.className = 'form-group mb-4';
281
-
282
- let html = '';
283
- if (label) {
284
- html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
285
- }
286
-
287
- html += `
288
- <input
289
- type="${type}"
290
- name="${name}"
291
- placeholder="${UIComponents.escapeHtml(placeholder)}"
292
- value="${UIComponents.escapeHtml(value)}"
293
- ${required ? 'required' : ''}
294
- class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
295
- />
296
- `;
297
-
298
- container.innerHTML = html;
299
- return container;
300
- }
301
-
302
- /**
303
- * Create a select dropdown
304
- */
305
- static createSelect(config = {}) {
306
- const {
307
- name = '',
308
- label = '',
309
- options = [],
310
- value = '',
311
- required = false
312
- } = config;
313
-
314
- const container = document.createElement('div');
315
- container.className = 'form-group mb-4';
316
-
317
- let html = '';
318
- if (label) {
319
- html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
320
- }
321
-
322
- html += `
323
- <select
324
- name="${name}"
325
- ${required ? 'required' : ''}
326
- class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
327
- >
328
- ${options.map(opt => `
329
- <option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>
330
- ${UIComponents.escapeHtml(opt.label)}
331
- </option>
332
- `).join('')}
333
- </select>
334
- `;
335
-
336
- container.innerHTML = html;
337
- return container;
338
- }
339
-
340
- /**
341
- * Create a button group
342
- */
343
- static createButtonGroup(config = {}) {
344
- const {
345
- buttons = [],
346
- vertical = false
347
- } = config;
348
-
349
- const container = document.createElement('div');
350
- container.className = `button-group flex gap-2 ${vertical ? 'flex-col' : 'flex-row'}`;
351
-
352
- buttons.forEach(btn => {
353
- const button = document.createElement('button');
354
- button.className = `btn btn-${btn.variant || 'secondary'} flex-${vertical ? '1' : 'none'}`;
355
- button.textContent = btn.label;
356
- if (btn.onClick) {
357
- button.addEventListener('click', btn.onClick);
358
- }
359
- container.appendChild(button);
360
- });
361
-
362
- return container;
363
- }
364
-
365
- /**
366
- * Create a badge/tag
367
- */
368
- static createBadge(config = {}) {
369
- const {
370
- label = '',
371
- variant = 'default', // default, primary, success, warning, error
372
- size = 'medium' // small, medium, large
373
- } = config;
374
-
375
- const sizeClasses = {
376
- 'small': 'text-xs px-2 py-1',
377
- 'medium': 'text-sm px-3 py-1',
378
- 'large': 'text-base px-4 py-2'
379
- };
380
-
381
- const variantClasses = {
382
- 'default': 'bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
383
- 'primary': 'bg-blue-200 text-blue-800 dark:bg-blue-700 dark:text-blue-200',
384
- 'success': 'bg-green-200 text-green-800 dark:bg-green-700 dark:text-green-200',
385
- 'warning': 'bg-yellow-200 text-yellow-800 dark:bg-yellow-700 dark:text-yellow-200',
386
- 'error': 'bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200'
387
- };
388
-
389
- const badge = document.createElement('span');
390
- badge.className = `badge rounded-full font-medium ${sizeClasses[size] || sizeClasses['medium']} ${variantClasses[variant] || variantClasses['default']}`;
391
- badge.textContent = label;
392
- return badge;
393
- }
394
-
395
- /**
396
- * HTML escape utility
397
- */
398
- static escapeHtml(text) {
399
- return window._escHtml(text);
400
- }
401
-
402
- /**
403
- * Copy text to clipboard
404
- */
405
- static copyToClipboard(text) {
406
- return navigator.clipboard.writeText(text).catch(err => {
407
- console.error('Failed to copy:', err);
408
- return false;
409
- });
410
- }
411
-
412
- /**
413
- * Download data as file
414
- */
415
- static downloadFile(data, filename, mimeType = 'text/plain') {
416
- const blob = new Blob([data], { type: mimeType });
417
- const url = URL.createObjectURL(blob);
418
- const link = document.createElement('a');
419
- link.href = url;
420
- link.download = filename;
421
- document.body.appendChild(link);
422
- link.click();
423
- document.body.removeChild(link);
424
- URL.revokeObjectURL(url);
425
- }
426
- }
427
-
428
- // Export for use in browser
429
- if (typeof module !== 'undefined' && module.exports) {
430
- module.exports = UIComponents;
431
- }
1
+ /**
2
+ * UI Components
3
+ * Reusable UI building blocks for modals, tabs, buttons, and more
4
+ */
5
+
6
+ class UIComponents {
7
+ /**
8
+ * Create a modal dialog
9
+ */
10
+ static createModal(config = {}) {
11
+ const {
12
+ title = 'Dialog',
13
+ content = '',
14
+ buttons = [],
15
+ onClose = null,
16
+ size = 'medium' // small, medium, large
17
+ } = config;
18
+
19
+ const modal = document.createElement('div');
20
+ modal.className = 'modal-overlay';
21
+ modal.dataset.modal = 'true';
22
+
23
+ const sizeClasses = {
24
+ 'small': 'max-w-sm',
25
+ 'medium': 'max-w-md',
26
+ 'large': 'max-w-2xl'
27
+ };
28
+
29
+ modal.innerHTML = `
30
+ <div class="modal-content ${sizeClasses[size] || sizeClasses['medium']} bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6">
31
+ <div class="modal-header flex justify-between items-center mb-4 pb-4 border-b">
32
+ <h2 class="text-xl font-bold">${UIComponents.escapeHtml(title)}</h2>
33
+ <button class="modal-close text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 text-2xl leading-none">&times;</button>
34
+ </div>
35
+ <div class="modal-body mb-4">
36
+ ${typeof content === 'string' ? UIComponents.escapeHtml(content) : ''}
37
+ </div>
38
+ <div class="modal-footer flex gap-2 justify-end">
39
+ ${buttons.map(btn => `
40
+ <button class="btn btn-${btn.variant || 'secondary'}" data-action="${btn.action || 'close'}">
41
+ ${UIComponents.escapeHtml(btn.label)}
42
+ </button>
43
+ `).join('')}
44
+ </div>
45
+ </div>
46
+ `;
47
+
48
+ // Add close handler
49
+ const closeBtn = modal.querySelector('.modal-close');
50
+ closeBtn.addEventListener('click', () => {
51
+ modal.remove();
52
+ if (onClose) onClose();
53
+ });
54
+
55
+ // Add button handlers
56
+ modal.querySelectorAll('[data-action]').forEach(btn => {
57
+ btn.addEventListener('click', (e) => {
58
+ const action = e.target.dataset.action;
59
+ if (action === 'close') {
60
+ modal.remove();
61
+ if (onClose) onClose();
62
+ }
63
+ });
64
+ });
65
+
66
+ // Close on background click
67
+ modal.addEventListener('click', (e) => {
68
+ if (e.target === modal) {
69
+ modal.remove();
70
+ if (onClose) onClose();
71
+ }
72
+ });
73
+
74
+ return modal;
75
+ }
76
+
77
+ /**
78
+ * Create a tabbed interface
79
+ */
80
+ static createTabs(config = {}) {
81
+ const {
82
+ tabs = [],
83
+ activeTab = 0,
84
+ onChange = null
85
+ } = config;
86
+
87
+ const container = document.createElement('div');
88
+ container.className = 'tabs';
89
+
90
+ // Tab buttons
91
+ const tabButtons = document.createElement('div');
92
+ tabButtons.className = 'tab-buttons flex border-b';
93
+
94
+ tabs.forEach((tab, index) => {
95
+ const btn = document.createElement('button');
96
+ btn.className = `tab-button px-4 py-2 font-medium transition-colors ${
97
+ index === activeTab
98
+ ? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
99
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
100
+ }`;
101
+ btn.textContent = tab.label;
102
+ btn.dataset.tabIndex = index;
103
+
104
+ btn.addEventListener('click', () => {
105
+ // Update active button
106
+ tabButtons.querySelectorAll('.tab-button').forEach((b, i) => {
107
+ if (i === index) {
108
+ b.classList.add('border-b-2', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
109
+ b.classList.remove('text-gray-600', 'dark:text-gray-400');
110
+ } else {
111
+ b.classList.remove('border-b-2', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
112
+ b.classList.add('text-gray-600', 'dark:text-gray-400');
113
+ }
114
+ });
115
+
116
+ // Update tab content
117
+ tabContent.querySelectorAll('.tab-pane').forEach((pane, i) => {
118
+ pane.style.display = i === index ? 'block' : 'none';
119
+ });
120
+
121
+ if (onChange) onChange(index);
122
+ });
123
+
124
+ tabButtons.appendChild(btn);
125
+ });
126
+
127
+ container.appendChild(tabButtons);
128
+
129
+ // Tab content
130
+ const tabContent = document.createElement('div');
131
+ tabContent.className = 'tab-content mt-4';
132
+
133
+ tabs.forEach((tab, index) => {
134
+ const pane = document.createElement('div');
135
+ pane.className = 'tab-pane';
136
+ pane.style.display = index === activeTab ? 'block' : 'none';
137
+ pane.innerHTML = typeof tab.content === 'string' ? tab.content : '';
138
+ tabContent.appendChild(pane);
139
+ });
140
+
141
+ container.appendChild(tabContent);
142
+ return container;
143
+ }
144
+
145
+ /**
146
+ * Create an alert/notification
147
+ */
148
+ static createAlert(config = {}) {
149
+ const {
150
+ message = '',
151
+ type = 'info', // info, success, warning, error
152
+ duration = 5000,
153
+ dismissible = true
154
+ } = config;
155
+
156
+ const alert = document.createElement('div');
157
+ const typeClasses = {
158
+ 'info': 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-200',
159
+ 'success': 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-200',
160
+ 'warning': 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200',
161
+ 'error': 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-200'
162
+ };
163
+
164
+ alert.className = `alert border-l-4 p-4 mb-4 rounded ${typeClasses[type] || typeClasses['info']}`;
165
+ alert.innerHTML = `
166
+ <div class="flex justify-between items-center">
167
+ <span>${UIComponents.escapeHtml(message)}</span>
168
+ ${dismissible ? '<button class="text-current hover:opacity-75">&times;</button>' : ''}
169
+ </div>
170
+ `;
171
+
172
+ if (dismissible) {
173
+ const closeBtn = alert.querySelector('button');
174
+ closeBtn.addEventListener('click', () => alert.remove());
175
+ }
176
+
177
+ if (duration > 0) {
178
+ setTimeout(() => alert.remove(), duration);
179
+ }
180
+
181
+ return alert;
182
+ }
183
+
184
+ /**
185
+ * Create a loading spinner
186
+ */
187
+ static createSpinner(config = {}) {
188
+ const {
189
+ size = 'medium', // small, medium, large
190
+ text = 'Loading...'
191
+ } = config;
192
+
193
+ const sizeClasses = {
194
+ 'small': 'w-4 h-4',
195
+ 'medium': 'w-8 h-8',
196
+ 'large': 'w-12 h-12'
197
+ };
198
+
199
+ const container = document.createElement('div');
200
+ container.className = 'flex items-center gap-3 justify-center p-4';
201
+ container.innerHTML = `
202
+ <svg class="animate-spin ${sizeClasses[size] || sizeClasses['medium']} text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
203
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" opacity="0.25"></circle>
204
+ <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
205
+ </svg>
206
+ <span class="text-gray-700 dark:text-gray-300">${UIComponents.escapeHtml(text)}</span>
207
+ `;
208
+ return container;
209
+ }
210
+
211
+ /**
212
+ * Create a progress bar
213
+ */
214
+ static createProgressBar(config = {}) {
215
+ const {
216
+ percentage = 0,
217
+ label = '',
218
+ showLabel = true
219
+ } = config;
220
+
221
+ const container = document.createElement('div');
222
+ container.className = 'progress-container';
223
+
224
+ let html = '';
225
+ if (label && showLabel) {
226
+ html += `<div class="flex justify-between mb-2 text-sm"><span>${UIComponents.escapeHtml(label)}</span><span>${Math.round(percentage)}%</span></div>`;
227
+ }
228
+
229
+ html += `
230
+ <div class="progress-bar bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
231
+ <div class="progress-fill bg-blue-500 h-full transition-all" style="width: ${Math.min(100, Math.max(0, percentage))}%"></div>
232
+ </div>
233
+ `;
234
+
235
+ container.innerHTML = html;
236
+ return container;
237
+ }
238
+
239
+ /**
240
+ * Create a collapsible section
241
+ */
242
+ static createCollapsible(config = {}) {
243
+ const {
244
+ title = 'Details',
245
+ content = '',
246
+ isOpen = false
247
+ } = config;
248
+
249
+ const container = document.createElement('div');
250
+ container.className = 'collapsible';
251
+
252
+ container.innerHTML = `
253
+ <details ${isOpen ? 'open' : ''}>
254
+ <summary class="cursor-pointer font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1 rounded transition-colors">
255
+ ${UIComponents.escapeHtml(title)}
256
+ </summary>
257
+ <div class="content mt-2 ml-4">
258
+ ${typeof content === 'string' ? content : ''}
259
+ </div>
260
+ </details>
261
+ `;
262
+
263
+ return container;
264
+ }
265
+
266
+ /**
267
+ * Create a form input
268
+ */
269
+ static createInput(config = {}) {
270
+ const {
271
+ type = 'text',
272
+ name = '',
273
+ label = '',
274
+ placeholder = '',
275
+ value = '',
276
+ required = false
277
+ } = config;
278
+
279
+ const container = document.createElement('div');
280
+ container.className = 'form-group mb-4';
281
+
282
+ let html = '';
283
+ if (label) {
284
+ html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
285
+ }
286
+
287
+ html += `
288
+ <input
289
+ type="${type}"
290
+ name="${name}"
291
+ placeholder="${UIComponents.escapeHtml(placeholder)}"
292
+ value="${UIComponents.escapeHtml(value)}"
293
+ ${required ? 'required' : ''}
294
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
295
+ />
296
+ `;
297
+
298
+ container.innerHTML = html;
299
+ return container;
300
+ }
301
+
302
+ /**
303
+ * Create a select dropdown
304
+ */
305
+ static createSelect(config = {}) {
306
+ const {
307
+ name = '',
308
+ label = '',
309
+ options = [],
310
+ value = '',
311
+ required = false
312
+ } = config;
313
+
314
+ const container = document.createElement('div');
315
+ container.className = 'form-group mb-4';
316
+
317
+ let html = '';
318
+ if (label) {
319
+ html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
320
+ }
321
+
322
+ html += `
323
+ <select
324
+ name="${name}"
325
+ ${required ? 'required' : ''}
326
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
327
+ >
328
+ ${options.map(opt => `
329
+ <option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>
330
+ ${UIComponents.escapeHtml(opt.label)}
331
+ </option>
332
+ `).join('')}
333
+ </select>
334
+ `;
335
+
336
+ container.innerHTML = html;
337
+ return container;
338
+ }
339
+
340
+ /**
341
+ * Create a button group
342
+ */
343
+ static createButtonGroup(config = {}) {
344
+ const {
345
+ buttons = [],
346
+ vertical = false
347
+ } = config;
348
+
349
+ const container = document.createElement('div');
350
+ container.className = `button-group flex gap-2 ${vertical ? 'flex-col' : 'flex-row'}`;
351
+
352
+ buttons.forEach(btn => {
353
+ const button = document.createElement('button');
354
+ button.className = `btn btn-${btn.variant || 'secondary'} flex-${vertical ? '1' : 'none'}`;
355
+ button.textContent = btn.label;
356
+ if (btn.onClick) {
357
+ button.addEventListener('click', btn.onClick);
358
+ }
359
+ container.appendChild(button);
360
+ });
361
+
362
+ return container;
363
+ }
364
+
365
+ /**
366
+ * Create a badge/tag
367
+ */
368
+ static createBadge(config = {}) {
369
+ const {
370
+ label = '',
371
+ variant = 'default', // default, primary, success, warning, error
372
+ size = 'medium' // small, medium, large
373
+ } = config;
374
+
375
+ const sizeClasses = {
376
+ 'small': 'text-xs px-2 py-1',
377
+ 'medium': 'text-sm px-3 py-1',
378
+ 'large': 'text-base px-4 py-2'
379
+ };
380
+
381
+ const variantClasses = {
382
+ 'default': 'bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
383
+ 'primary': 'bg-blue-200 text-blue-800 dark:bg-blue-700 dark:text-blue-200',
384
+ 'success': 'bg-green-200 text-green-800 dark:bg-green-700 dark:text-green-200',
385
+ 'warning': 'bg-yellow-200 text-yellow-800 dark:bg-yellow-700 dark:text-yellow-200',
386
+ 'error': 'bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200'
387
+ };
388
+
389
+ const badge = document.createElement('span');
390
+ badge.className = `badge rounded-full font-medium ${sizeClasses[size] || sizeClasses['medium']} ${variantClasses[variant] || variantClasses['default']}`;
391
+ badge.textContent = label;
392
+ return badge;
393
+ }
394
+
395
+ /**
396
+ * HTML escape utility
397
+ */
398
+ static escapeHtml(text) {
399
+ return window._escHtml(text);
400
+ }
401
+
402
+ /**
403
+ * Copy text to clipboard
404
+ */
405
+ static copyToClipboard(text) {
406
+ return navigator.clipboard.writeText(text).catch(err => {
407
+ console.error('Failed to copy:', err);
408
+ return false;
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Download data as file
414
+ */
415
+ static downloadFile(data, filename, mimeType = 'text/plain') {
416
+ const blob = new Blob([data], { type: mimeType });
417
+ const url = URL.createObjectURL(blob);
418
+ const link = document.createElement('a');
419
+ link.href = url;
420
+ link.download = filename;
421
+ document.body.appendChild(link);
422
+ link.click();
423
+ document.body.removeChild(link);
424
+ URL.revokeObjectURL(url);
425
+ }
426
+ }
427
+
428
+ // Export for use in browser
429
+ if (typeof module !== 'undefined' && module.exports) {
430
+ module.exports = UIComponents;
431
+ }