@xeplr/ui-utils 1.0.0

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,163 @@
1
+ import { useState, useRef } from 'react';
2
+ import { uploadFile } from './uploadFile.js';
3
+
4
+ /**
5
+ * @param {object} options
6
+ * @param {string[]} [options.allowedTypes] - MIME types: ['image/*', 'application/pdf']
7
+ * @param {number} [options.maxSize] - Max file size in bytes
8
+ * @param {number} [options.maxFiles=1] - Max number of files
9
+ * @param {string} [options.url] - Upload URL override
10
+ * @param {string} [options.fieldName='file'] - Form field name
11
+ * @param {object} [options.extraData] - Additional form data
12
+ * @param {function} [options.onSuccess] - Called with server response
13
+ * @param {function} [options.onError] - Called with error
14
+ */
15
+ export function useFileUploadController(options = {}) {
16
+ var [files, setFiles] = useState([]);
17
+ var [uploading, setUploading] = useState(false);
18
+ var [progress, setProgress] = useState(0);
19
+ var [error, setError] = useState('');
20
+ var [result, setResult] = useState(null);
21
+ var [dragOver, setDragOver] = useState(false);
22
+ var inputRef = useRef(null);
23
+
24
+ var allowedTypes = options.allowedTypes || ['*'];
25
+ var maxSize = options.maxSize || 5 * 1024 * 1024;
26
+ var maxFiles = options.maxFiles || 1;
27
+
28
+ function isTypeAllowed(mimetype) {
29
+ for (var i = 0; i < allowedTypes.length; i++) {
30
+ var allowed = allowedTypes[i];
31
+ if (allowed === '*') return true;
32
+ if (allowed.endsWith('/*')) {
33
+ var category = allowed.split('/')[0];
34
+ if (mimetype.startsWith(category + '/')) return true;
35
+ }
36
+ if (allowed === mimetype) return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function validate(fileList) {
42
+ var validated = [];
43
+ for (var i = 0; i < fileList.length; i++) {
44
+ var file = fileList[i];
45
+ if (!isTypeAllowed(file.type)) {
46
+ return { error: 'File type not allowed: ' + file.name };
47
+ }
48
+ if (file.size > maxSize) {
49
+ return { error: 'File too large: ' + file.name };
50
+ }
51
+ validated.push(file);
52
+ }
53
+ if (validated.length > maxFiles) {
54
+ return { error: 'Too many files. Maximum: ' + maxFiles };
55
+ }
56
+ return { files: validated };
57
+ }
58
+
59
+ function handleFiles(fileList) {
60
+ setError('');
61
+ setResult(null);
62
+ var validation = validate(fileList);
63
+ if (validation.error) {
64
+ setError(validation.error);
65
+ return;
66
+ }
67
+ setFiles(validation.files);
68
+ }
69
+
70
+ function handleChange(e) {
71
+ handleFiles(Array.from(e.target.files));
72
+ }
73
+
74
+ function handleDragOver(e) {
75
+ e.preventDefault();
76
+ setDragOver(true);
77
+ }
78
+
79
+ function handleDragLeave(e) {
80
+ e.preventDefault();
81
+ setDragOver(false);
82
+ }
83
+
84
+ function handleDrop(e) {
85
+ e.preventDefault();
86
+ setDragOver(false);
87
+ handleFiles(Array.from(e.dataTransfer.files));
88
+ }
89
+
90
+ function openFilePicker() {
91
+ if (inputRef.current) {
92
+ inputRef.current.click();
93
+ }
94
+ }
95
+
96
+ function removeFile(index) {
97
+ setFiles(function(prev) {
98
+ var next = prev.slice();
99
+ next.splice(index, 1);
100
+ return next;
101
+ });
102
+ }
103
+
104
+ function clear() {
105
+ setFiles([]);
106
+ setError('');
107
+ setResult(null);
108
+ setProgress(0);
109
+ if (inputRef.current) {
110
+ inputRef.current.value = '';
111
+ }
112
+ }
113
+
114
+ async function handleUpload() {
115
+ if (files.length === 0) return;
116
+ setError('');
117
+ setUploading(true);
118
+ setProgress(0);
119
+ try {
120
+ var response = await uploadFile(files, {
121
+ url: options.url,
122
+ fieldName: options.fieldName,
123
+ extraData: options.extraData,
124
+ onProgress: function(percent) {
125
+ setProgress(percent);
126
+ }
127
+ });
128
+ setResult(response);
129
+ if (options.onSuccess) options.onSuccess(response);
130
+ } catch (err) {
131
+ setError(err.message);
132
+ if (options.onError) options.onError(err);
133
+ } finally {
134
+ setUploading(false);
135
+ }
136
+ }
137
+
138
+ return {
139
+ // State
140
+ files,
141
+ uploading,
142
+ progress,
143
+ error,
144
+ result,
145
+ dragOver,
146
+ inputRef,
147
+
148
+ // Actions
149
+ handleChange,
150
+ handleDragOver,
151
+ handleDragLeave,
152
+ handleDrop,
153
+ handleUpload,
154
+ openFilePicker,
155
+ removeFile,
156
+ clear,
157
+
158
+ // Config (for view to read)
159
+ allowedTypes,
160
+ maxSize,
161
+ maxFiles
162
+ };
163
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Validates that required elements exist in the rendered design.
5
+ * Throws a visible error if any required element is missing.
6
+ *
7
+ * @param {string} componentName - Name of the page (for error messages)
8
+ * @param {Array<{id?: string, role?: string, selector?: string, label: string}>} requiredElements
9
+ */
10
+ export function useDesignValidator(componentName, requiredElements) {
11
+ var containerRef = useRef(null);
12
+
13
+ useEffect(function() {
14
+ if (!containerRef.current) return;
15
+
16
+ var missing = [];
17
+ for (var i = 0; i < requiredElements.length; i++) {
18
+ var rule = requiredElements[i];
19
+ var found = false;
20
+
21
+ if (rule.id) {
22
+ found = !!containerRef.current.querySelector('#' + rule.id);
23
+ } else if (rule.role) {
24
+ found = !!containerRef.current.querySelector('[role="' + rule.role + '"]');
25
+ } else if (rule.selector) {
26
+ found = !!containerRef.current.querySelector(rule.selector);
27
+ }
28
+
29
+ if (!found) {
30
+ missing.push(rule.label + (rule.id ? ' (id="' + rule.id + '")' : '') + (rule.selector ? ' (' + rule.selector + ')' : ''));
31
+ }
32
+ }
33
+
34
+ if (missing.length > 0) {
35
+ throw new Error(
36
+ '[xeplr-ui-utils] ' + componentName + ' design is missing required elements:\n' +
37
+ missing.map(function(m) { return ' - ' + m; }).join('\n')
38
+ );
39
+ }
40
+ }, []);
41
+
42
+ return containerRef;
43
+ }
44
+
45
+ export var FILE_UPLOAD_RULES = [
46
+ { selector: 'input[type="file"]', label: 'File input' },
47
+ { selector: '.xeplr-upload-dropzone, [role="dropzone"]', label: 'Drop zone area' },
48
+ { selector: 'button', label: 'Upload/action button' }
49
+ ];
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // File Upload — model
2
+ export { configureUpload, uploadFile } from './fileUpload/uploadFile.js';
3
+
4
+ // File Upload — controller
5
+ export { useFileUploadController } from './fileUpload/useFileUploadController.js';
6
+
7
+ // File Upload — design validation
8
+ export { useDesignValidator, FILE_UPLOAD_RULES } from './fileUpload/validateDesign.js';
9
+
10
+ // File Upload — sample design
11
+ export { FileUploadSample } from './fileUpload/designs/index.js';
12
+
13
+ // File Upload — ready-made page (controller + sample design wired)
14
+ export { FileUploadPage } from './fileUpload/pages.jsx';
15
+
16
+ // Snackbar
17
+ export { raiseSnackbar } from './snackbar/snackbar.js';
18
+
19
+ // Static data
20
+ export { default as COUNTRIES } from './data/countries.json';
21
+ import _statesData from './data/states.json';
22
+ export var STATES = _statesData.STATES;
23
+ export var STATES_IN = _statesData.STATES_IN;
@@ -0,0 +1,72 @@
1
+ var _container = null;
2
+ var _queue = [];
3
+ var _timer = null;
4
+
5
+ var DESIGNS = {
6
+ success: { bg: '#059669', color: '#fff' },
7
+ error: { bg: '#dc2626', color: '#fff' },
8
+ default: { bg: '#2563eb', color: '#fff' }
9
+ };
10
+
11
+ var DEFAULT_DURATION = 3000;
12
+
13
+ function _ensureContainer() {
14
+ if (_container) return _container;
15
+
16
+ _container = document.createElement('div');
17
+ _container.id = 'xeplr-snackbar-container';
18
+ _container.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;';
19
+ document.body.appendChild(_container);
20
+ return _container;
21
+ }
22
+
23
+ function _createEl(message, design, duration) {
24
+ var colors = DESIGNS[design] || DESIGNS.default;
25
+
26
+ var el = document.createElement('div');
27
+ el.style.cssText = 'padding:12px 20px;border-radius:6px;font:14px/1.4 system-ui,sans-serif;max-width:380px;word-break:break-word;pointer-events:auto;opacity:0;transform:translateY(8px);transition:opacity 0.2s,transform 0.2s;cursor:pointer;'
28
+ + 'background:' + colors.bg + ';color:' + colors.color + ';';
29
+ el.textContent = message;
30
+
31
+ el.onclick = function() { _dismiss(el); };
32
+
33
+ // Animate in
34
+ requestAnimationFrame(function() {
35
+ el.style.opacity = '1';
36
+ el.style.transform = 'translateY(0)';
37
+ });
38
+
39
+ // Auto dismiss
40
+ el._timeout = setTimeout(function() { _dismiss(el); }, duration);
41
+
42
+ return el;
43
+ }
44
+
45
+ function _dismiss(el) {
46
+ if (el._dismissed) return;
47
+ el._dismissed = true;
48
+ clearTimeout(el._timeout);
49
+ el.style.opacity = '0';
50
+ el.style.transform = 'translateY(8px)';
51
+ setTimeout(function() {
52
+ if (el.parentNode) el.parentNode.removeChild(el);
53
+ }, 200);
54
+ }
55
+
56
+ /**
57
+ * Show a snackbar notification.
58
+ *
59
+ * @param {string} message - Text to display
60
+ * @param {object} [options]
61
+ * @param {string} [options.design='default'] - 'success' | 'error' | 'default'
62
+ * @param {number} [options.duration=3000] - Auto-dismiss in ms
63
+ */
64
+ export function raiseSnackbar(message, options) {
65
+ options = options || {};
66
+ var design = options.design || 'default';
67
+ var duration = options.duration || DEFAULT_DURATION;
68
+
69
+ var container = _ensureContainer();
70
+ var el = _createEl(message, design, duration);
71
+ container.appendChild(el);
72
+ }