thatcher 1.0.4

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,23 @@
1
+ export function generateTestPage() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>System Test | MOONLANDING</title>
8
+ <style>
9
+ body { font-family: monospace; padding: 2rem; background: #04141f; color: #ced4da; }
10
+ h1 { color: #3b82f6; }
11
+ .check { color: #40c057; }
12
+ .info { color: #868e96; font-size: 0.85rem; margin-top: 1rem; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <h1>MOONLANDING System Test</h1>
17
+ <p class="check">✓ Server is running</p>
18
+ <p class="check">✓ Page handler is operational</p>
19
+ <p class="info">Timestamp: ${new Date().toISOString()}</p>
20
+ <p class="info">Environment: ${process.env.NODE_ENV || 'development'}</p>
21
+ </body>
22
+ </html>`;
23
+ }
@@ -0,0 +1,73 @@
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
+ };
@@ -0,0 +1,147 @@
1
+ import { ValidationRules } from '@/ui/validation-rules';
2
+
3
+ export const ClientValidator = {
4
+ validateField(input, rules) {
5
+ const errors = [];
6
+ const value = input.type === 'file' ? input.files[0] : input.value;
7
+
8
+ for (const rule of rules) {
9
+ let error;
10
+ if (typeof rule === 'string') {
11
+ error = ValidationRules[rule]?.(value);
12
+ } else if (typeof rule === 'object') {
13
+ const [ruleName, ...args] = Object.entries(rule)[0];
14
+ error = ValidationRules[ruleName]?.(value, ...args);
15
+ }
16
+ if (error) errors.push(error);
17
+ }
18
+
19
+ return errors;
20
+ },
21
+
22
+ showFieldError(input, errors) {
23
+ ClientValidator.clearFieldError(input);
24
+
25
+ if (errors.length === 0) {
26
+ input.classList.remove('input-error', 'select-error', 'textarea-error');
27
+ input.classList.add('input-success');
28
+ input.removeAttribute('aria-invalid');
29
+ input.removeAttribute('aria-describedby');
30
+ return;
31
+ }
32
+
33
+ input.classList.remove('input-success');
34
+ input.classList.add('input-error');
35
+ input.setAttribute('aria-invalid', 'true');
36
+
37
+ const errorId = 'err-' + (input.id || input.name);
38
+ const errorDiv = document.createElement('div');
39
+ errorDiv.className = 'field-error text-error text-xs mt-1';
40
+ errorDiv.id = errorId;
41
+ errorDiv.setAttribute('role', 'alert');
42
+ errorDiv.textContent = errors[0];
43
+ errorDiv.dataset.fieldError = input.id || input.name;
44
+
45
+ input.setAttribute('aria-describedby', errorId);
46
+ input.parentNode.insertBefore(errorDiv, input.nextSibling);
47
+ },
48
+
49
+ clearFieldError(input) {
50
+ const existingError = input.parentNode.querySelector(`[data-field-error="${input.id || input.name}"]`);
51
+ if (existingError) existingError.remove();
52
+ input.classList.remove('input-error', 'input-success', 'select-error', 'textarea-error');
53
+ },
54
+
55
+ attachRealTimeValidation(form) {
56
+ const inputs = form.querySelectorAll('[data-validate]');
57
+
58
+ inputs.forEach(input => {
59
+ const rules = JSON.parse(input.dataset.validate || '[]');
60
+
61
+ const validate = () => {
62
+ const errors = ClientValidator.validateField(input, rules);
63
+ ClientValidator.showFieldError(input, errors);
64
+ };
65
+
66
+ input.addEventListener('blur', validate);
67
+ input.addEventListener('input', () => {
68
+ if (input.classList.contains('input-error')) {
69
+ validate();
70
+ }
71
+ });
72
+ });
73
+ },
74
+
75
+ validateForm(form) {
76
+ const inputs = form.querySelectorAll('[data-validate]');
77
+ let allValid = true;
78
+ const allErrors = {};
79
+
80
+ inputs.forEach(input => {
81
+ const rules = JSON.parse(input.dataset.validate || '[]');
82
+ const errors = ClientValidator.validateField(input, rules);
83
+
84
+ ClientValidator.showFieldError(input, errors);
85
+
86
+ if (errors.length > 0) {
87
+ allValid = false;
88
+ allErrors[input.id || input.name] = errors;
89
+ }
90
+ });
91
+
92
+ return { valid: allValid, errors: allErrors };
93
+ },
94
+
95
+ showFormSummary(form, errors) {
96
+ ClientValidator.clearFormSummary(form);
97
+
98
+ if (Object.keys(errors).length === 0) return;
99
+
100
+ const summary = document.createElement('div');
101
+ summary.className = 'alert alert-error mb-4';
102
+ summary.dataset.formSummary = 'true';
103
+
104
+ const heading = document.createElement('strong');
105
+ heading.textContent = 'Please fix the following errors:';
106
+
107
+ const ul = document.createElement('ul');
108
+ ul.className = 'list-disc ml-4 mt-2';
109
+
110
+ Object.entries(errors).forEach(([field, errs]) => {
111
+ const li = document.createElement('li');
112
+ li.textContent = `${field}: ${errs[0]}`;
113
+ ul.appendChild(li);
114
+ });
115
+
116
+ const wrapper = document.createElement('div');
117
+ wrapper.appendChild(heading);
118
+ wrapper.appendChild(ul);
119
+ summary.appendChild(wrapper);
120
+
121
+ form.insertBefore(summary, form.firstChild);
122
+ },
123
+
124
+ clearFormSummary(form) {
125
+ const existing = form.querySelector('[data-form-summary]');
126
+ if (existing) existing.remove();
127
+ }
128
+ };
129
+
130
+ if (typeof window !== 'undefined') {
131
+ window.ClientValidator = ClientValidator;
132
+
133
+ document.addEventListener('DOMContentLoaded', () => {
134
+ document.querySelectorAll('form[data-validate-form]').forEach(form => {
135
+ ClientValidator.attachRealTimeValidation(form);
136
+
137
+ form.addEventListener('submit', (e) => {
138
+ const result = ClientValidator.validateForm(form);
139
+ if (!result.valid) {
140
+ e.preventDefault();
141
+ ClientValidator.showFormSummary(form, result.errors);
142
+ form.querySelector('.input-error')?.focus();
143
+ }
144
+ });
145
+ });
146
+ });
147
+ }
@@ -0,0 +1,107 @@
1
+ export function virtualScrollScript(containerId, rowHeight = 50, buffer = 5) {
2
+ return `
3
+ (function(){
4
+ const c=document.getElementById('${containerId}');
5
+ if(!c)return;
6
+ const rh=${rowHeight};
7
+ const buf=${buffer};
8
+ let allRows=[];
9
+ let scrollT=null;
10
+
11
+ function init(){
12
+ allRows=Array.from(c.querySelectorAll('tbody tr'));
13
+ if(allRows.length<50)return;
14
+ const tbody=c.querySelector('tbody');
15
+ const wrap=document.createElement('div');
16
+ wrap.style.position='relative';
17
+ wrap.style.height=(allRows.length*rh)+'px';
18
+ tbody.innerHTML='';
19
+ tbody.appendChild(wrap);
20
+ c.addEventListener('scroll',()=>{
21
+ clearTimeout(scrollT);
22
+ scrollT=setTimeout(render,16);
23
+ });
24
+ render();
25
+ }
26
+
27
+ function render(){
28
+ const st=c.scrollTop||0;
29
+ const ch=c.clientHeight||600;
30
+ const start=Math.max(0,Math.floor(st/rh)-buf);
31
+ const end=Math.min(allRows.length,Math.ceil((st+ch)/rh)+buf);
32
+ const tbody=c.querySelector('tbody>div');
33
+ if(!tbody)return;
34
+ tbody.innerHTML='';
35
+ for(let i=start;i<end;i++){
36
+ const r=allRows[i].cloneNode(true);
37
+ r.style.position='absolute';
38
+ r.style.top=(i*rh)+'px';
39
+ r.style.left='0';
40
+ r.style.right='0';
41
+ tbody.appendChild(r);
42
+ }
43
+ }
44
+
45
+ if(document.readyState==='loading'){
46
+ document.addEventListener('DOMContentLoaded',init);
47
+ }else{
48
+ init();
49
+ }
50
+ })();
51
+ `.trim()
52
+ }
53
+
54
+ export function lazyLoadImages() {
55
+ return `
56
+ (function(){
57
+ const obs=new IntersectionObserver((entries)=>{
58
+ entries.forEach(e=>{
59
+ if(e.isIntersecting){
60
+ const img=e.target;
61
+ if(img.dataset.src){
62
+ img.src=img.dataset.src;
63
+ delete img.dataset.src;
64
+ obs.unobserve(img);
65
+ }
66
+ }
67
+ });
68
+ },{rootMargin:'50px'});
69
+ document.querySelectorAll('img[data-src]').forEach(img=>obs.observe(img));
70
+ })();
71
+ `.trim()
72
+ }
73
+
74
+ export function deferOffscreen(selector = '.defer-load') {
75
+ return `
76
+ (function(){
77
+ const obs=new IntersectionObserver((entries)=>{
78
+ entries.forEach(e=>{
79
+ if(e.isIntersecting){
80
+ const el=e.target;
81
+ const html=el.dataset.content;
82
+ if(html){
83
+ el.innerHTML=html;
84
+ delete el.dataset.content;
85
+ obs.unobserve(el);
86
+ }
87
+ }
88
+ });
89
+ },{rootMargin:'100px'});
90
+ document.querySelectorAll('${selector}').forEach(el=>obs.observe(el));
91
+ })();
92
+ `.trim()
93
+ }
94
+
95
+ export function debounceInput(inputId, callback, delay = 300) {
96
+ return `
97
+ (function(){
98
+ const inp=document.getElementById('${inputId}');
99
+ if(!inp)return;
100
+ let t=null;
101
+ inp.addEventListener('input',()=>{
102
+ clearTimeout(t);
103
+ t=setTimeout(()=>{${callback}},${delay});
104
+ });
105
+ })();
106
+ `.trim()
107
+ }
@@ -0,0 +1,61 @@
1
+ const VOID_ELEMENTS = new Set([
2
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
3
+ 'link', 'meta', 'param', 'source', 'track', 'wbr', 'circle', 'path', 'line', 'rect'
4
+ ])
5
+
6
+ function escapeHtml(str) {
7
+ if (str === null || str === undefined) return ''
8
+ return String(str)
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ }
14
+
15
+ function renderAttr(key, value) {
16
+ if (value === null || value === undefined || value === false) return ''
17
+ if (typeof value === 'function') return ''
18
+ if (key === 'children' || key === 'innerHTML') return ''
19
+ if (key === 'className') key = 'class'
20
+ if (value === true) return ` ${key}`
21
+ if (key === 'style' && typeof value === 'object') {
22
+ const css = Object.entries(value)
23
+ .map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
24
+ .join(';')
25
+ return ` style="${escapeHtml(css)}"`
26
+ }
27
+ return ` ${key}="${escapeHtml(String(value))}"`
28
+ }
29
+
30
+ function renderChildren(children) {
31
+ if (children === null || children === undefined || children === false) return ''
32
+ if (typeof children === 'string' || typeof children === 'number') return String(children)
33
+ if (Array.isArray(children)) return children.map(renderChildren).join('')
34
+ return String(children)
35
+ }
36
+
37
+ export function h(type, props, ...children) {
38
+ if (typeof type === 'function') {
39
+ const merged = { ...props }
40
+ const kids = children.length === 1 ? children[0] : children.length > 0 ? children : undefined
41
+ if (kids !== undefined) merged.children = kids
42
+ return type(merged)
43
+ }
44
+
45
+ const attrs = props ? Object.entries(props).map(([k, v]) => renderAttr(k, v)).join('') : ''
46
+ const tag = type
47
+
48
+ if (VOID_ELEMENTS.has(tag)) {
49
+ return `<${tag}${attrs} />`
50
+ }
51
+
52
+ const inner = props?.innerHTML
53
+ ? props.innerHTML
54
+ : renderChildren(children.length > 0 ? children : props?.children)
55
+
56
+ return `<${tag}${attrs}>${inner}</${tag}>`
57
+ }
58
+
59
+ export function Fragment({ children }) {
60
+ return renderChildren(children)
61
+ }
@@ -0,0 +1,152 @@
1
+ import { h } from '@/ui/webjsx.js'
2
+ import { STAGE_COLORS, AVATAR_COLORS, AVATAR_SIZES, nameHash, getInitials } from '@/ui/render-helpers.js'
3
+
4
+ export function linearProgress(value = 0, max = 100, label = '', variant = 'medium') {
5
+ const pct = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0
6
+ const ht = variant === 'thin' ? '4px' : variant === 'thick' ? '16px' : '8px'
7
+ const color = pct < 30 ? '#ef4444' : pct < 70 ? '#f59e0b' : '#22c55e'
8
+ const ariaLabel = label || 'Progress'
9
+ return h('div', { className: 'linear-progress-wrap' },
10
+ label ? h('span', { className: 'progress-label' }, label) : '',
11
+ h('div', { className: 'linear-progress', style: `height:${ht}`, role: 'progressbar', 'aria-valuenow': String(pct), 'aria-valuemin': '0', 'aria-valuemax': '100', 'aria-label': ariaLabel },
12
+ h('div', { className: 'linear-progress-bar', style: `width:${pct}%;height:100%;background:${color}` })
13
+ ),
14
+ h('span', { className: 'progress-pct', style: `color:${color}`, 'aria-hidden': 'true' }, `${pct}%`)
15
+ )
16
+ }
17
+
18
+ export function circularProgress(value = 0, max = 100, label = '') {
19
+ const pct = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0
20
+ const color = pct < 30 ? '#ef4444' : pct < 70 ? '#f59e0b' : '#22c55e'
21
+ const r = 40, circ = 2 * Math.PI * r, offset = circ - (pct / 100) * circ
22
+ const ariaLabel = label || 'Progress'
23
+ return `<div class="circular-progress" style="width:100px;height:100px" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="${ariaLabel}">
24
+ <svg width="100" height="100" viewBox="0 0 100 100" aria-hidden="true">
25
+ <circle cx="50" cy="50" r="${r}" fill="none" stroke="#e5e7eb" stroke-width="8"/>
26
+ <circle cx="50" cy="50" r="${r}" fill="none" stroke="${color}" stroke-width="8"
27
+ stroke-dasharray="${circ}" stroke-dashoffset="${offset}" stroke-linecap="round"/>
28
+ </svg>
29
+ <div class="circular-progress-text" aria-hidden="true">
30
+ <span class="circular-progress-pct">${pct}%</span>
31
+ ${label ? `<span class="circular-progress-label">${label}</span>` : ''}
32
+ </div>
33
+ </div>`
34
+ }
35
+
36
+ export function engagementProgress(stage, stages = null) {
37
+ const stageKeys = stages || Object.keys(STAGE_COLORS)
38
+ const activeIdx = stageKeys.indexOf(stage)
39
+ const pills = stageKeys.map((key, i) => {
40
+ const s = STAGE_COLORS[key] || { bg: '#f3f4f6', text: '#4b5563', label: key }
41
+ const cls = i === activeIdx ? 'stage-active' : i < activeIdx ? 'stage-completed' : ''
42
+ return h('div', { className: `stage-pill ${cls}`, style: `background:${s.bg};color:${s.text}` }, s.label)
43
+ }).join('')
44
+ return h('div', { className: 'engagement-pipeline' }, pills)
45
+ }
46
+
47
+ export function emptyState(icon = '', title = '', message = '', actionHref = '', actionLabel = '') {
48
+ const btn = actionHref && actionLabel ? h('a', { href: actionHref, className: 'btn btn-primary btn-sm' }, actionLabel) : ''
49
+ return h('div', { className: 'empty-state' },
50
+ h('div', { className: 'empty-state-icon' }, icon),
51
+ h('div', { className: 'empty-state-title' }, title),
52
+ h('div', { className: 'empty-state-msg' }, message),
53
+ btn
54
+ )
55
+ }
56
+
57
+ export function getUserAvatarUrl(user) {
58
+ const name = user?.name || user?.email || 'User'
59
+ const initials = getInitials(name)
60
+ const color = AVATAR_COLORS[nameHash(name) % AVATAR_COLORS.length]
61
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" aria-hidden="true"><rect width="48" height="48" rx="24" fill="${color}"/><text x="24" y="24" dy=".35em" text-anchor="middle" fill="white" font-family="sans-serif" font-size="18" font-weight="600">${initials}</text></svg>`
62
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`
63
+ }
64
+
65
+ export function userAvatar(user, size = 'md', showStatus = false) {
66
+ const px = AVATAR_SIZES[size] || AVATAR_SIZES.md
67
+ const name = user?.name || user?.email || 'User'
68
+ const initials = getInitials(name)
69
+ const color = AVATAR_COLORS[nameHash(name) % AVATAR_COLORS.length]
70
+ const fontSize = Math.round(px * 0.4)
71
+ const statusDot = showStatus ? `<span class="avatar-status avatar-status-${user?.status === 'active' || user?.online ? 'online' : 'offline'}" style="width:${Math.round(px * 0.3)}px;height:${Math.round(px * 0.3)}px"></span>` : ''
72
+ return `<span class="user-avatar user-avatar-${size}" style="width:${px}px;height:${px}px;background:${color};font-size:${fontSize}px" title="${name}" aria-label="${name}" role="img"><span aria-hidden="true">${initials}</span>${statusDot}</span>`
73
+ }
74
+
75
+ export function teamAvatarGroup(users = [], maxShow = 3) {
76
+ if (!users.length) return ''
77
+ const shown = users.slice(0, maxShow)
78
+ const overflow = users.length - maxShow
79
+ const avatars = shown.map((u, i) => `<span class="avatar-group-item" style="z-index:${maxShow - i}">${userAvatar(u, 'sm')}</span>`).join('')
80
+ const badge = overflow > 0 ? `<span class="avatar-group-overflow">+${overflow}</span>` : ''
81
+ return `<span class="avatar-group">${avatars}${badge}</span>`
82
+ }
83
+
84
+ export function infoBubble(text, position = 'top') {
85
+ return `<span class="info-bubble info-bubble-${position}" data-tooltip="${text.replace(/"/g, '&quot;')}"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/><text x="8" y="12" text-anchor="middle" fill="currentColor" font-size="10" font-weight="600">i</text></svg></span>`
86
+ }
87
+
88
+ export function sortableList(items = [], containerId = 'sortable') {
89
+ const lis = items.map((item, i) =>
90
+ `<li class="sortable-item" draggable="true" data-index="${i}"><span class="sortable-handle">&#9776;</span><span>${typeof item === 'string' ? item : item.label || item.name || ''}</span></li>`
91
+ ).join('')
92
+ const script = `(function(){const c=document.getElementById('${containerId}');let dragged=null;c.addEventListener('dragstart',e=>{dragged=e.target.closest('.sortable-item');e.dataTransfer.effectAllowed='move'});c.addEventListener('dragover',e=>{e.preventDefault();const t=e.target.closest('.sortable-item');if(t&&t!==dragged)t.classList.add('drag-over')});c.addEventListener('dragleave',e=>{const t=e.target.closest('.sortable-item');if(t)t.classList.remove('drag-over')});c.addEventListener('drop',e=>{e.preventDefault();const t=e.target.closest('.sortable-item');if(t&&t!==dragged){t.classList.remove('drag-over');c.insertBefore(dragged,t.nextSibling);const order=[...c.querySelectorAll('.sortable-item')].map(el=>+el.dataset.index);c.dispatchEvent(new CustomEvent('sortable-reorder',{detail:{order}}))}});})();`
93
+ return `<ul id="${containerId}" class="sortable-list">${lis}</ul><script>${script}</script>`
94
+ }
95
+
96
+ export function responseChoiceBox(name, options = [], selected = null, type = 'radio') {
97
+ const sel = Array.isArray(selected) ? selected : (selected != null ? [selected] : [])
98
+ const items = options.map((opt) => {
99
+ const val = typeof opt === 'string' ? opt : opt.value || opt
100
+ const lbl = typeof opt === 'string' ? opt : opt.label || opt.value || opt
101
+ const checked = sel.includes(val) ? 'checked' : ''
102
+ return `<label class="choice-option"><input type="${type}" name="${name}" value="${val}" ${checked}/><span class="choice-label">${lbl}</span></label>`
103
+ }).join('')
104
+ return `<div class="choice-group">${items}</div>`
105
+ }
106
+
107
+ export function responseAttachment(file = {}) {
108
+ const name = file.name || file.filename || 'Unnamed'
109
+ const size = file.size ? (file.size < 1024 ? file.size + ' B' : file.size < 1048576 ? (file.size / 1024).toFixed(1) + ' KB' : (file.size / 1048576).toFixed(1) + ' MB') : ''
110
+ const href = file.url || file.path || '#'
111
+ const ext = name.split('.').pop().toLowerCase()
112
+ const isImg = ['png','jpg','jpeg','gif','webp','svg'].includes(ext)
113
+ const isPdf = ext === 'pdf'
114
+ const icon = isImg ? '&#128444;' : isPdf ? '&#128196;' : '&#128206;'
115
+ const preview = isImg ? `<img src="${href}" alt="${name}" class="attachment-preview"/>` : isPdf ? `<iframe src="${href}" class="attachment-preview" style="width:100%;height:200px;border:none"></iframe>` : ''
116
+ return `<div class="attachment-card"><span class="attachment-icon">${icon}</span><div class="attachment-info"><div class="attachment-name">${name}</div>${size ? `<div class="attachment-size">${size}</div>` : ''}${preview}</div><a href="${href}" download class="btn btn-ghost btn-xs">Download</a></div>`
117
+ }
118
+
119
+ export function accordion(items) {
120
+ if (!items?.length) return ''
121
+ return h('div', { className: 'accordion' },
122
+ items.map(item =>
123
+ h('details', { className: 'accordion-item' },
124
+ h('summary', { className: 'accordion-summary' }, item.title),
125
+ h('div', { className: 'accordion-content' }, item.content)
126
+ )
127
+ ).join('')
128
+ )
129
+ }
130
+
131
+ export function divider(label) {
132
+ if (!label) return '<hr class="divider"/>'
133
+ return `<div class="divider-labeled"><hr class="divider-line"/><span class="divider-text">${label}</span><hr class="divider-line"/></div>`
134
+ }
135
+
136
+ export function responsiveClass(breakpoint) {
137
+ const bp = { sm: 640, md: 768, lg: 1024, xl: 1280 }
138
+ if (!bp[breakpoint]) return ''
139
+ return `resp-${breakpoint}`
140
+ }
141
+
142
+ export function reviewCalcFields(review, highlights) {
143
+ const hl = highlights || []
144
+ const total = hl.length
145
+ const resolved = hl.filter(h => h.resolved || h.status === 'resolved').length
146
+ const unresolved = total - resolved
147
+ const resolutionPct = total > 0 ? Math.round((resolved / total) * 100) : 0
148
+ const flagged = hl.filter(h => h.flagged || h.flag).length
149
+ const byColor = {}
150
+ hl.forEach(h => { const c = h.color || 'none'; byColor[c] = (byColor[c] || 0) + 1 })
151
+ return { total, resolved, unresolved, resolutionPct, flagged, byColor }
152
+ }