@modernman00/shared-js-lib 1.2.81 → 1.2.83

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,367 @@
1
+ /**
2
+ * CardCanvasGenerator.js
3
+ * High-resolution 1200x630 / 1200x675 HTML5 Canvas Social Card Generator.
4
+ * Part of @modernman00/shared-js-lib
5
+ */
6
+
7
+ export class CardCanvasGenerator {
8
+ /**
9
+ * Default configuration options
10
+ */
11
+ static get DEFAULT_OPTIONS() {
12
+ return {
13
+ width: 1200,
14
+ height: 675,
15
+ brandName: 'Platform',
16
+ brandBadge: 'DECISION INTELLIGENCE',
17
+ title: 'Strategic Analysis',
18
+ subtitle: '',
19
+ score: 85,
20
+ maxScore: 100,
21
+ scoreLabel: 'SCORE',
22
+ verdict: 'STRONG GO',
23
+ verdictColor: '#10b981', // emerald
24
+ primaryColor: '#10b981',
25
+ secondaryColor: '#3b82f6',
26
+ accentColor: '#6366f1',
27
+ backgroundColor: '#090d16',
28
+ cardBackground: 'rgba(255, 255, 255, 0.03)',
29
+ borderColor: 'rgba(255, 255, 255, 0.1)',
30
+ textColor: '#ffffff',
31
+ mutedTextColor: '#94a3b8',
32
+ watermarkText: 'Generated with Decision Intelligence Platform',
33
+ metrics: [], // Array of { label: string, score: number, maxScore?: number }
34
+ showGauge: true,
35
+ showMetrics: true
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Renders the card onto a target HTML5 canvas element
41
+ * @param {HTMLCanvasElement} canvas
42
+ * @param {Object} options
43
+ * @returns {HTMLCanvasElement}
44
+ */
45
+ static renderToCanvas(canvas, options = {}) {
46
+ const opts = { ...this.DEFAULT_OPTIONS, ...options };
47
+ const width = opts.width;
48
+ const height = opts.height;
49
+
50
+ canvas.width = width;
51
+ canvas.height = height;
52
+ const ctx = canvas.getContext('2d');
53
+ if (!ctx) {
54
+ throw new Error('Unable to obtain 2D canvas rendering context.');
55
+ }
56
+
57
+ // 1. Draw Deep Gradient Background
58
+ const bgGrad = ctx.createLinearGradient(0, 0, width, height);
59
+ bgGrad.addColorStop(0, opts.backgroundColor || '#090d16');
60
+ bgGrad.addColorStop(0.5, '#0d1527');
61
+ bgGrad.addColorStop(1, '#050811');
62
+ ctx.fillStyle = bgGrad;
63
+ ctx.fillRect(0, 0, width, height);
64
+
65
+ // 2. Ambient Lighting / Glow Orbs
66
+ this._drawGlowOrb(ctx, 150, 120, 250, opts.primaryColor, 0.18);
67
+ this._drawGlowOrb(ctx, width - 200, height - 150, 300, opts.secondaryColor, 0.15);
68
+
69
+ // 3. Subtle Grid Pattern
70
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)';
71
+ ctx.lineWidth = 1;
72
+ for (let x = 40; x < width; x += 60) {
73
+ ctx.beginPath();
74
+ ctx.moveTo(x, 0);
75
+ ctx.lineTo(x, height);
76
+ ctx.stroke();
77
+ }
78
+ for (let y = 40; y < height; y += 60) {
79
+ ctx.beginPath();
80
+ ctx.moveTo(0, y);
81
+ ctx.lineTo(width, y);
82
+ ctx.stroke();
83
+ }
84
+
85
+ // 4. Main Glassmorphic Card Container
86
+ const margin = 40;
87
+ const cardW = width - margin * 2;
88
+ const cardH = height - margin * 2;
89
+ this._drawRoundedRect(ctx, margin, margin, cardW, cardH, 24);
90
+ ctx.fillStyle = opts.cardBackground;
91
+ ctx.fill();
92
+ ctx.strokeStyle = opts.borderColor;
93
+ ctx.lineWidth = 1.5;
94
+ ctx.stroke();
95
+
96
+ // 5. Header: Brand & Badge
97
+ ctx.font = 'bold 26px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
98
+ ctx.fillStyle = opts.textColor;
99
+ ctx.textAlign = 'left';
100
+ ctx.textBaseline = 'middle';
101
+ ctx.fillText(opts.brandName, margin + 40, margin + 50);
102
+
103
+ if (opts.brandBadge) {
104
+ const brandWidth = ctx.measureText(opts.brandName).width;
105
+ const badgeX = margin + 40 + brandWidth + 16;
106
+ const badgeY = margin + 37;
107
+ const badgeText = String(opts.brandBadge).toUpperCase();
108
+ ctx.font = 'bold 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
109
+ const badgeW = ctx.measureText(badgeText).width + 24;
110
+
111
+ this._drawRoundedRect(ctx, badgeX, badgeY, badgeW, 26, 6);
112
+ ctx.fillStyle = 'rgba(16, 185, 129, 0.15)';
113
+ ctx.fill();
114
+ ctx.strokeStyle = 'rgba(16, 185, 129, 0.4)';
115
+ ctx.lineWidth = 1;
116
+ ctx.stroke();
117
+
118
+ ctx.fillStyle = opts.primaryColor;
119
+ ctx.fillText(badgeText, badgeX + 12, badgeY + 13);
120
+ }
121
+
122
+ // Header Divider Line
123
+ ctx.beginPath();
124
+ ctx.moveTo(margin + 40, margin + 85);
125
+ ctx.lineTo(margin + cardW - 40, margin + 85);
126
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
127
+ ctx.lineWidth = 1;
128
+ ctx.stroke();
129
+
130
+ // 6. Left Column: Circular Glowing Gauge (if enabled)
131
+ const hasGauge = Boolean(opts.showGauge);
132
+ const leftColWidth = hasGauge ? 340 : 0;
133
+ const contentStartX = margin + 40 + leftColWidth;
134
+
135
+ if (hasGauge) {
136
+ const gaugeCenterX = margin + 170;
137
+ const gaugeCenterY = margin + 280;
138
+ const gaugeRadius = 110;
139
+
140
+ // Outer track
141
+ ctx.beginPath();
142
+ ctx.arc(gaugeCenterX, gaugeCenterY, gaugeRadius, 0, Math.PI * 2);
143
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
144
+ ctx.lineWidth = 16;
145
+ ctx.lineCap = 'round';
146
+ ctx.stroke();
147
+
148
+ // Active Arc
149
+ const scorePct = Math.max(0, Math.min(100, opts.score)) / (opts.maxScore || 100);
150
+ const startAngle = -Math.PI / 2;
151
+ const endAngle = startAngle + scorePct * (Math.PI * 2);
152
+
153
+ ctx.beginPath();
154
+ ctx.arc(gaugeCenterX, gaugeCenterY, gaugeRadius, startAngle, endAngle);
155
+ const gaugeGrad = ctx.createLinearGradient(
156
+ gaugeCenterX - gaugeRadius,
157
+ gaugeCenterY - gaugeRadius,
158
+ gaugeCenterX + gaugeRadius,
159
+ gaugeCenterY + gaugeRadius
160
+ );
161
+ gaugeGrad.addColorStop(0, opts.primaryColor);
162
+ gaugeGrad.addColorStop(1, opts.secondaryColor);
163
+ ctx.strokeStyle = gaugeGrad;
164
+ ctx.lineWidth = 16;
165
+ ctx.stroke();
166
+
167
+ // Score Value
168
+ ctx.textAlign = 'center';
169
+ ctx.textBaseline = 'middle';
170
+ ctx.font = 'bold 64px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
171
+ ctx.fillStyle = opts.textColor;
172
+ ctx.fillText(`${Math.round(opts.score)}%`, gaugeCenterX, gaugeCenterY - 10);
173
+
174
+ ctx.font = '600 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
175
+ ctx.fillStyle = opts.mutedTextColor;
176
+ ctx.fillText(opts.scoreLabel, gaugeCenterX, gaugeCenterY + 36);
177
+
178
+ // Verdict Pill below gauge
179
+ if (opts.verdict) {
180
+ const pillY = gaugeCenterY + gaugeRadius + 30;
181
+ ctx.font = 'bold 15px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
182
+ const pillText = String(opts.verdict).toUpperCase();
183
+ const pillW = ctx.measureText(pillText).width + 36;
184
+ const pillH = 34;
185
+
186
+ this._drawRoundedRect(ctx, gaugeCenterX - pillW / 2, pillY, pillW, pillH, 17);
187
+ ctx.fillStyle = 'rgba(16, 185, 129, 0.15)';
188
+ ctx.fill();
189
+ ctx.strokeStyle = opts.verdictColor;
190
+ ctx.lineWidth = 1.5;
191
+ ctx.stroke();
192
+
193
+ ctx.fillStyle = opts.verdictColor;
194
+ ctx.fillText(pillText, gaugeCenterX, pillY + pillH / 2);
195
+ }
196
+ }
197
+
198
+ // 7. Right Column: Headline, Subtitle & Metrics
199
+ ctx.textAlign = 'left';
200
+ ctx.textBaseline = 'top';
201
+
202
+ // Title (multi-line word wrap)
203
+ ctx.font = 'bold 38px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
204
+ ctx.fillStyle = opts.textColor;
205
+ const maxTitleW = cardW - (contentStartX - margin) - 40;
206
+ const titleLines = this._wrapText(ctx, opts.title, maxTitleW);
207
+ let curY = margin + 120;
208
+
209
+ titleLines.slice(0, 2).forEach((line) => {
210
+ ctx.fillText(line, contentStartX, curY);
211
+ curY += 48;
212
+ });
213
+
214
+ // Subtitle
215
+ if (opts.subtitle) {
216
+ curY += 6;
217
+ ctx.font = '500 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
218
+ ctx.fillStyle = opts.mutedTextColor;
219
+ const subLines = this._wrapText(ctx, opts.subtitle, maxTitleW);
220
+ subLines.slice(0, 2).forEach((line) => {
221
+ ctx.fillText(line, contentStartX, curY);
222
+ curY += 26;
223
+ });
224
+ }
225
+
226
+ // Metrics Breakdown Bars (if provided)
227
+ if (opts.showMetrics && Array.isArray(opts.metrics) && opts.metrics.length > 0) {
228
+ curY += 24;
229
+ const barTotalWidth = Math.min(maxTitleW, 640);
230
+ const metricsToShow = opts.metrics.slice(0, 4);
231
+
232
+ metricsToShow.forEach((metric) => {
233
+ const mScore = Number(metric.score ?? 0);
234
+ const mMax = Number(metric.maxScore ?? 10);
235
+ const mRatio = Math.max(0, Math.min(1, mMax > 0 ? mScore / mMax : 0));
236
+
237
+ // Metric label & score
238
+ ctx.font = '600 15px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
239
+ ctx.fillStyle = '#e2e8f0';
240
+ ctx.fillText(metric.label, contentStartX, curY);
241
+
242
+ ctx.textAlign = 'right';
243
+ ctx.fillStyle = opts.primaryColor;
244
+ ctx.fillText(`${mScore} / ${mMax}`, contentStartX + barTotalWidth, curY);
245
+ ctx.textAlign = 'left';
246
+
247
+ // Track bar
248
+ const barY = curY + 24;
249
+ const barH = 8;
250
+ this._drawRoundedRect(ctx, contentStartX, barY, barTotalWidth, barH, 4);
251
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
252
+ ctx.fill();
253
+
254
+ // Fill bar
255
+ if (mRatio > 0) {
256
+ const fillW = Math.max(8, barTotalWidth * mRatio);
257
+ this._drawRoundedRect(ctx, contentStartX, barY, fillW, barH, 4);
258
+ const barGrad = ctx.createLinearGradient(contentStartX, barY, contentStartX + fillW, barY);
259
+ barGrad.addColorStop(0, opts.primaryColor);
260
+ barGrad.addColorStop(1, opts.secondaryColor);
261
+ ctx.fillStyle = barGrad;
262
+ ctx.fill();
263
+ }
264
+
265
+ curY += 46;
266
+ });
267
+ }
268
+
269
+ // 8. Footer Watermark & Domain
270
+ const footerY = margin + cardH - 35;
271
+ ctx.font = '500 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
272
+ ctx.fillStyle = opts.mutedTextColor;
273
+ ctx.textAlign = 'left';
274
+ ctx.textBaseline = 'middle';
275
+ ctx.fillText(opts.watermarkText, margin + 40, footerY);
276
+
277
+ return canvas;
278
+ }
279
+
280
+ /**
281
+ * Generates a PNG Blob from options
282
+ * @param {Object} options
283
+ * @param {string} [type='image/png']
284
+ * @param {number} [quality=0.95]
285
+ * @returns {Promise<Blob>}
286
+ */
287
+ static generateBlob(options = {}, type = 'image/png', quality = 0.95) {
288
+ return new Promise((resolve, reject) => {
289
+ try {
290
+ const canvas = document.createElement('canvas');
291
+ this.renderToCanvas(canvas, options);
292
+ canvas.toBlob((blob) => {
293
+ if (blob) {
294
+ resolve(blob);
295
+ } else {
296
+ reject(new Error('Canvas toBlob returned null'));
297
+ }
298
+ }, type, quality);
299
+ } catch (err) {
300
+ reject(err);
301
+ }
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Generates a Data URL string from options
307
+ * @param {Object} options
308
+ * @param {string} [type='image/png']
309
+ * @returns {string}
310
+ */
311
+ static generateDataUrl(options = {}, type = 'image/png') {
312
+ const canvas = document.createElement('canvas');
313
+ this.renderToCanvas(canvas, options);
314
+ return canvas.toDataURL(type);
315
+ }
316
+
317
+ // --- Internal Drawing Helpers ---
318
+
319
+ static _drawGlowOrb(ctx, x, y, radius, color, alpha) {
320
+ ctx.save();
321
+ ctx.beginPath();
322
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
323
+ const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
324
+ grad.addColorStop(0, color);
325
+ grad.addColorStop(1, 'transparent');
326
+ ctx.fillStyle = grad;
327
+ ctx.globalAlpha = alpha;
328
+ ctx.fill();
329
+ ctx.restore();
330
+ }
331
+
332
+ static _drawRoundedRect(ctx, x, y, width, height, radius) {
333
+ ctx.beginPath();
334
+ ctx.moveTo(x + radius, y);
335
+ ctx.lineTo(x + width - radius, y);
336
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
337
+ ctx.lineTo(x + width, y + height - radius);
338
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
339
+ ctx.lineTo(x + radius, y + height);
340
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
341
+ ctx.lineTo(x, y + radius);
342
+ ctx.quadraticCurveTo(x, y, x + radius, y);
343
+ ctx.closePath();
344
+ }
345
+
346
+ static _wrapText(ctx, text, maxWidth) {
347
+ if (!text) return [];
348
+ const words = String(text).split(' ');
349
+ const lines = [];
350
+ let currentLine = words[0] || '';
351
+
352
+ for (let i = 1; i < words.length; i++) {
353
+ const word = words[i];
354
+ const width = ctx.measureText(`${currentLine} ${word}`).width;
355
+ if (width < maxWidth) {
356
+ currentLine += ` ${word}`;
357
+ } else {
358
+ lines.push(currentLine);
359
+ currentLine = word;
360
+ }
361
+ }
362
+ if (currentLine) {
363
+ lines.push(currentLine);
364
+ }
365
+ return lines;
366
+ }
367
+ }
package/Clipboard.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ import { swalToast, swalError } from './Alert.js';
3
+
4
+ /**
5
+ * Universal clipboard copy utility with automatic SweetAlert2 toast feedback.
6
+ *
7
+ * @param {string} text - Text to copy
8
+ * @param {string} [successMessage='Copied to clipboard!'] - Custom toast notification
9
+ * @param {boolean} [showToast=true] - Whether to fire Swal toast
10
+ * @returns {Promise<boolean>} True if copy was successful
11
+ */
12
+ export const copyToClipboard = async (
13
+ text,
14
+ successMessage = 'Copied to clipboard!',
15
+ showToast = true
16
+ ) => {
17
+ if (typeof window === 'undefined') return false;
18
+
19
+ try {
20
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
21
+ await navigator.clipboard.writeText(text);
22
+ } else {
23
+ // Fallback for older browsers
24
+ const textarea = document.createElement('textarea');
25
+ textarea.value = text;
26
+ textarea.style.position = 'fixed';
27
+ textarea.style.left = '-9999px';
28
+ document.body.appendChild(textarea);
29
+ textarea.select();
30
+ document.execCommand('copy');
31
+ document.body.removeChild(textarea);
32
+ }
33
+
34
+ if (showToast) {
35
+ swalToast(successMessage, 'success', 2500);
36
+ }
37
+ return true;
38
+ } catch (err) {
39
+ console.error('copyToClipboard Error:', err);
40
+ if (showToast) {
41
+ swalError('Failed to copy text to clipboard.', false);
42
+ }
43
+ return false;
44
+ }
45
+ };
package/Cookie.js CHANGED
@@ -28,25 +28,7 @@ export const getCookie = (cname) => {
28
28
 
29
29
 
30
30
 
31
- /**
32
- * Checks if a cookie with the given name exists. If not, prompts the user
33
- * for a name and sets the cookie for 365 days. If the cookie does exist,
34
- * alerts the user with a welcome message.
35
- * @param {string} name - The name of the cookie to check.
36
- */
37
- export const checkCookie = (name) => {
38
- let user = getCookie(name);
39
- if (!user) {
40
- const askName = prompt("Please enter your name:", "");
41
- if (askName) {
42
- setCookie(name, askName, 365);
43
- user = askName;
44
- }
45
- }
46
- if (user) {
47
- alert(`Welcome again ${user}`);
48
- }
49
- }
31
+
50
32
 
51
33
  // CHECK TOKEN FROM COOKIE
52
34
  export const getCookieValue = (name) => {
package/FormLock.js ADDED
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+ import { swalToast, swalWarning } from './Alert.js';
3
+
4
+ /**
5
+ * Registry of active in-flight locks keyed by form ID or URL.
6
+ */
7
+ const inFlightLocks = new Set();
8
+
9
+ /**
10
+ * Checks if an operation is currently locked.
11
+ * @param {string} key
12
+ * @returns {boolean}
13
+ */
14
+ export const isOperationLocked = (key) => inFlightLocks.has(key);
15
+
16
+ /**
17
+ * Acquires a lock for a key. Returns true if acquired, false if already in flight.
18
+ * @param {string} key
19
+ * @returns {boolean}
20
+ */
21
+ export const acquireLock = (key) => {
22
+ if (inFlightLocks.has(key)) return false;
23
+ inFlightLocks.add(key);
24
+ return true;
25
+ };
26
+
27
+ /**
28
+ * Releases a lock for a key.
29
+ * @param {string} key
30
+ */
31
+ export const releaseLock = (key) => {
32
+ inFlightLocks.delete(key);
33
+ };
34
+
35
+ /**
36
+ * Guard utility to prevent concurrent or rapid double-submissions.
37
+ *
38
+ * @param {string} lockKey - Unique identifier (e.g. formId or URL)
39
+ * @param {() => Promise<any>} asyncFn - Async action to run exclusively
40
+ * @param {string} [busyMessage='Please wait, an operation is already in progress...']
41
+ * @returns {Promise<any>}
42
+ */
43
+ export const withSubmissionLock = async (
44
+ lockKey,
45
+ asyncFn,
46
+ busyMessage = 'Please wait, an operation is already in progress...'
47
+ ) => {
48
+ if (!acquireLock(lockKey)) {
49
+ swalToast(busyMessage, 'warning', 2500);
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ return await asyncFn();
55
+ } finally {
56
+ releaseLock(lockKey);
57
+ }
58
+ };