@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,647 @@
1
+ /**
2
+ * Smooth.js - Seamless, Fluid Continuous Infinite Ticker Slider Engine
3
+ * * Unlike traditional carousels that transition and pause, Smooth.js uses
4
+ * high-performance requestAnimationFrame delta-time rendering to run a
5
+ * completely seamless, non-stutter, continuous carousel loop.
6
+ * * Supports cloning rich DOM nodes, fallback image URLs, responsive column count,
7
+ * customizable speeds, and on-the-fly direction switching.
8
+ * * Responsive Breakpoints:
9
+ * - Desktop (>= 768px): Displays 3 slides at once
10
+ * - Mobile (< 768px): Displays 2 slides at once
11
+ */
12
+
13
+ class Smooth {
14
+ static defaultStylesInjected = false;
15
+ static activeAutoCarousels = new Map();
16
+ static autoInitTimer = null;
17
+ static autoLifecycleBound = false;
18
+
19
+ constructor() {
20
+ this.rawInputList = []; // Holds element selectors/IDs or image URLs
21
+ this.slides = []; // Resolved slide nodes and content elements
22
+ this.pixelsPerSecond = 60; // Standard ticker speed (px/sec)
23
+ this.directionVector = 'left'; // Horizontal scroll direction ('left' | 'right')
24
+ this.container = null; // Active mount viewport element
25
+ this.viewport = null; // Internal masked viewport element
26
+ this.track = null; // Ticker movement rail element
27
+
28
+ this.isPlaying = false; // Playback running state
29
+ this.isHovered = false; // Kept for backwards-compatible telemetry
30
+ this.pauseOnHover = false; // Hover suspension is disabled
31
+
32
+ this.scrollOffset = 0; // Raw scroll distance accumulator (pixels)
33
+ this.lastTimestamp = null; // Frame delta tracker
34
+ this.animationFrameId = null; // Animation loop reference handle
35
+ this.changeCallback = null; // Active telemetry update listeners
36
+
37
+ this.lastItemsPerPage = this.getItemsPerPage(); // Cache width state
38
+ this.resizeBound = false; // Guards against duplicate window listeners
39
+ this.itemsWidthSum = 0; // Total physical width of unique slide pack (one full loop)
40
+ }
41
+
42
+ /**
43
+ * Adds baseline carousel styling once per page so images look polished even
44
+ * when the user does not provide custom CSS.
45
+ */
46
+ injectDefaultStyles() {
47
+ if (Smooth.defaultStylesInjected || typeof document === 'undefined') return;
48
+
49
+ const style = document.createElement('style');
50
+ style.id = 'smooth-default-styles';
51
+ style.textContent = `
52
+ .smooth-viewport {
53
+ overflow: hidden;
54
+ width: 100%;
55
+ }
56
+
57
+ .smooth-track {
58
+ display: flex;
59
+ align-items: center;
60
+ width: max-content;
61
+ will-change: transform;
62
+ }
63
+
64
+ .smooth-slide-item {
65
+ box-sizing: border-box;
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ flex-shrink: 0;
70
+ padding: 8px;
71
+ }
72
+
73
+ .smooth-media-frame {
74
+ width: 100%;
75
+ aspect-ratio: var(--smooth-image-ratio, 4 / 3);
76
+ overflow: hidden;
77
+ border-radius: var(--smooth-image-radius, 12px);
78
+ background: var(--smooth-image-background, #f1f5f9);
79
+ }
80
+
81
+ .smooth-media-image {
82
+ display: block;
83
+ width: 100%;
84
+ height: 100%;
85
+ object-fit: var(--smooth-image-fit, cover);
86
+ }
87
+ `;
88
+
89
+ document.head.appendChild(style);
90
+ Smooth.defaultStylesInjected = true;
91
+ }
92
+
93
+ /**
94
+ * Gives cloned image nodes the same default frame used by URL images.
95
+ * @param {HTMLImageElement} image
96
+ * @returns {HTMLDivElement}
97
+ */
98
+ createImageFrame(image) {
99
+ const imgWrapper = document.createElement('div');
100
+ imgWrapper.className = 'smooth-media-frame';
101
+
102
+ image.classList.add('smooth-media-image');
103
+ image.style.display = '';
104
+ image.removeAttribute('id'); // Avoid ID duplication conflicts
105
+
106
+ imgWrapper.appendChild(image);
107
+ return imgWrapper;
108
+ }
109
+
110
+ /**
111
+ * Starts one or many carousels from simple config objects.
112
+ * @param {Object|Object[]} configs
113
+ * @returns {boolean}
114
+ */
115
+ static start(configs) {
116
+ const list = Array.isArray(configs) ? configs : [configs];
117
+ Smooth.autoConfigs = list.filter(Boolean);
118
+ Smooth.setupAutoLifecycle();
119
+ Smooth.scheduleAutoInit();
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Instance-friendly alias for Smooth.start().
125
+ * @param {Object|Object[]} configs
126
+ * @returns {Smooth}
127
+ */
128
+ start(configs) {
129
+ Smooth.start(configs);
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Binds page lifecycle events once so declarative configs survive delayed
135
+ * rendering and route changes.
136
+ */
137
+ static setupAutoLifecycle() {
138
+ if (Smooth.autoLifecycleBound || typeof window === 'undefined') return;
139
+ Smooth.autoLifecycleBound = true;
140
+
141
+ if (document.readyState === 'loading') {
142
+ document.addEventListener('DOMContentLoaded', Smooth.scheduleAutoInit);
143
+ } else {
144
+ Smooth.scheduleAutoInit();
145
+ }
146
+
147
+ window.addEventListener('load', Smooth.scheduleAutoInit);
148
+ window.addEventListener('hashchange', Smooth.scheduleAutoInit);
149
+ window.addEventListener('popstate', Smooth.scheduleAutoInit);
150
+
151
+ const observer = new MutationObserver(Smooth.scheduleAutoInit);
152
+ observer.observe(document.documentElement, {
153
+ childList: true,
154
+ subtree: true
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Debounces automatic setup while frameworks are rendering.
160
+ */
161
+ static scheduleAutoInit() {
162
+ clearTimeout(Smooth.autoInitTimer);
163
+ Smooth.autoInitTimer = setTimeout(Smooth.initConfiguredCarousels, 50);
164
+ }
165
+
166
+ /**
167
+ * Initializes every carousel declared through Smooth.start().
168
+ * @returns {boolean}
169
+ */
170
+ static initConfiguredCarousels() {
171
+ const configs = Smooth.autoConfigs || [];
172
+ return configs.map(Smooth.initConfiguredCarousel).every(Boolean);
173
+ }
174
+
175
+ /**
176
+ * Stops a managed carousel when its container leaves the DOM.
177
+ * @param {string} selector
178
+ */
179
+ static stopConfiguredCarousel(selector) {
180
+ const active = Smooth.activeAutoCarousels.get(selector);
181
+ if (!active) return;
182
+
183
+ active.carousel.stop();
184
+ Smooth.activeAutoCarousels.delete(selector);
185
+ }
186
+
187
+ /**
188
+ * Resolves item IDs/selectors from a declarative carousel config.
189
+ * @param {HTMLElement} container
190
+ * @param {Object} config
191
+ * @returns {string[]}
192
+ */
193
+ static getConfiguredItems(container, config) {
194
+ if (Array.isArray(config.items)) return config.items;
195
+ if (Array.isArray(config.images)) return config.images;
196
+
197
+ const itemSelector = config.itemSelector || 'img';
198
+ const idPrefix = config.idPrefix || `smooth-item-${Math.random().toString(36).slice(2)}`;
199
+ const items = Array.from(container.querySelectorAll(itemSelector));
200
+
201
+ return items.map((item, index) => {
202
+ if (config.idPrefix || !item.id) item.id = `${idPrefix}-${index + 1}`;
203
+ item.style.display = 'none';
204
+ return item.id;
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Initializes a single declarative carousel config.
210
+ * @param {Object} config
211
+ * @returns {boolean}
212
+ */
213
+ static initConfiguredCarousel(config) {
214
+ if (!config || !config.selector) return false;
215
+
216
+ const container = document.querySelector(config.selector);
217
+ const active = Smooth.activeAutoCarousels.get(config.selector);
218
+
219
+ if (!container) {
220
+ Smooth.stopConfiguredCarousel(config.selector);
221
+ return false;
222
+ }
223
+
224
+ if (active?.container === container) return true;
225
+
226
+ Smooth.stopConfiguredCarousel(config.selector);
227
+
228
+ const items = Smooth.getConfiguredItems(container, config);
229
+ if (items.length === 0) return false;
230
+
231
+ const carousel = new Smooth();
232
+ carousel
233
+ .mount(container)
234
+ .items(items)
235
+ .speed(config.speed || 60)
236
+ .direction(config.direction || 'left');
237
+
238
+ if (config.autoPlay !== false) carousel.play();
239
+
240
+ Smooth.activeAutoCarousels.set(config.selector, { carousel, container });
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Resolves IDs and CSS selectors without throwing on plain image URLs.
246
+ * @param {string} value
247
+ * @returns {HTMLElement|null}
248
+ */
249
+ resolveInputElement(value) {
250
+ const trimmed = value.trim();
251
+ const idMatch = document.getElementById(trimmed);
252
+ if (idMatch) return idMatch;
253
+
254
+ try {
255
+ return document.querySelector(trimmed);
256
+ } catch (error) {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Detects the optimal layout mode based on viewport width.
263
+ * Responsive: Desktop (>= 768px) shows 3, Mobile (< 768px) shows 2.
264
+ * @returns {number}
265
+ */
266
+ getItemsPerPage() {
267
+ if (typeof window !== 'undefined' && window.innerWidth < 768) {
268
+ return 2;
269
+ }
270
+ return 3;
271
+ }
272
+
273
+ /**
274
+ * Anchors the carousel wrapper to the targeted element.
275
+ * @param {string|HTMLElement} selector
276
+ * @returns {Smooth}
277
+ */
278
+ mount(selector) {
279
+ if (Array.isArray(selector) || (selector && typeof selector === 'object' && selector.selector)) {
280
+ return this.start(selector);
281
+ }
282
+
283
+ this.container = typeof selector === 'string'
284
+ ? document.querySelector(selector)
285
+ : selector;
286
+
287
+ if (!this.container) {
288
+ console.error(`Smooth.js: Mounting container "${selector}" was not resolved.`);
289
+ } else {
290
+ this.setupResizeEvent();
291
+ }
292
+ return this;
293
+ }
294
+
295
+ /**
296
+ * Hover pausing is disabled; kept as a no-op for backwards compatibility.
297
+ */
298
+ setupHoverEvents() {
299
+ return this;
300
+ }
301
+
302
+ /**
303
+ * Sets up highly responsive window resize listeners to reconstruct
304
+ * track sizes and boundaries.
305
+ */
306
+ setupResizeEvent() {
307
+ if (this.resizeBound || typeof window === 'undefined') return;
308
+ this.resizeBound = true;
309
+
310
+ let resizeTimeout;
311
+ window.addEventListener('resize', () => {
312
+ clearTimeout(resizeTimeout);
313
+ resizeTimeout = setTimeout(() => {
314
+ const currentBreakpointVal = this.getItemsPerPage();
315
+ if (currentBreakpointVal !== this.lastItemsPerPage) {
316
+ this.lastItemsPerPage = currentBreakpointVal;
317
+ this.render(); // Breakpoint changed: requires full reconstruction
318
+ } else {
319
+ this.recalculateDimensions(); // Just adapt sizing on same layout mode
320
+ }
321
+ }, 150);
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Accepts array list representing raw image IDs, selectors, or direct URLs.
327
+ * @param {Array} list
328
+ * @returns {Smooth}
329
+ */
330
+ items(list) {
331
+ this.rawInputList = Array.isArray(list) ? list : [list];
332
+ this.render();
333
+ return this;
334
+ }
335
+
336
+ /**
337
+ * Alias interface for items() to ensure backwards compatibility.
338
+ * @param {Array} list
339
+ * @returns {Smooth}
340
+ */
341
+ images(list) {
342
+ return this.items(list);
343
+ }
344
+
345
+ /**
346
+ * Sets continuous motion speed (pixels scrolled per second).
347
+ * @param {number} rate
348
+ * @returns {Smooth}
349
+ */
350
+ speed(rate) {
351
+ this.pixelsPerSecond = Math.max(1, Number(rate) || 60);
352
+ this.updateCallback();
353
+ return this;
354
+ }
355
+
356
+ /**
357
+ * Adjusts the moving direction of the carousel loop.
358
+ * @param {string} dir ('left' | 'right')
359
+ * @returns {Smooth}
360
+ */
361
+ direction(dir) {
362
+ if (dir === 'left' || dir === 'right') {
363
+ this.directionVector = dir;
364
+ this.updateCallback();
365
+ }
366
+ return this;
367
+ }
368
+
369
+ /**
370
+ * Hover suspension is disabled; kept chainable for existing calls.
371
+ * @param {boolean} value
372
+ * @returns {Smooth}
373
+ */
374
+ setPauseOnHover(value) {
375
+ this.pauseOnHover = false;
376
+ return this;
377
+ }
378
+
379
+ /**
380
+ * Resolves selector nodes, clones DOM elements, constructs circular buffer blocks,
381
+ * and mounts the elements into the track.
382
+ */
383
+ render() {
384
+ this.slides = [];
385
+ this.injectDefaultStyles();
386
+
387
+ this.rawInputList.forEach(item => {
388
+ if (typeof item === 'string') {
389
+ const trimmed = item.trim();
390
+ const element = this.resolveInputElement(trimmed);
391
+ if (element) {
392
+ element.style.display = 'none'; // Hide static layout source elements
393
+ this.slides.push({ type: 'dom', node: element });
394
+ } else {
395
+ this.slides.push({ type: 'url', url: item });
396
+ }
397
+ } else {
398
+ this.slides.push({ type: 'url', url: String(item) });
399
+ }
400
+ });
401
+
402
+ if (this.slides.length === 0) return;
403
+
404
+ // Automatic container parent matching fallback
405
+ if (!this.container) {
406
+ const firstDom = this.slides.find(s => s.type === 'dom');
407
+ if (firstDom && firstDom.node.parentElement) {
408
+ this.container = firstDom.node.parentElement;
409
+ this.setupResizeEvent();
410
+ }
411
+ }
412
+
413
+ if (!this.container) return;
414
+
415
+ // Set styling baseline to mask content
416
+ this.container.style.position = 'relative';
417
+ this.container.style.overflow = 'hidden';
418
+
419
+ // Establish viewport layer
420
+ this.viewport = this.container.querySelector('.smooth-viewport');
421
+ if (!this.viewport) {
422
+ this.viewport = document.createElement('div');
423
+ this.viewport.className = 'smooth-viewport';
424
+ this.viewport.style.overflow = 'hidden';
425
+ this.viewport.style.width = '100%';
426
+ this.container.appendChild(this.viewport);
427
+ } else {
428
+ this.viewport.innerHTML = ''; // Fresh DOM compilation
429
+ }
430
+
431
+ // Horizontal sliding track rail
432
+ this.track = document.createElement('div');
433
+ this.track.className = 'smooth-track';
434
+ this.track.style.display = 'flex';
435
+ this.track.style.width = 'max-content';
436
+ this.track.style.willChange = 'transform';
437
+ this.viewport.appendChild(this.track);
438
+
439
+ const itemsPerPage = this.getItemsPerPage();
440
+
441
+ // Triple-duplication technique:
442
+ // We render: [Buffer Left Block] [Main Content Block] [Buffer Right Block]
443
+ // Allowing the window to continuously warp offsets seamlessly without blank gaps.
444
+ const originalSet = [...this.slides];
445
+ const tripleSets = [...originalSet, ...originalSet, ...originalSet];
446
+
447
+ tripleSets.forEach((slide) => {
448
+ const itemWrapper = document.createElement('div');
449
+ itemWrapper.className = 'smooth-slide-item';
450
+ itemWrapper.style.width = `${this.viewport.clientWidth / itemsPerPage}px`;
451
+ itemWrapper.style.flexShrink = '0';
452
+ itemWrapper.style.boxSizing = 'border-box';
453
+ itemWrapper.style.padding = '8px'; // Fluid spacing gutters
454
+
455
+ if (slide.type === 'dom') {
456
+ const clone = slide.node.cloneNode(true);
457
+
458
+ if (clone.tagName === 'IMG') {
459
+ itemWrapper.appendChild(this.createImageFrame(clone));
460
+ } else {
461
+ clone.style.display = ''; // Clear display hidden from source
462
+ clone.style.width = '100%';
463
+ clone.removeAttribute('id'); // Avoid ID duplication conflicts
464
+ itemWrapper.appendChild(clone);
465
+ }
466
+ } else {
467
+ // Fallback placeholder image element
468
+ const imgWrapper = document.createElement('div');
469
+ imgWrapper.className = 'smooth-media-frame';
470
+
471
+ const img = document.createElement('img');
472
+ img.src = slide.url;
473
+ img.className = 'smooth-media-image';
474
+ imgWrapper.appendChild(img);
475
+ itemWrapper.appendChild(imgWrapper);
476
+ }
477
+
478
+ this.track.appendChild(itemWrapper);
479
+ });
480
+
481
+ this.recalculateDimensions();
482
+
483
+ // Align starting scroll position cleanly onto the middle (main) copy
484
+ this.scrollOffset = this.itemsWidthSum;
485
+ this.updatePositionDOM();
486
+ }
487
+
488
+ /**
489
+ * Recalculates individual item widths and updates the boundary parameters.
490
+ */
491
+ recalculateDimensions() {
492
+ if (!this.viewport || !this.track) return;
493
+ const itemsPerPage = this.getItemsPerPage();
494
+ const slideWidth = this.viewport.clientWidth / itemsPerPage;
495
+
496
+ const slideNodes = this.track.querySelectorAll('.smooth-slide-item');
497
+ slideNodes.forEach(node => {
498
+ node.style.width = `${slideWidth}px`;
499
+ });
500
+
501
+ // Compute absolute scroll length of 1 unique set iteration
502
+ this.itemsWidthSum = slideWidth * this.slides.length;
503
+ }
504
+
505
+ /**
506
+ * Triggers hardware-accelerated transform manipulations directly on the track.
507
+ */
508
+ updatePositionDOM() {
509
+ if (!this.track) return;
510
+ this.track.style.transform = `translate3d(-${this.scrollOffset}px, 0, 0)`;
511
+ }
512
+
513
+ /**
514
+ * Animation frame loop calculation using delta time variables to maintain
515
+ * uniform rate values on variable device configurations (60Hz, 120Hz, 144Hz monitors).
516
+ * @param {DOMHighResTimeStamp} timestamp
517
+ */
518
+ tick(timestamp) {
519
+ if (!this.isPlaying) return;
520
+
521
+ if (!this.lastTimestamp) {
522
+ this.lastTimestamp = timestamp;
523
+ this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
524
+ return;
525
+ }
526
+
527
+ const deltaTime = (timestamp - this.lastTimestamp) / 1000;
528
+ this.lastTimestamp = timestamp;
529
+
530
+ // Safeguard frame skip jumps if user changes background tabs
531
+ if (deltaTime < 0.1) {
532
+ const deltaScroll = this.pixelsPerSecond * deltaTime;
533
+
534
+ if (this.directionVector === 'left') {
535
+ this.scrollOffset += deltaScroll;
536
+ } else {
537
+ this.scrollOffset -= deltaScroll;
538
+ }
539
+
540
+ // Bound checking wrapping resets
541
+ if (this.scrollOffset >= this.itemsWidthSum * 2) {
542
+ this.scrollOffset -= this.itemsWidthSum; // Wrap backwards
543
+ } else if (this.scrollOffset <= this.itemsWidthSum) {
544
+ this.scrollOffset += this.itemsWidthSum; // Wrap forwards
545
+ }
546
+
547
+ this.updatePositionDOM();
548
+ }
549
+
550
+ this.updateCallback();
551
+ this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
552
+ }
553
+
554
+ /**
555
+ * Starts the continuous loop motion.
556
+ * @returns {Smooth}
557
+ */
558
+ play() {
559
+ if (this.isPlaying) return this;
560
+ this.isPlaying = true;
561
+ this.lastTimestamp = null;
562
+ this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
563
+ this.updateCallback();
564
+ return this;
565
+ }
566
+
567
+ /**
568
+ * Halts the continuous loop motion immediately.
569
+ * @returns {Smooth}
570
+ */
571
+ stop() {
572
+ this.isPlaying = false;
573
+ if (this.animationFrameId) {
574
+ cancelAnimationFrame(this.animationFrameId);
575
+ this.animationFrameId = null;
576
+ }
577
+ this.updateCallback();
578
+ return this;
579
+ }
580
+
581
+ /**
582
+ * Registers a callback hook to track carousel position and telemetry updates in real-time.
583
+ * @param {function} fn
584
+ * @returns {Smooth}
585
+ */
586
+ onUpdate(fn) {
587
+ this.changeCallback = fn;
588
+ return this;
589
+ }
590
+
591
+ /**
592
+ * Broadcasts engine states downstream.
593
+ */
594
+ updateCallback() {
595
+ if (this.changeCallback) {
596
+ this.changeCallback({
597
+ scrollOffset: this.scrollOffset,
598
+ direction: this.directionVector,
599
+ speed: this.pixelsPerSecond,
600
+ isPlaying: this.isPlaying,
601
+ isHoverSuspended: this.isHovered && this.pauseOnHover,
602
+ itemsPerPage: this.getItemsPerPage()
603
+ });
604
+ }
605
+ }
606
+ }
607
+
608
+ // Global Exposure
609
+ if (typeof window !== 'undefined') {
610
+ window.Smooth = Smooth;
611
+ window.smooth = new Smooth();
612
+ }
613
+
614
+ // Module Support
615
+ if (typeof module !== 'undefined' && module.exports) {
616
+ module.exports = Smooth;
617
+ }
618
+
619
+ /*
620
+ Example usage:
621
+
622
+ smooth.start([
623
+ {
624
+ selector: '#carousel-container',
625
+ itemSelector: '.carousol-image',
626
+ idPrefix: 'impact-carousel-image',
627
+ speed: 80,
628
+ direction: 'left'
629
+ },
630
+ {
631
+ selector: '#schools-carousel-container',
632
+ itemSelector: '.partner-logo',
633
+ idPrefix: 'schools-carousel-logo',
634
+ speed: 70,
635
+ direction: 'right'
636
+ }
637
+ ]);
638
+
639
+ You can also use the classic chainable API:
640
+
641
+ smooth
642
+ .mount('#carousel-container')
643
+ .items(['img1', 'img2', 'img3', 'img4'])
644
+ .speed(100)
645
+ .direction('left')
646
+ .play();
647
+ */
@@ -0,0 +1,36 @@
1
+ const app = document.getElementById("app");
2
+
3
+ const routeLink = (label, route) => Chex.button({
4
+ class: window.router.path.value === route ? "nav-btn active" : "nav-btn",
5
+ onclick: () => window.router.navigate(route)
6
+ }, label);
7
+
8
+ const App = () => Chex.div({ class: "app" }, [
9
+ Chex.h1({ class: "hero-title" }, "Chex Multi-Page Demo"),
10
+ Chex.p({ class: "hero-copy" }, "Each page lives in its own file under components/."),
11
+ Chex.div({ class: "nav" }, [
12
+ routeLink("Home", "/home"),
13
+ routeLink("About", "/about"),
14
+ routeLink("Contact", "/contact"),
15
+ routeLink("Demo", "/demo")
16
+ ]),
17
+ window.router.view()
18
+ ]);
19
+
20
+ Chex.render(app, App);
21
+
22
+ if (window.router.path.value === "/") {
23
+ window.router.navigate("/home");
24
+ }
25
+
26
+ Chex.animate('.hero-title', {
27
+ slideFrom: "top",
28
+ duration: 700,
29
+ opacity: [0, 1]
30
+ });
31
+
32
+ Chex.animate('.page', {
33
+ slideFrom: "bottom",
34
+ duration: 500,
35
+ opacity: [0, 1]
36
+ });
@@ -0,0 +1 @@
1
+