@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05

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 (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. package/src/scss/variables/_components.scss +41 -2
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Pure Admin Modal Dialogs
3
+ * Promise-based programmatic modal system (confirm, alert, prompt)
4
+ *
5
+ * Usage:
6
+ * const result = await PureAdmin.confirm({ title: 'Delete?', message: '...' });
7
+ * await PureAdmin.alert({ title: 'Success!', message: '...' });
8
+ * const value = await PureAdmin.prompt({ title: 'Enter name:', message: '...' });
9
+ *
10
+ * Options (all methods):
11
+ * position: 'center' | 'top' - Modal vertical position (default: 'center')
12
+ */
13
+
14
+ (function(window) {
15
+ 'use strict';
16
+
17
+ // Namespace
18
+ const PureAdmin = window.PureAdmin || {};
19
+ window.PureAdmin = PureAdmin;
20
+
21
+ // Modal counter for unique IDs
22
+ let modalCounter = 0;
23
+
24
+ /**
25
+ * Create modal element with given structure
26
+ */
27
+ function createModal(options) {
28
+ const {
29
+ id,
30
+ size = 'sm',
31
+ variant = null,
32
+ position = 'center', // 'center' or 'top'
33
+ title,
34
+ message,
35
+ footer
36
+ } = options;
37
+
38
+ const modal = document.createElement('div');
39
+ // Build modal class with optional variant on the wrapper (not header)
40
+ let modalClass = 'pa-modal pa-modal--show';
41
+ if (position === 'top') modalClass += ' pa-modal--top';
42
+ if (variant) modalClass += ` pa-modal--${variant}`;
43
+ modal.className = modalClass;
44
+ modal.id = id;
45
+ modal.setAttribute('role', 'dialog');
46
+ modal.setAttribute('aria-modal', 'true');
47
+ modal.setAttribute('aria-labelledby', `${id}-title`);
48
+
49
+ // Container size class
50
+ const containerClass = size === 'md'
51
+ ? 'pa-modal__container'
52
+ : `pa-modal__container pa-modal__container--${size}`;
53
+
54
+ // Header class - variant is on modal wrapper, not here
55
+ const headerClass = 'pa-modal__header';
56
+
57
+ modal.innerHTML = `
58
+ <div class="pa-modal__backdrop"></div>
59
+ <div class="${containerClass}">
60
+ <div class="${headerClass}">
61
+ <h3 class="pa-modal__title" id="${id}-title">${escapeHtml(title)}</h3>
62
+ </div>
63
+ <div class="pa-modal__body">
64
+ <p>${escapeHtml(message)}</p>
65
+ ${options.inputHtml || ''}
66
+ </div>
67
+ <div class="pa-modal__footer">
68
+ ${footer}
69
+ </div>
70
+ </div>
71
+ `;
72
+
73
+ return modal;
74
+ }
75
+
76
+ /**
77
+ * Show modal and return promise that resolves when user responds
78
+ */
79
+ function showModal(modal, options = {}) {
80
+ return new Promise((resolve) => {
81
+ // Calculate scrollbar width to prevent layout shift
82
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
83
+
84
+ // Add to DOM
85
+ document.body.appendChild(modal);
86
+ document.body.style.overflow = 'hidden'; // Prevent background scrolling
87
+ document.body.style.paddingRight = scrollbarWidth + 'px'; // Compensate for scrollbar
88
+
89
+ // Focus first input if exists, otherwise first button
90
+ setTimeout(() => {
91
+ const firstInput = modal.querySelector('input, textarea');
92
+ const firstButton = modal.querySelector('button');
93
+ if (firstInput) {
94
+ firstInput.focus();
95
+ } else if (firstButton) {
96
+ firstButton.focus();
97
+ }
98
+ }, 100);
99
+
100
+ // Store resolve function for cleanup
101
+ modal._resolve = resolve;
102
+
103
+ // Backdrop click to close (if enabled)
104
+ if (options.closeOnBackdrop !== false) {
105
+ const backdrop = modal.querySelector('.pa-modal__backdrop');
106
+ if (backdrop) {
107
+ backdrop.addEventListener('click', () => {
108
+ closeModal(modal, options.cancelValue);
109
+ });
110
+ }
111
+ }
112
+
113
+ // ESC key to close
114
+ const escHandler = (e) => {
115
+ if (e.key === 'Escape') {
116
+ closeModal(modal, options.cancelValue);
117
+ document.removeEventListener('keydown', escHandler);
118
+ }
119
+ };
120
+ document.addEventListener('keydown', escHandler);
121
+ modal._escHandler = escHandler;
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Close modal and resolve promise
127
+ */
128
+ function closeModal(modal, value) {
129
+ if (!modal._resolve) return;
130
+
131
+ // Remove show class (triggers fade out)
132
+ modal.classList.remove('pa-modal--show');
133
+
134
+ // Wait for animation, then remove from DOM
135
+ setTimeout(() => {
136
+ // Clean up event listeners
137
+ if (modal._escHandler) {
138
+ document.removeEventListener('keydown', modal._escHandler);
139
+ }
140
+
141
+ // Restore body overflow and padding
142
+ document.body.style.overflow = '';
143
+ document.body.style.paddingRight = '';
144
+
145
+ // Resolve promise
146
+ modal._resolve(value);
147
+ modal._resolve = null;
148
+
149
+ // Remove from DOM
150
+ if (modal.parentNode) {
151
+ modal.parentNode.removeChild(modal);
152
+ }
153
+ }, 300); // Match modal transition time
154
+ }
155
+
156
+ /**
157
+ * Escape HTML to prevent XSS
158
+ */
159
+ function escapeHtml(text) {
160
+ const div = document.createElement('div');
161
+ div.textContent = text;
162
+ return div.innerHTML;
163
+ }
164
+
165
+ /**
166
+ * CONFIRM DIALOG
167
+ * Shows a confirmation dialog with OK/Cancel buttons
168
+ * Returns Promise<boolean> - true if confirmed, false if cancelled
169
+ */
170
+ PureAdmin.confirm = function(options = {}) {
171
+ const {
172
+ title = 'Confirm',
173
+ message = 'Are you sure?',
174
+ confirmText = 'OK',
175
+ cancelText = 'Cancel',
176
+ variant = 'primary',
177
+ size = 'sm',
178
+ position = 'center',
179
+ confirmVariant = variant,
180
+ closeOnBackdrop = true
181
+ } = options;
182
+
183
+ const id = `pa-modal-confirm-${++modalCounter}`;
184
+
185
+ // Create footer with two buttons
186
+ const footer = `
187
+ <button type="button" class="pa-btn pa-btn--secondary" data-action="cancel">
188
+ ${escapeHtml(cancelText)}
189
+ </button>
190
+ <button type="button" class="pa-btn pa-btn--${confirmVariant}" data-action="confirm">
191
+ ${escapeHtml(confirmText)}
192
+ </button>
193
+ `;
194
+
195
+ const modal = createModal({
196
+ id,
197
+ size,
198
+ variant,
199
+ position,
200
+ title,
201
+ message,
202
+ footer
203
+ });
204
+
205
+ // Attach button handlers
206
+ const confirmBtn = modal.querySelector('[data-action="confirm"]');
207
+ const cancelBtn = modal.querySelector('[data-action="cancel"]');
208
+
209
+ confirmBtn.addEventListener('click', () => closeModal(modal, true));
210
+ cancelBtn.addEventListener('click', () => closeModal(modal, false));
211
+
212
+ // Enter key confirms
213
+ const enterHandler = (e) => {
214
+ if (e.key === 'Enter' && !e.shiftKey) {
215
+ e.preventDefault();
216
+ closeModal(modal, true);
217
+ document.removeEventListener('keydown', enterHandler);
218
+ }
219
+ };
220
+ document.addEventListener('keydown', enterHandler);
221
+ modal._enterHandler = enterHandler;
222
+
223
+ return showModal(modal, { closeOnBackdrop, cancelValue: false });
224
+ };
225
+
226
+ /**
227
+ * ALERT DIALOG
228
+ * Shows an alert dialog with single OK button
229
+ * Returns Promise<void> - resolves when user clicks OK
230
+ */
231
+ PureAdmin.alert = function(options = {}) {
232
+ const {
233
+ title = 'Alert',
234
+ message = '',
235
+ okText = 'OK',
236
+ variant = 'primary',
237
+ size = 'sm',
238
+ position = 'center',
239
+ closeOnBackdrop = true
240
+ } = options;
241
+
242
+ const id = `pa-modal-alert-${++modalCounter}`;
243
+
244
+ // Create footer with single button
245
+ const footer = `
246
+ <button type="button" class="pa-btn pa-btn--${variant}" data-action="ok">
247
+ ${escapeHtml(okText)}
248
+ </button>
249
+ `;
250
+
251
+ const modal = createModal({
252
+ id,
253
+ size,
254
+ variant,
255
+ position,
256
+ title,
257
+ message,
258
+ footer
259
+ });
260
+
261
+ // Attach button handler
262
+ const okBtn = modal.querySelector('[data-action="ok"]');
263
+ okBtn.addEventListener('click', () => closeModal(modal, true));
264
+
265
+ // Enter key confirms
266
+ const enterHandler = (e) => {
267
+ if (e.key === 'Enter') {
268
+ e.preventDefault();
269
+ closeModal(modal, true);
270
+ document.removeEventListener('keydown', enterHandler);
271
+ }
272
+ };
273
+ document.addEventListener('keydown', enterHandler);
274
+ modal._enterHandler = enterHandler;
275
+
276
+ return showModal(modal, { closeOnBackdrop, cancelValue: true });
277
+ };
278
+
279
+ /**
280
+ * PROMPT DIALOG
281
+ * Shows a prompt dialog with text input
282
+ * Returns Promise<string | null> - string if submitted, null if cancelled
283
+ */
284
+ PureAdmin.prompt = function(options = {}) {
285
+ const {
286
+ title = 'Input',
287
+ message = 'Enter value:',
288
+ defaultValue = '',
289
+ placeholder = '',
290
+ confirmText = 'OK',
291
+ cancelText = 'Cancel',
292
+ variant = 'primary',
293
+ size = 'sm',
294
+ position = 'center',
295
+ validator = null,
296
+ closeOnBackdrop = true
297
+ } = options;
298
+
299
+ const id = `pa-modal-prompt-${++modalCounter}`;
300
+ const inputId = `${id}-input`;
301
+ const errorId = `${id}-error`;
302
+
303
+ // Create input HTML
304
+ const inputHtml = `
305
+ <div class="pa-form-group" style="margin-top: 1rem;">
306
+ <div class="pa-input-wrapper">
307
+ <input
308
+ type="text"
309
+ id="${inputId}"
310
+ class="pa-input"
311
+ value="${escapeHtml(defaultValue)}"
312
+ placeholder="${escapeHtml(placeholder)}"
313
+ aria-describedby="${errorId}"
314
+ />
315
+ </div>
316
+ <div id="${errorId}" class="pa-form-error" style="display: none;"></div>
317
+ </div>
318
+ `;
319
+
320
+ // Create footer with two buttons
321
+ const footer = `
322
+ <button type="button" class="pa-btn pa-btn--secondary" data-action="cancel">
323
+ ${escapeHtml(cancelText)}
324
+ </button>
325
+ <button type="button" class="pa-btn pa-btn--${variant}" data-action="confirm">
326
+ ${escapeHtml(confirmText)}
327
+ </button>
328
+ `;
329
+
330
+ const modal = createModal({
331
+ id,
332
+ size,
333
+ variant,
334
+ position,
335
+ title,
336
+ message,
337
+ inputHtml,
338
+ footer
339
+ });
340
+
341
+ // Get elements
342
+ const input = modal.querySelector(`#${inputId}`);
343
+ const errorDiv = modal.querySelector(`#${errorId}`);
344
+ const confirmBtn = modal.querySelector('[data-action="confirm"]');
345
+ const cancelBtn = modal.querySelector('[data-action="cancel"]');
346
+
347
+ // Validation function
348
+ function validate() {
349
+ if (!validator) return true;
350
+
351
+ const value = input.value;
352
+ const result = validator(value);
353
+
354
+ if (result === true) {
355
+ input.classList.remove('pa-input--error');
356
+ errorDiv.style.display = 'none';
357
+ return true;
358
+ } else {
359
+ input.classList.add('pa-input--error');
360
+ errorDiv.textContent = typeof result === 'string' ? result : 'Invalid input';
361
+ errorDiv.style.display = 'block';
362
+ return false;
363
+ }
364
+ }
365
+
366
+ // Attach button handlers
367
+ confirmBtn.addEventListener('click', () => {
368
+ if (validate()) {
369
+ closeModal(modal, input.value);
370
+ }
371
+ });
372
+
373
+ cancelBtn.addEventListener('click', () => closeModal(modal, null));
374
+
375
+ // Enter key submits
376
+ input.addEventListener('keydown', (e) => {
377
+ if (e.key === 'Enter') {
378
+ e.preventDefault();
379
+ if (validate()) {
380
+ closeModal(modal, input.value);
381
+ }
382
+ }
383
+ });
384
+
385
+ // Clear error on input
386
+ if (validator) {
387
+ input.addEventListener('input', () => {
388
+ if (errorDiv.style.display !== 'none') {
389
+ validate();
390
+ }
391
+ });
392
+ }
393
+
394
+ return showModal(modal, { closeOnBackdrop: false, cancelValue: null });
395
+ };
396
+
397
+ /**
398
+ * CUSTOM DIALOG
399
+ * Advanced API for fully custom modal content
400
+ * Returns Promise that resolves with whatever value you pass to resolve()
401
+ */
402
+ PureAdmin.custom = function(options = {}) {
403
+ const {
404
+ title = 'Dialog',
405
+ size = 'md',
406
+ variant = null,
407
+ position = 'center',
408
+ closeOnBackdrop = true,
409
+ render
410
+ } = options;
411
+
412
+ if (typeof render !== 'function') {
413
+ throw new Error('PureAdmin.custom() requires a render function');
414
+ }
415
+
416
+ const id = `pa-modal-custom-${++modalCounter}`;
417
+
418
+ const modal = document.createElement('div');
419
+ // Build modal class with optional variant on the wrapper (not header)
420
+ let modalClass = 'pa-modal pa-modal--show';
421
+ if (position === 'top') modalClass += ' pa-modal--top';
422
+ if (variant) modalClass += ` pa-modal--${variant}`;
423
+ modal.className = modalClass;
424
+ modal.id = id;
425
+ modal.setAttribute('role', 'dialog');
426
+ modal.setAttribute('aria-modal', 'true');
427
+
428
+ // Container size class
429
+ const containerClass = size === 'md'
430
+ ? 'pa-modal__container'
431
+ : `pa-modal__container pa-modal__container--${size}`;
432
+
433
+ // Header class - variant is on modal wrapper, not here
434
+ const headerClass = 'pa-modal__header';
435
+
436
+ // Create backdrop
437
+ const backdrop = document.createElement('div');
438
+ backdrop.className = 'pa-modal__backdrop';
439
+ modal.appendChild(backdrop);
440
+
441
+ // Create container
442
+ const container = document.createElement('div');
443
+ container.className = containerClass;
444
+
445
+ // Create header
446
+ const headerDiv = document.createElement('div');
447
+ headerDiv.className = headerClass;
448
+ headerDiv.innerHTML = `<h3 class="pa-modal__title">${escapeHtml(title)}</h3>`;
449
+ container.appendChild(headerDiv);
450
+
451
+ modal.appendChild(container);
452
+
453
+ // Render function gets container and close callback
454
+ const closeCallback = (value) => closeModal(modal, value);
455
+ render(container, closeCallback);
456
+
457
+ return showModal(modal, { closeOnBackdrop, cancelValue: null });
458
+ };
459
+
460
+ })(window);