@myxo-victor/chexjs 6.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,212 @@
1
+ /**
2
+ * ScrollEcho.js
3
+ * A high-performance, lightweight scroll-triggered text reveal library.
4
+ * Supports character-by-character and word-by-word reveal cascades.
5
+ * * Initializer Syntax:
6
+ * ScrollEcho.auto('#target', { type: 'char', delay: 20 });
7
+ */
8
+
9
+ class ScrollEcho {
10
+ constructor(selector, options = {}) {
11
+ // DOM lookup for target elements
12
+ this.elements = document.querySelectorAll(selector);
13
+
14
+ // Configuration options with fluid defaults
15
+ this.options = {
16
+ type: options.type || 'word', // Reveal type: 'word' or 'char'
17
+ delay: options.delay !== undefined ? options.delay : 40, // Stagger delay gap in milliseconds
18
+ duration: options.duration !== undefined ? options.duration : 400, // Speed of transition animation in ms
19
+ threshold: options.threshold !== undefined ? options.threshold : 0.15, // Viewport visibility scroll threshold (0.0 to 1.0)
20
+ transformY: options.transformY || '12px', // Vertical rise offset displacement
21
+ ease: options.ease || 'cubic-bezier(0.25, 1, 0.5, 1)', // Easing transition timing curve
22
+ onRevealComplete: options.onRevealComplete || null, // Optional callback executed when reveal loop completes
23
+ ...options
24
+ };
25
+
26
+ // Initialize global instance registry for manual retrigger controls
27
+ if (!window.scrollTextTriggerInstances) {
28
+ window.scrollTextTriggerInstances = {};
29
+ }
30
+
31
+ this.init();
32
+ }
33
+
34
+ /**
35
+ * Static Auto Initializer Endpoint
36
+ * @param {string} selector - Target selector string (e.g. '#target-element')
37
+ * @param {object} options - Configuration overrides
38
+ * @returns {ScrollEcho} Instance of ScrollEcho
39
+ */
40
+ static auto(selector, options = {}) {
41
+ return new ScrollEcho(selector, options);
42
+ }
43
+
44
+ /**
45
+ * Initializes elements by parsing text content and wrapping with spans
46
+ */
47
+ init() {
48
+ this.elements.forEach((element) => {
49
+ // Guard clause to prevent duplicate parsing initialization on the same node
50
+ if (element.dataset.revealerInitialized) return;
51
+
52
+ const originalText = element.textContent.trim();
53
+ element.innerHTML = ''; // Clear original text nodes safely
54
+ element.dataset.revealerInitialized = "true";
55
+
56
+ // Store original text format for re-trigger calculations
57
+ element.dataset.originalContent = originalText;
58
+
59
+ let splitArray = [];
60
+ if (this.options.type === 'char') {
61
+ splitArray = originalText.split(''); // Split to individual characters
62
+ } else {
63
+ splitArray = originalText.split(/\s+/); // Split on whitespace blocks to get words
64
+ }
65
+
66
+ const docFragment = document.createDocumentFragment();
67
+
68
+ splitArray.forEach((item, index) => {
69
+ const span = document.createElement('span');
70
+
71
+ if (this.options.type === 'char') {
72
+ span.className = 'reveal-char';
73
+ // Manage spaces elegantly using non-breaking spaces
74
+ if (item === ' ') {
75
+ span.innerHTML = ' ';
76
+ } else {
77
+ span.innerText = item;
78
+ }
79
+ } else {
80
+ span.className = 'reveal-word';
81
+ span.innerText = item;
82
+
83
+ // Inject a separation space node to avoid text compression when wrapped inside inline-block elements
84
+ if (index < splitArray.length - 1) {
85
+ const space = document.createTextNode(' ');
86
+ docFragment.appendChild(span);
87
+ docFragment.appendChild(space);
88
+ return;
89
+ }
90
+ }
91
+
92
+ // Apply initial hidden layout properties & custom configurations
93
+ span.style.display = 'inline-block';
94
+ span.style.maxWidth = '100%';
95
+ span.style.opacity = '0';
96
+ span.style.transform = `translateY(${this.options.transformY})`;
97
+ span.style.transitionProperty = 'opacity, transform';
98
+ span.style.transitionDuration = `${this.options.duration}ms`;
99
+ span.style.transitionTimingFunction = this.options.ease;
100
+
101
+ docFragment.appendChild(span);
102
+ });
103
+
104
+ element.appendChild(docFragment);
105
+
106
+ // Dynamically assign an ID if element does not have one to map callbacks
107
+ const trackerId = element.id || 'el-' + Math.random().toString(36).substr(2, 9);
108
+ if (!element.id) element.id = trackerId;
109
+
110
+ // Map reference inside global window register
111
+ window.scrollTextTriggerInstances[trackerId] = this;
112
+
113
+ // Bind element to the IntersectionObserver API
114
+ this.createObserver(element);
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Binds observation triggers using native high-performance IntersectionObserver
120
+ * @param {HTMLElement} element - Target text node element container
121
+ */
122
+ createObserver(element) {
123
+ const observerOptions = {
124
+ root: null, // Scans parent window viewport bounds
125
+ rootMargin: '0px',
126
+ threshold: this.options.threshold
127
+ };
128
+
129
+ const observer = new IntersectionObserver((entries, observer) => {
130
+ entries.forEach(entry => {
131
+ if (entry.isIntersecting) {
132
+ this.animateReveal(entry.target);
133
+ observer.unobserve(entry.target); // Kill observer to fire animation only once
134
+ }
135
+ });
136
+ }, observerOptions);
137
+
138
+ observer.observe(element);
139
+ }
140
+
141
+ /**
142
+ * Staggers animation opacity & transformation triggers for elements children spans
143
+ * @param {HTMLElement} element - Parent element containing split reveal spans
144
+ */
145
+ animateReveal(element) {
146
+ const targetClass = this.options.type === 'char' ? '.reveal-char' : '.reveal-word';
147
+ const children = element.querySelectorAll(targetClass);
148
+
149
+ children.forEach((child, index) => {
150
+ const itemDelay = index * this.options.delay;
151
+
152
+ setTimeout(() => {
153
+ child.style.opacity = '1';
154
+ child.style.transform = 'translateY(0)';
155
+ }, itemDelay);
156
+ });
157
+
158
+ // Trigger optional complete callback if defined
159
+ if (typeof this.options.onRevealComplete === 'function') {
160
+ const totalAnimateTime = (children.length * this.options.delay) + this.options.duration;
161
+ setTimeout(() => {
162
+ this.options.onRevealComplete(element);
163
+ }, totalAnimateTime);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Public method to reset animations and play the reveal sequence on command
169
+ * @param {string} elementId - ID string of target element to replay
170
+ */
171
+ retrigger(elementId) {
172
+ const element = document.getElementById(elementId);
173
+ if (!element) return;
174
+
175
+ const targetClass = this.options.type === 'char' ? '.reveal-char' : '.reveal-word';
176
+ const children = element.querySelectorAll(targetClass);
177
+
178
+ // Reset elements silently back to offset position without transitions
179
+ children.forEach(child => {
180
+ child.style.transition = 'none'; // Temporarily bypass CSS transitions
181
+ child.style.opacity = '0';
182
+ child.style.transform = `translateY(${this.options.transformY})`;
183
+
184
+ // Force browser layout reflow/repaint recalculation
185
+ child.offsetHeight;
186
+
187
+ child.style.transition = ''; // Restore style defaults for cascade
188
+ });
189
+
190
+ // Play the staggered cascade reveal loop
191
+ this.animateReveal(element);
192
+ }
193
+ }
194
+
195
+
196
+
197
+ /*
198
+ **************************
199
+ How to use this library
200
+ **************************
201
+ */
202
+
203
+
204
+ /*
205
+ ScrollEcho.auto('#target', {
206
+ type: 'char', // 'char' (characters) or 'word' (words)
207
+ delay: 30, // stagger delay between each piece (ms)
208
+ duration: 500, // speed of the reveal animation (ms)
209
+ threshold: 0.15, // viewport enter intersection threshold
210
+ transformY: '12px' // customized rise offset
211
+ });
212
+ */
@@ -0,0 +1,341 @@
1
+ /**
2
+ * skeleton.js (v1.0.0)
3
+ * Zero-Config DOM-to-Skeleton Compiler
4
+ *
5
+ * Auto-creates highly accurate shimmer layouts directly from your rendered DOM markup
6
+ * to guarantee zero layout drift (CLS) without writing manual skeleton designs.
7
+ */
8
+
9
+ class SkeletonLoader {
10
+ constructor(target, options = {}) {
11
+ // Resolve target selector or HTMLElement directly
12
+ this.target = typeof target === 'string' ? document.querySelector(target) : target;
13
+
14
+ // Default config values
15
+ this.options = {
16
+ speed: options.speed || 1600, // Shimmer speed in milliseconds
17
+ baseColor: options.baseColor || '#1e293b', // Base placeholder background color (dark slate default)
18
+ varyTextLines: options.varyTextLines !== undefined ? options.varyTextLines : true, // Vary sentence endings
19
+ exclude: options.exclude || [], // Selectors to skip morphing
20
+ classToKeep: options.classToKeep || [], // Classes to preserve during layout transfer
21
+ ...options
22
+ };
23
+
24
+ this.skeletonClone = null;
25
+ this.isShowing = false;
26
+ this.originalStyles = {
27
+ display: '',
28
+ visibility: ''
29
+ };
30
+
31
+ // Calculate light linear gradient contrast shine based on base color
32
+ this.highlightColor = this._computeHighlight(this.options.baseColor);
33
+
34
+ // Inject styles dynamically into document head
35
+ this._injectStyles();
36
+ }
37
+
38
+ /**
39
+ * Automatically handles injecting visual shimmer keyframes into head to make library Zero-Config
40
+ */
41
+ _injectStyles() {
42
+ const styleId = 'skeleton-loader-shimmer-styles';
43
+ let styleTag = document.getElementById(styleId);
44
+
45
+ if (!styleTag) {
46
+ styleTag = document.createElement('style');
47
+ styleTag.id = styleId;
48
+ document.head.appendChild(styleTag);
49
+ }
50
+
51
+ styleTag.textContent = `
52
+ @keyframes skeleton-shimmer-sweep {
53
+ 0% { background-position: -200% 0; }
54
+ 100% { background-position: 200% 0; }
55
+ }
56
+ .sk-shimmer-active {
57
+ background-size: 200% 100% !important;
58
+ animation: skeleton-shimmer-sweep ${this.options.speed}ms infinite linear !important;
59
+ }
60
+ .sk-hidden-original {
61
+ display: none !important;
62
+ }
63
+ .sk-faded-text {
64
+ color: transparent !important;
65
+ user-select: none !important;
66
+ pointer-events: none !important;
67
+ }
68
+ `;
69
+ }
70
+
71
+ /**
72
+ * Helper algorithm to calculate slightly brighter color tone for gradients
73
+ */
74
+ _computeHighlight(hex) {
75
+ try {
76
+ let cleanHex = hex.replace('#', '');
77
+ if (cleanHex.length === 3) {
78
+ cleanHex = cleanHex.split('').map(char => char + char).join('');
79
+ }
80
+ let r = parseInt(cleanHex.substring(0, 2), 16);
81
+ let g = parseInt(cleanHex.substring(2, 4), 16);
82
+ let b = parseInt(cleanHex.substring(4, 6), 16);
83
+
84
+ // Brighten color channel weights
85
+ r = Math.min(255, Math.round(r + 20));
86
+ g = Math.min(255, Math.round(g + 20));
87
+ b = Math.min(255, Math.round(b + 20));
88
+
89
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
90
+ } catch (e) {
91
+ return '#334155'; // Fallback highlight
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Core Traversal Engine: Mutates cloned elements into static blocks while maintaining coordinates
97
+ */
98
+ _compileSkeleton(node) {
99
+ if (node.nodeType === Node.ELEMENT_NODE) {
100
+
101
+ // Handle user custom skips or ignored blocks
102
+ if (node.hasAttribute('data-skeleton-ignore') || this._shouldExclude(node)) {
103
+ node.style.visibility = 'hidden';
104
+ return;
105
+ }
106
+
107
+ // 1. Convert Images and heavy vectors to Solid blocks
108
+ if (node.tagName === 'IMG' || node.tagName === 'svg' || node.classList.contains('avatar') || node.hasAttribute('data-skeleton-img')) {
109
+ this._morphToPlaceholderBlock(node, 'image');
110
+ return;
111
+ }
112
+
113
+ // 2. Convert Buttons, Pills & small Badges
114
+ if (node.tagName === 'BUTTON' || node.classList.contains('badge') || node.hasAttribute('data-skeleton-badge')) {
115
+ this._morphToPlaceholderBlock(node, 'badge');
116
+ return;
117
+ }
118
+
119
+ // 3. Process direct text lines inside child elements
120
+ const children = Array.from(node.childNodes);
121
+ let containsDirectText = false;
122
+
123
+ children.forEach(child => {
124
+ if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim().length > 0) {
125
+ containsDirectText = true;
126
+ } else {
127
+ this._compileSkeleton(child);
128
+ }
129
+ });
130
+
131
+ // 4. Render block text wireframe bands
132
+ if (containsDirectText && node.children.length === 0) {
133
+ this._morphTextPlaceholder(node);
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Converts plain text characters into modern, line-scaled shimmer bars
140
+ */
141
+ _morphTextPlaceholder(element) {
142
+ const originalText = element.textContent.trim();
143
+ element.innerHTML = '';
144
+ element.classList.add('sk-faded-text');
145
+
146
+ // Estimate line generation bounds based on string size
147
+ const charactersCount = originalText.length;
148
+ const linesToGenerate = Math.max(1, Math.min(6, Math.round(charactersCount / 65)));
149
+
150
+ for (let i = 0; i < linesToGenerate; i++) {
151
+ const textBar = document.createElement('span');
152
+ textBar.style.display = 'block';
153
+ textBar.style.height = '0.7rem';
154
+ textBar.style.marginTop = i === 0 ? '0px' : '6px';
155
+ textBar.style.borderRadius = '0.25rem';
156
+ textBar.style.backgroundColor = this.options.baseColor;
157
+
158
+ // Calculate layout variance percentages for paragraph look
159
+ if (linesToGenerate > 1 && i === linesToGenerate - 1 && this.options.varyTextLines) {
160
+ textBar.style.width = `${50 + Math.floor(Math.random() * 25)}%`;
161
+ } else if (this.options.varyTextLines && linesToGenerate > 1) {
162
+ textBar.style.width = `${85 + Math.floor(Math.random() * 15)}%`;
163
+ } else {
164
+ textBar.style.width = '100%';
165
+ }
166
+
167
+ // Add shimmer transitions
168
+ textBar.classList.add('sk-shimmer-active');
169
+ textBar.style.backgroundImage = `linear-gradient(90deg, ${this.options.baseColor} 25%, ${this.highlightColor} 50%, ${this.options.baseColor} 75%)`;
170
+
171
+ element.appendChild(textBar);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Helper to strip sources, colors, and border classes to enforce neutral shapes
177
+ */
178
+ _morphToPlaceholderBlock(element, type) {
179
+ element.innerHTML = '';
180
+ element.style.backgroundImage = 'none';
181
+ element.style.borderColor = 'transparent';
182
+ element.style.backgroundColor = this.options.baseColor;
183
+ element.removeAttribute('src');
184
+
185
+ element.classList.add('sk-shimmer-active');
186
+ element.style.backgroundImage = `linear-gradient(90deg, ${this.options.baseColor} 25%, ${this.highlightColor} 50%, ${this.options.baseColor} 75%)`;
187
+
188
+ if (type === 'image') {
189
+ element.style.borderRadius = '0.75rem';
190
+ if (!element.style.height && element.clientHeight === 0) {
191
+ element.style.minHeight = '150px'; // Safe aspect boundary
192
+ }
193
+ } else if (type === 'badge') {
194
+ element.style.borderRadius = '9999px';
195
+ element.style.height = '22px';
196
+ element.style.minWidth = '60px';
197
+ }
198
+ }
199
+
200
+ _shouldExclude(element) {
201
+ return this.options.exclude.some(selector => element.matches(selector));
202
+ }
203
+
204
+ /**
205
+ * Creates the visual skeleton clone and hides original DOM layouts
206
+ */
207
+ start() {
208
+ if (!this.target) {
209
+ console.warn('SkeletonLoader: Target element not found inside current DOM scope.');
210
+ return;
211
+ }
212
+ if (this.isShowing) return;
213
+
214
+ // 1. Deep clone target markup layout
215
+ this.skeletonClone = this.target.cloneNode(true);
216
+ this.skeletonClone.classList.add('skeleton-loader-clone');
217
+ this.skeletonClone.setAttribute('aria-busy', 'true');
218
+ this.skeletonClone.setAttribute('role', 'alert');
219
+
220
+ // Remove ID tags to prevent duplicate selectors breaking DOM scripts
221
+ this.skeletonClone.removeAttribute('id');
222
+ const childsWithId = this.skeletonClone.querySelectorAll('[id]');
223
+ childsWithId.forEach(el => el.removeAttribute('id'));
224
+
225
+ // 2. Transpile the clone nodes to shimmering frames
226
+ this._compileSkeleton(this.skeletonClone);
227
+
228
+ // 3. Hide original layouts and inject the skeleton directly adjacent
229
+ this.target.classList.add('sk-hidden-original');
230
+ this.target.parentNode.insertBefore(this.skeletonClone, this.target.nextSibling);
231
+
232
+ this.isShowing = true;
233
+ }
234
+
235
+ /**
236
+ * Restores original components cleanly and drops simulated skeleton elements
237
+ */
238
+ dismiss() {
239
+ if (!this.isShowing) return;
240
+
241
+ if (this.skeletonClone && this.skeletonClone.parentNode) {
242
+ this.skeletonClone.parentNode.removeChild(this.skeletonClone);
243
+ }
244
+
245
+ this.target.classList.remove('sk-hidden-original');
246
+ this.isShowing = false;
247
+ this.skeletonClone = null;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Global factory interface to fulfill simple API requirement:
253
+ * const sk = skeleton.load('#element', { speed: 1600 });
254
+ * sk.start();
255
+ */
256
+ const skeleton = {
257
+ load(targetSelector, options = {}) {
258
+ return new SkeletonLoader(targetSelector, options);
259
+ }
260
+ };
261
+
262
+ // Make library accessible to both browser globals & modern module architectures
263
+ if (typeof window !== 'undefined') {
264
+ window.skeleton = skeleton;
265
+ }
266
+ if (typeof module !== 'undefined' && module.exports) {
267
+ module.exports = skeleton;
268
+ }
269
+
270
+ /**
271
+ * ==============================================================================
272
+ * SKELETON.JS DEVELOPER MANUAL
273
+ * ==============================================================================
274
+ *
275
+ * skeleton.js is a zero-dependency, automated skeleton placeholder generator.
276
+ * It inspects your actual rendered markup and transforms a clone of it into a
277
+ * proportion-perfect, shimmering skeletal container. This prevents Cumulative
278
+ * Layout Shift (CLS) on dynamic websites.
279
+ *
280
+ * ------------------------------------------------------------------------------
281
+ * 1. QUICKEST START (DEFAULT)
282
+ * ------------------------------------------------------------------------------
283
+ * Include this script file in your project, and execute:
284
+ *
285
+ * const myLoader = skeleton.load('#my-product-card');
286
+ * myLoader.start();
287
+ *
288
+ * ------------------------------------------------------------------------------
289
+ * 2. FULL CONFIGURATION PARAMETERS
290
+ * ------------------------------------------------------------------------------
291
+ * Pass configuration arguments into the second initialization parameter:
292
+ *
293
+ * const myLoader = skeleton.load('#my-product-card', {
294
+ * speed: 1600, // Shimmer sweep animation loop speed (milliseconds)
295
+ * baseColor: '#1e293b', // Background plate color of placeholders (HEX)
296
+ * varyTextLines: true, // Give multi-line text paragraphs varied endings
297
+ * exclude: ['.no-shimmer', 'footer'] // Array of CSS classes/tags to ignore
298
+ * });
299
+ *
300
+ * ------------------------------------------------------------------------------
301
+ * 3. COMPLETE REAL-WORLD WORKFLOW EXAMPLE
302
+ * ------------------------------------------------------------------------------
303
+ * Here is how you run skeleton.js during standard API data fetch calls:
304
+ *
305
+ * // A. Instantiate and show skeleton over container during network wait
306
+ * const profileLoader = skeleton.load('#profile-section', {
307
+ * speed: 1200,
308
+ * baseColor: '#0f172a'
309
+ * });
310
+ *
311
+ * profileLoader.start(); // This clones your component, morphs it, & hides original
312
+ *
313
+ * // B. Call backend resource
314
+ * fetch('/api/user/profile')
315
+ * .then(response => response.json())
316
+ * .then(data => {
317
+ * // C. Populate layout fields with actual incoming records
318
+ * document.querySelector('#user-name').textContent = data.name;
319
+ * document.querySelector('#user-avatar').src = data.avatarUrl;
320
+ * document.querySelector('#user-bio').textContent = data.bio;
321
+ *
322
+ * // D. Simply dismiss the skeleton. The real populated components instantly reappear!
323
+ * profileLoader.dismiss();
324
+ * });
325
+ *
326
+ * ------------------------------------------------------------------------------
327
+ * 4. SPECIFIC SECTOR ATTR OVERRIDES
328
+ * ------------------------------------------------------------------------------
329
+ * If there are elements in your DOM that you want to exclude or control manually,
330
+ * use these simple HTML5 markers inside your template tags:
331
+ *
332
+ * <div data-skeleton-ignore="true">
333
+ * This whole section and its children will become hidden during generation.
334
+ * </div>
335
+ *
336
+ * <div class="custom-card-graphic" data-skeleton-img="true">
337
+ * This node will automatically represent an image placeholder block.
338
+ * </div>
339
+ *
340
+ * ==============================================================================
341
+ */