@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.
- package/README.md +55 -13
- package/dist/css/main.css +554 -7
- package/package.json +3 -1
- package/snippets/buttons.html +51 -0
- package/snippets/cards.html +132 -47
- package/snippets/comparison.html +26 -22
- package/snippets/manifest.json +180 -115
- package/snippets/range-group.html +125 -0
- package/snippets/splitter.html +44 -38
- package/snippets/statistics.html +31 -0
- package/src/js/btn-split-auto-absorb.js +327 -0
- package/src/js/command-palette.js +472 -0
- package/src/js/file-selector.js +1275 -0
- package/src/js/internal/logging.js +121 -0
- package/src/js/logic-tree-renderer.js +303 -0
- package/src/js/modal-dialogs.js +460 -0
- package/src/js/overflow.js +371 -0
- package/src/js/pa-stat-fit.js +184 -0
- package/src/js/range-group.js +663 -0
- package/src/js/search-autocomplete-v2.js +907 -0
- package/src/js/search-autocomplete.js +434 -0
- package/src/js/settings-panel.js +245 -0
- package/src/js/split-button.js +141 -0
- package/src/js/splitter.js +1323 -0
- package/src/js/toast-service.js +302 -0
- package/src/js/tooltips-popovers.js +275 -0
- package/src/js/virtual-scroll.js +143 -0
- package/src/js/virtual-textbox.js +803 -0
- package/src/scss/_core.scss +7 -0
- package/src/scss/core-components/_buttons.scss +44 -0
- package/src/scss/core-components/_cards.scss +95 -6
- package/src/scss/core-components/_overflow.scss +50 -0
- package/src/scss/core-components/_range-group.scss +474 -0
- package/src/scss/core-components/_statistics.scss +163 -0
- 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);
|