thatcher 1.0.38 → 1.0.40
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/package.json +2 -2
- package/src/ui/perf-helpers.js +0 -96
- package/src/ui/validation-rules.js +0 -73
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thatcher",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"description": "A config-driven application framework for building data-intensive web apps without code.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"bun": ">=1.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"arctic": "^
|
|
52
|
+
"arctic": "^3.7.0",
|
|
53
53
|
"bcrypt": "^6.0.0",
|
|
54
54
|
"busybase": "^1.0.2",
|
|
55
55
|
"googleapis": "^173.0.0",
|
package/src/ui/perf-helpers.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { memoize } from '@/lib/render-cache.js'
|
|
2
|
-
|
|
3
|
-
export const renderLargeList = memoize(function renderLargeList(items, renderFn, options = {}) {
|
|
4
|
-
const { chunkSize = 100, useVirtual = false } = options
|
|
5
|
-
if (!items || items.length === 0) return ''
|
|
6
|
-
if (items.length <= chunkSize || !useVirtual) {
|
|
7
|
-
return items.map(renderFn).join('')
|
|
8
|
-
}
|
|
9
|
-
const visible = items.slice(0, chunkSize)
|
|
10
|
-
const deferred = items.slice(chunkSize)
|
|
11
|
-
return visible.map(renderFn).join('') +
|
|
12
|
-
`<tr class="defer-load" data-count="${deferred.length}"><td colspan="99" class="text-center text-gray-500 py-4">Loading ${deferred.length} more...</td></tr>`
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export const batchRender = memoize(function batchRender(items, renderFn, batchSize = 50) {
|
|
16
|
-
if (!items || items.length === 0) return ''
|
|
17
|
-
const batches = []
|
|
18
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
19
|
-
batches.push(items.slice(i, i + batchSize))
|
|
20
|
-
}
|
|
21
|
-
return batches.map((batch, idx) => {
|
|
22
|
-
const html = batch.map(renderFn).join('')
|
|
23
|
-
if (idx === 0) return html
|
|
24
|
-
return `<div class="defer-load" data-content="${escapeAttr(html)}"></div>`
|
|
25
|
-
}).join('')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
function escapeAttr(str) {
|
|
29
|
-
return String(str).replace(/"/g, '"').replace(/'/g, ''')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function eventDelegation(containerId, selector, eventType, handlerCode) {
|
|
33
|
-
return `
|
|
34
|
-
(function(){
|
|
35
|
-
const c=document.getElementById('${containerId}');
|
|
36
|
-
if(!c)return;
|
|
37
|
-
c.addEventListener('${eventType}',function(e){
|
|
38
|
-
const t=e.target.closest('${selector}');
|
|
39
|
-
if(t){${handlerCode}}
|
|
40
|
-
});
|
|
41
|
-
})();
|
|
42
|
-
`.trim()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function optimizeTableScroll(tableId) {
|
|
46
|
-
return `
|
|
47
|
-
(function(){
|
|
48
|
-
const t=document.getElementById('${tableId}');
|
|
49
|
-
if(!t)return;
|
|
50
|
-
let ticking=false;
|
|
51
|
-
t.addEventListener('scroll',()=>{
|
|
52
|
-
if(!ticking){
|
|
53
|
-
requestAnimationFrame(()=>{
|
|
54
|
-
ticking=false;
|
|
55
|
-
});
|
|
56
|
-
ticking=true;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
})();
|
|
60
|
-
`.trim()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const styleFragment = memoize(function styleFragment(styles) {
|
|
64
|
-
return `<style>${Object.entries(styles).map(([k, v]) => `${k}{${v}}`).join('')}</style>`
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
export function minifyHtml(html) {
|
|
68
|
-
return html
|
|
69
|
-
.replace(/\s+/g, ' ')
|
|
70
|
-
.replace(/>\s+</g, '><')
|
|
71
|
-
.replace(/\s+>/g, '>')
|
|
72
|
-
.replace(/<\s+/g, '<')
|
|
73
|
-
.trim()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export const progressiveEnhance = memoize(function progressiveEnhance(baseHtml, enhancedHtml) {
|
|
77
|
-
return baseHtml + `<noscript>${enhancedHtml}</noscript>`
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
export function createSkeleton(type = 'list', count = 3) {
|
|
81
|
-
if (type === 'list') {
|
|
82
|
-
return Array(count).fill(0).map(() =>
|
|
83
|
-
`<div class="skeleton-row" style="height:48px;background:#f3f4f6;margin:8px 0;border-radius:4px;animation:pulse 1.5s ease-in-out infinite"></div>`
|
|
84
|
-
).join('')
|
|
85
|
-
}
|
|
86
|
-
if (type === 'card') {
|
|
87
|
-
return Array(count).fill(0).map(() =>
|
|
88
|
-
`<div class="skeleton-card" style="width:100%;height:200px;background:#f3f4f6;border-radius:8px;animation:pulse 1.5s ease-in-out infinite"></div>`
|
|
89
|
-
).join('')
|
|
90
|
-
}
|
|
91
|
-
return ''
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function perfBudget(key, maxMs) {
|
|
95
|
-
return `performance.mark('${key}-start');(window.requestIdleCallback||function(cb){setTimeout(cb,1)})(()=>{performance.mark('${key}-end');const m=performance.measure('${key}','${key}-start','${key}-end');if(m.duration>${maxMs})console.warn('[PERF] ${key} exceeded budget: '+m.duration.toFixed(2)+'ms > ${maxMs}ms')});`
|
|
96
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
export const ValidationPatterns = {
|
|
2
|
-
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
3
|
-
phone: /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/,
|
|
4
|
-
url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)$/,
|
|
5
|
-
zip: /^\d{5}(-\d{4})?$/,
|
|
6
|
-
alpha: /^[a-zA-Z]+$/,
|
|
7
|
-
alphanumeric: /^[a-zA-Z0-9]+$/,
|
|
8
|
-
numeric: /^[0-9]+$/,
|
|
9
|
-
xss: /<script[^>]*>|javascript:|on\w+\s*=|<iframe|<object|<embed/gi
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const ValidationRules = {
|
|
13
|
-
required(value) {
|
|
14
|
-
return value !== null && value !== undefined && value !== '' ? null : 'This field is required';
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
minLength(value, min) {
|
|
18
|
-
return (!value || value.length >= min) ? null : `Minimum length is ${min} characters`;
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
maxLength(value, max) {
|
|
22
|
-
return (!value || value.length <= max) ? null : `Maximum length is ${max} characters`;
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
min(value, min) {
|
|
26
|
-
const num = Number(value);
|
|
27
|
-
return (!value || !isNaN(num) && num >= min) ? null : `Minimum value is ${min}`;
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
max(value, max) {
|
|
31
|
-
const num = Number(value);
|
|
32
|
-
return (!value || !isNaN(num) && num <= max) ? null : `Maximum value is ${max}`;
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
pattern(value, patternName) {
|
|
36
|
-
const pattern = ValidationPatterns[patternName];
|
|
37
|
-
if (!pattern) return `Unknown pattern: ${patternName}`;
|
|
38
|
-
return (!value || pattern.test(value)) ? null : `Invalid format for ${patternName}`;
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
email(value) {
|
|
42
|
-
return (!value || ValidationPatterns.email.test(value)) ? null : 'Invalid email address';
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
url(value) {
|
|
46
|
-
return (!value || ValidationPatterns.url.test(value)) ? null : 'Invalid URL';
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
phone(value) {
|
|
50
|
-
return (!value || ValidationPatterns.phone.test(value)) ? null : 'Invalid phone number';
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
noXSS(value) {
|
|
54
|
-
return (!value || !ValidationPatterns.xss.test(value)) ? null : 'Invalid characters detected';
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
match(value, matchFieldId) {
|
|
58
|
-
const matchField = document.getElementById(matchFieldId);
|
|
59
|
-
const matchValue = matchField ? matchField.value : '';
|
|
60
|
-
return (!value || value === matchValue) ? null : 'Values do not match';
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
fileSize(file, maxSizeBytes) {
|
|
64
|
-
return (!file || file.size <= maxSizeBytes) ? null : `File size must be under ${Math.round(maxSizeBytes / 1024 / 1024)}MB`;
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
fileType(file, allowedTypes) {
|
|
68
|
-
if (!file) return null;
|
|
69
|
-
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
70
|
-
const allowed = allowedTypes.split(',').map(t => t.trim().toLowerCase());
|
|
71
|
-
return allowed.includes(ext) ? null : `Only ${allowedTypes} files allowed`;
|
|
72
|
-
}
|
|
73
|
-
};
|