skeleton-loader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +401 -0
  4. data/app/assets/javascripts/skeleton_loader.js +2 -0
  5. data/app/assets/javascripts/skeleton_loader.js.LICENSE.txt +1 -0
  6. data/app/assets/stylesheets/skeleton_loader.css +83 -0
  7. data/app/controllers/skeleton_loader/skeleton_loader_controller.rb +60 -0
  8. data/app/javascript/client_skeleton_loader.js +234 -0
  9. data/app/javascript/server_skeleton_loader.js +113 -0
  10. data/app/javascript/skeleton_loader.js +6 -0
  11. data/config/routes.rb +5 -0
  12. data/lib/generators/skeleton_loader/add_templates_generator.rb +16 -0
  13. data/lib/generators/skeleton_loader/reset_templates_generator.rb +16 -0
  14. data/lib/generators/skeleton_loader/templates/_card.html.erb +31 -0
  15. data/lib/generators/skeleton_loader/templates/_comment.html.erb +61 -0
  16. data/lib/generators/skeleton_loader/templates/_default.html.erb +23 -0
  17. data/lib/generators/skeleton_loader/templates/_gallery.html.erb +19 -0
  18. data/lib/generators/skeleton_loader/templates/_paragraph.html.erb +28 -0
  19. data/lib/generators/skeleton_loader/templates/_product.html.erb +49 -0
  20. data/lib/generators/skeleton_loader/templates/_profile.html.erb +28 -0
  21. data/lib/skeleton-loader.rb +7 -0
  22. data/lib/skeleton_loader/configuration.rb +68 -0
  23. data/lib/skeleton_loader/engine.rb +42 -0
  24. data/lib/skeleton_loader/skeleton_element_generator.rb +49 -0
  25. data/lib/skeleton_loader/template_path_finder.rb +24 -0
  26. data/lib/skeleton_loader/template_renderer.rb +41 -0
  27. data/lib/skeleton_loader/version.rb +5 -0
  28. data/lib/skeleton_loader/view_helpers.rb +19 -0
  29. data/lib/skeleton_loader.rb +36 -0
  30. data/lib/tasks/skeleton_loader_tasks.rake +23 -0
  31. metadata +91 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1f3870a383b99e72e3c7536797fba096526f8b9d72659f09f718c4a7790c16f
4
+ data.tar.gz: 19e17acfcf43058cba8ce8acc1040be050e078e14a73ee8bf2d30c0b6bb67ed3
5
+ SHA512:
6
+ metadata.gz: d0c4b8979f8984131fb39a49fb95e459b090911cf96f78fd72dfa9e64cd9eaf6d8993900be5eccbb0a341200a586bb6e83c6f319b7f68eaf47ce7f8614c48f03
7
+ data.tar.gz: c8bbf8deb35d75bed0d5ed768a0ca5e8f36a411e85667c1cf594ba1edc93f386ec39f52ad52d9876374c09c2fab00f13f4bf1b73ffa08114b63ae039fbb75473
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Emad Rahimi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,401 @@
1
+ # Skeleton Loader
2
+
3
+ ![Gem Version](https://img.shields.io/badge/gem-v0.1.0-brightgreen)
4
+ [![CircleCI](https://dl.circleci.com/status-badge/img/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main.svg?style=svg&circle-token=CCIPRJ_PF8xu3Svcj2Ro4D8jhjCi7_71b7c0a7c781e09fc7194cd58cca67aecdc111b5)](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
5
+ ![Test Coverage: 100%](https://img.shields.io/badge/Test%20Coverage-100%25-brightgreen)
6
+ [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ **Skeleton Loader** is a Ruby on Rails gem for creating animated placeholders that enhance loading states. Whether rendered through Rails views or dynamically with JavaScript, these skeletons provide a seamless visual experience while content loads.
9
+
10
+ **⚠ Note:** Skeleton Loader is an experimental gem currently in early development, exploring the possibilities in the magical world of Rails gems. While loaders are ideally handled client-side, this gem aims to make it easy to add placeholders directly within the Rails realm.
11
+
12
+ ---
13
+
14
+ ## Table of Contents
15
+
16
+ - [Features](#features)
17
+ - [Installation](#installation)
18
+ - [Setup](#setup)
19
+ - [Rails Integration](#rails-integration)
20
+ - [JavaScript Integration](#javascript-integration)
21
+ - [Configuration](#configuration)
22
+ - [Code Quality](#code-quality)
23
+ - [Roadmap](#roadmap)
24
+ - [Contributing](#contributing)
25
+ - [License](#license)
26
+
27
+ ---
28
+
29
+ ![1](https://github.com/user-attachments/assets/f6f91f55-1bfa-42eb-9e8c-41606eb8afd5)
30
+
31
+ ## Features
32
+
33
+ - 🚀 **Seamless Rails integration:** Enhances loading transitions with minimal setup.
34
+ - 🔨 **Universal skeletons:** Creates loading states in Rails views & JavaScript.
35
+ - ⚙️ **Consistency:** Unified templates & options across both environments.
36
+ - ⚡ **Lightweight & fast:** Designed to be fast and easy to implement with minimal dependencies.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application’s Gemfile:
43
+
44
+ ```ruby
45
+ gem "skeleton-loader"
46
+ ```
47
+
48
+ Then, run:
49
+
50
+ ```bash
51
+ bundle install
52
+ ```
53
+
54
+ #### Requirements
55
+
56
+ - **Ruby** 2.5 or higher
57
+ - **Rails** 5.0 or higher
58
+ - **Asset Pipeline** (Required for CSS handling)
59
+
60
+ ---
61
+
62
+ ## Setup
63
+
64
+ ### CSS Assets
65
+
66
+ The gem's CSS is always managed via the Asset Pipeline:
67
+ ```javascript
68
+ /* app/assets/stylesheets/application.css */
69
+ *= require skeleton_loader
70
+ ```
71
+
72
+ ### JavaScript Assets
73
+
74
+ Include the JavaScript functionality using Asset Pipeline, Webpack, or Importmap, depending on your setup:
75
+
76
+
77
+ #### **Option 1: Asset Pipeline (Default for Rails 5)**
78
+
79
+ In `app/assets/javascripts/application.js`, add:
80
+
81
+ ```javascript
82
+ //= require skeleton_loader
83
+ ```
84
+
85
+ #### **Option 2: Webpack (Default for Rails 6)**
86
+
87
+ 1. Install the package using Yarn:
88
+
89
+ ```bash
90
+ yarn add "@ersync/skeleton-loader"
91
+ ```
92
+
93
+ ```javascript
94
+ import SkeletonLoader from "skeleton-loader"
95
+ ```
96
+
97
+ #### **Option 3: Importmap (Default for Rails 7)**
98
+
99
+ 1. Pin the package in `config/importmap.rb`:
100
+
101
+ ```ruby
102
+ pin "skeleton-loader", to: "skeleton_loader.js"
103
+ ```
104
+
105
+ ```javascript
106
+ import SkeletonLoader from "skeleton-loader"
107
+ ```
108
+
109
+ ### Install Templates
110
+
111
+ To install predefined templates, run:
112
+
113
+ ```bash
114
+ rails generate skeleton_loader:add_templates
115
+ ```
116
+
117
+ This adds templates to `app/views/skeleton_loader/`. Customize them as needed.
118
+
119
+ To restore defaults later:
120
+
121
+ ```bash
122
+ rails generate skeleton_loader:reset_templates
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Rails Integration
128
+
129
+ The gem provides a primary view helper, `skeleton_loader`, which generates a skeleton that is automatically replaced with content once loading completes. You can use it with predefined templates or define your own HTML blocks.
130
+
131
+ ```ruby
132
+ skeleton_loader(
133
+ content_id:, # Required. Target element ID for replacement
134
+ **options, # Optional. Customize the template
135
+ &block # Optional. Define custom skeleton HTML (excludes type/options)
136
+ )
137
+ ```
138
+
139
+ ### 1. Pre-defined Templates
140
+
141
+ Specify `content_id` and, optionally, a template `type` and customization `options`.
142
+
143
+ ```erb
144
+ <%= skeleton_loader(content_id: 'content-element',
145
+ type: "card",
146
+ count: 5,
147
+ scale: 1.2,
148
+ animation_type: 'animation-pulse') %>
149
+ ```
150
+
151
+ ### 2. Custom Skeletons with a Block
152
+
153
+ Define your own HTML structure in a block. Note that `type` and `options` are not used.
154
+
155
+ ```erb
156
+ <%= skeleton_loader(content_id: 'content-element') do %>
157
+ <div class="custom-skeleton">
158
+ <div class="avatar skeleton-circle"></div>
159
+ <div class="text-lines"></div>
160
+ </div>
161
+ <% end %>
162
+ ```
163
+
164
+ See [Configuration](#configuration) for all available options.
165
+
166
+ ---
167
+
168
+ ## JavaScript Integration
169
+
170
+ Skeleton Loader also provides a JavaScript API to dynamically create skeletons independently of Rails views.
171
+
172
+ <details>
173
+ <summary>💡 You only need this API if you're loading content asynchronously i.e. AJAX calls. For standard Rails views, use the Rails helper method instead.</summary>
174
+
175
+ ### 1. Basic Setup
176
+
177
+ First, initialize the SkeletonLoader in your JavaScript:
178
+
179
+ ```javascript
180
+ const skeleton = new SkeletonLoader();
181
+ ```
182
+
183
+ ### 2. Creating Skeletons
184
+
185
+ There are two methods for creating skeletons using JavaScript:
186
+
187
+ #### Using Pre-defined Templates
188
+
189
+ ```javascript
190
+ const loader = skeleton.render({
191
+ contentId, // Required. ID of the element to replace with skeleton
192
+ ...options // Optional. See configuration section for available options
193
+ });
194
+ ```
195
+
196
+ #### Creating Custom Skeletons
197
+
198
+ ```javascript
199
+ const loader = skeleton.renderCustom({
200
+ contentId, // Required. ID of the element to replace with skeleton
201
+ markup // Required. Custom HTML string for skeleton content
202
+ });
203
+ ```
204
+
205
+ ### 3. Managing Loading States
206
+
207
+ Each skeleton instance (both `render()` and `renderCustom()`) returns an object with these methods:
208
+
209
+ - `isLoading()`: Returns the current loading state
210
+ - `reveal()`: Removes skeleton and displays content
211
+
212
+ ```javascript
213
+ // Check if still loading
214
+ if (loader.isLoading()) {
215
+ // Do something while content loads
216
+ }
217
+
218
+ // Remove skeleton and show content
219
+ loader.reveal();
220
+ ```
221
+
222
+ <details>
223
+ <summary><strong>Practical Examples</strong></summary>
224
+
225
+ Here's some example showing how to use the skeleton loader with an API call:
226
+
227
+ ```javascript
228
+ async function loadUserProfile(userId) {
229
+ // Create skeleton while loading
230
+ const loader = skeleton.render({
231
+ contentId: 'content-element',
232
+ type: 'profile'
233
+ });
234
+
235
+ try {
236
+ // Fetch your data
237
+ const response = await fetch(`/api/users/${userId}`);
238
+ const userData = await response.json();
239
+
240
+ // Update the DOM with your content
241
+ document.getElementById('content-element').innerHTML = createUserProfile(userData);
242
+
243
+ // Remove the skeleton
244
+ loader.reveal();
245
+ } catch (error) {
246
+ console.error('Failed to load user profile:', error);
247
+ loader.reveal(); // Always cleanup the skeleton
248
+ }
249
+ }
250
+ ```
251
+
252
+ ```javascript
253
+ async function loadDashboard() {
254
+ // Create multiple skeletons
255
+ const loaders = {
256
+ profile: skeleton.render({ contentId: 'profile', type: 'profile', scale:1.2, width:250 }),
257
+ stats: skeleton.render({ contentId: 'stats', type: 'card', scale:1.3, animationType:"sl-flow" }),
258
+ activities: skeleton.render({ contentId: 'activities', type: 'list' })
259
+ };
260
+
261
+ // Load your data
262
+ const [profileData, statsData, activitiesData] = await Promise.all([
263
+ fetchProfile(),
264
+ fetchStats(),
265
+ fetchActivities()
266
+ ]);
267
+
268
+ // Update content and reveal each section
269
+ updateProfile(profileData);
270
+ loaders.profile.reveal();
271
+
272
+ updateStats(statsData);
273
+ loaders.stats.reveal();
274
+
275
+ updateActivities(activitiesData);
276
+ loaders.activities.reveal();
277
+ }
278
+ ```
279
+ </details>
280
+ </details>
281
+
282
+ ---
283
+
284
+ ## Configuration
285
+
286
+ ### Options
287
+
288
+ The following options can be passed to both the Rails view helper and JavaScript API. Note that Rails uses snake_case (e.g., `animation_type`), while JavaScript uses camelCase (e.g., `animationType`).
289
+
290
+
291
+ | Option | Type | Description | Default | Example |
292
+ |--------|------|-------------|---------|---------|
293
+ | `content_id` | String | Unique identifier for the skeleton loader container | `nil` | `"content-element"` |
294
+ | `type` | String | Predefined template type (e.g., "product", "profile") | `"default"` | `"product"` |
295
+ | `width` | Integer | Base width of a single skeleton item in pixels | Depends on `type` | `250` |
296
+ | `count` | Integer | Number of skeleton items to render | Depends on `type` | `6` |
297
+ | `per_row` | Integer | Number of skeleton items displayed per row | Depends on `type` | `4` |
298
+ | `scale` | Float | Size multiplier for all skeleton item dimensions | `1.0` | `1.2` |
299
+ | `animation_type` | String | Type of loading animation | `"sl-gradient"` | `"sl-glow"` |
300
+
301
+ **Notes:**
302
+ - `type` determines default layout and styling
303
+ - `scale` affects width and spacing proportionally
304
+ - `animation_type` supports different loading effect styles
305
+
306
+ ### Available Templates
307
+
308
+ Skeleton Loader comes with several pre-built templates, each with their **default** configurations:
309
+
310
+ | Template Type | Default Width | Count | Items Per Row |
311
+ |--------------|---------------|-------|---------------|
312
+ | `card` | 200px | 3 | 3 |
313
+ | `comment` | 900px | 2 | 1 |
314
+ | `default` | 900px | 1 | 1 |
315
+ | `gallery` | 320px | 3 | 3 |
316
+ | `paragraph` | 900px | 1 | 1 |
317
+ | `product` | 320px | 3 | 3 |
318
+ | `profile` | 320px | 3 | 3 |
319
+
320
+ For an interactive preview of available templates and animations, visit the [Live Demo](https://ersync.github.io/skeleton-loader/).
321
+
322
+ ### Available Animations
323
+
324
+ Choose from several animation styles to match your design:
325
+ - `sl-gradient` (default): Smooth gradient movement
326
+ - `sl-shine`: Shimmer effect
327
+ - `sl-pulse`: Fade in/out
328
+ - `sl-flow`: Continuous flow
329
+ - `sl-neon`: Subtle glow
330
+ - `sl-breathing`: Gentle scaling
331
+
332
+ ### Application Defaults
333
+
334
+ To set application-wide defaults, create `config/initializers/skeleton_loader.rb`:
335
+
336
+ ```ruby
337
+ SkeletonLoader.configure do |config|
338
+ # Override default template settings
339
+ config.templates[:product] = {
340
+ width: 400,
341
+ count: 6,
342
+ per_row: 3
343
+ }
344
+ # Global settings
345
+ config.scale = 1.0 # Default: 1.0
346
+ config.animation_type = "sl-gradient" # Default: "sl-gradient"
347
+ end
348
+ ```
349
+
350
+ ### Advanced Configuration
351
+
352
+ For applications requiring custom HTML elements or styles:
353
+
354
+ ```ruby
355
+ SkeletonLoader.configure do |config|
356
+ config.additional_allowed_tags = [] # Default: []
357
+ config.additional_allowed_attributes = {} # Default: {}
358
+ config.additional_allowed_css_properties = [] # Default: []
359
+ end
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Code Quality
365
+
366
+ Skeleton Loader maintains code quality through:
367
+
368
+ - **RSpec**: Comprehensive tests covering core functionality.
369
+ - **CircleCI**: Continuous integration ensures all new changes meet quality standards.
370
+ - **Rubocop**: Consistent linting aligns code with Ruby community conventions.
371
+
372
+ ---
373
+
374
+ ## Roadmap
375
+
376
+ Here are some features I'd like to add when I have time:
377
+
378
+ - **New Templates & Animations**: Expanding template and animation options.
379
+ - **Turbo & Stimulus Support**: Enhancing compatibility with Rails Turbo and StimulusJS.
380
+ - **Builder Helper for Custom Skeletons:**: Introducing a helper to easily create custom skeletons (e.g., circles, rectangles, etc.) with customizable options, simplifying the process of designing custom skeletons.
381
+
382
+ Suggestions and contributions are welcome!
383
+
384
+ ---
385
+
386
+ ## Contributing
387
+
388
+ To contribute:
389
+
390
+ 1. Fork the repository.
391
+ 2. Create a new feature branch.
392
+ 3. Add tests for new features.
393
+ 4. Commit your changes and submit a pull request.
394
+
395
+ Please follow the [Code of Conduct](https://github.com/ersync/skeleton-loader/blob/main/CODE_OF_CONDUCT.md) for all contributions.
396
+
397
+ ---
398
+
399
+ ## License
400
+
401
+ Skeleton Loader is licensed under the MIT License. See [LICENSE](https://github.com/ersync/skeleton-loader/blob/main/LICENSE) for details.
@@ -0,0 +1,2 @@
1
+ /*! For license information please see skeleton_loader.js.LICENSE.txt */
2
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SkeletonLoader=e():t.SkeletonLoader=e()}(this,(()=>(()=>{"use strict";var t={d:(e,r)=>{for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>E});var r=["contentId","type"],n=["contentId","mode","markup","type"],o=["contentId","mode","markup","type"];function i(t){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i(t)}function a(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=Array(e);r<e;r++)n[r]=t[r];return n}function c(){c=function(){return e};var t,e={},r=Object.prototype,n=r.hasOwnProperty,o=Object.defineProperty||function(t,e,r){t[e]=r.value},a="function"==typeof Symbol?Symbol:{},u=a.iterator||"@@iterator",l=a.asyncIterator||"@@asyncIterator",s=a.toStringTag||"@@toStringTag";function f(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{f({},"")}catch(t){f=function(t,e,r){return t[e]=r}}function h(t,e,r,n){var i=e&&e.prototype instanceof b?e:b,a=Object.create(i.prototype),c=new I(n||[]);return o(a,"_invoke",{value:T(t,r,c)}),a}function p(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=h;var y="suspendedStart",d="suspendedYield",v="executing",m="completed",g={};function b(){}function w(){}function S(){}var k={};f(k,u,(function(){return this}));var E=Object.getPrototypeOf,L=E&&E(E(A([])));L&&L!==r&&n.call(L,u)&&(k=L);var O=S.prototype=b.prototype=Object.create(k);function x(t){["next","throw","return"].forEach((function(e){f(t,e,(function(t){return this._invoke(e,t)}))}))}function j(t,e){function r(o,a,c,u){var l=p(t[o],t,a);if("throw"!==l.type){var s=l.arg,f=s.value;return f&&"object"==i(f)&&n.call(f,"__await")?e.resolve(f.__await).then((function(t){r("next",t,c,u)}),(function(t){r("throw",t,c,u)})):e.resolve(f).then((function(t){s.value=t,c(s)}),(function(t){return r("throw",t,c,u)}))}u(l.arg)}var a;o(this,"_invoke",{value:function(t,n){function o(){return new e((function(e,o){r(t,n,e,o)}))}return a=a?a.then(o,o):o()}})}function T(e,r,n){var o=y;return function(i,a){if(o===v)throw Error("Generator is already running");if(o===m){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var c=n.delegate;if(c){var u=_(c,n);if(u){if(u===g)continue;return u}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===y)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=v;var l=p(e,r,n);if("normal"===l.type){if(o=n.done?m:d,l.arg===g)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o=m,n.method="throw",n.arg=l.arg)}}}function _(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,_(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),g;var i=p(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,g;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,g):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,g)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function A(e){if(e||""===e){var r=e[u];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,a=function r(){for(;++o<e.length;)if(n.call(e,o))return r.value=e[o],r.done=!1,r;return r.value=t,r.done=!0,r};return a.next=a}}throw new TypeError(i(e)+" is not iterable")}return w.prototype=S,o(O,"constructor",{value:S,configurable:!0}),o(S,"constructor",{value:w,configurable:!0}),w.displayName=f(S,s,"GeneratorFunction"),e.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===w||"GeneratorFunction"===(e.displayName||e.name))},e.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,S):(t.__proto__=S,f(t,s,"GeneratorFunction")),t.prototype=Object.create(O),t},e.awrap=function(t){return{__await:t}},x(j.prototype),f(j.prototype,l,(function(){return this})),e.AsyncIterator=j,e.async=function(t,r,n,o,i){void 0===i&&(i=Promise);var a=new j(h(t,r,n,o),i);return e.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},x(O),f(O,s,"Generator"),f(O,u,(function(){return this})),f(O,"toString",(function(){return"[object Generator]"})),e.keys=function(t){var e=Object(t),r=[];for(var n in e)r.push(n);return r.reverse(),function t(){for(;r.length;){var n=r.pop();if(n in e)return t.value=n,t.done=!1,t}return t.done=!0,t}},e.values=A,I.prototype={constructor:I,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var r in this)"t"===r.charAt(0)&&n.call(this,r)&&!isNaN(+r.slice(1))&&(this[r]=t)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var r=this;function o(n,o){return c.type="throw",c.arg=e,r.next=n,o&&(r.method="next",r.arg=t),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(u&&l){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(u){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!l)throw Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,g):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),g},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;C(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),g}},e}function u(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function l(t){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?u(Object(r),!0).forEach((function(e){y(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):u(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}function s(t,e){if(null==t)return{};var r,n,o=function(t,e){if(null==t)return{};var r={};for(var n in t)if({}.hasOwnProperty.call(t,n)){if(e.includes(n))continue;r[n]=t[n]}return r}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(n=0;n<i.length;n++)r=i[n],e.includes(r)||{}.propertyIsEnumerable.call(t,r)&&(o[r]=t[r])}return o}function f(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function h(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var i=t.apply(e,r);function a(t){f(i,n,o,a,c,"next",t)}function c(t){f(i,n,o,a,c,"throw",t)}a(void 0)}))}}function p(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,d(n.key),n)}}function y(t,e,r){return(e=d(e))in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}function d(t){var e=function(t){if("object"!=i(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var r=e.call(t,"string");if("object"!=i(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==i(e)?e:e+""}var v=function(){function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.defaultDisplayStyles={},this.skeletonClass="skeleton-loader--client",this.contentRefAttr="data-content-id",this.apiEndpoint="/skeleton_loader/templates"}return e=t,i=[{key:"getSkeletonInstance",value:function(e){return t.activeSkeletons.has(e)||t.activeSkeletons.set(e,this.createSkeletonInstance(e)),t.activeSkeletons.get(e)}},{key:"createSkeletonInstance",value:function(e){var r=this;return{isLoading:function(){return t.loadingStates.get(e)||!1},reveal:function(){var t=document.querySelector("[".concat(r.contentRefAttr,'="').concat(e,'"]')),n=document.getElementById(e);t&&n&&r.revealContent(t,n)}}}},{key:"render",value:(m=h(c().mark((function t(){var e,n,o,i,a,u=arguments;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=(e=u.length>0&&void 0!==u[0]?u[0]:{}).contentId,o=e.type,i=void 0===o?"default":o,a=s(e,r),t.abrupt("return",this.processRender(l({contentId:n,mode:"predefined",type:i},a)));case 2:case"end":return t.stop()}}),t,this)}))),function(){return m.apply(this,arguments)})},{key:"renderCustom",value:(v=h(c().mark((function t(){var e,r,n,o=arguments;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(r=(e=o.length>0&&void 0!==o[0]?o[0]:{}).contentId,n=e.markup){t.next=3;break}return t.abrupt("return",this.logError("Custom markup is required"));case 3:return t.abrupt("return",this.processRender({contentId:r,mode:"custom",markup:n}));case 4:case"end":return t.stop()}}),t,this)}))),function(){return v.apply(this,arguments)})},{key:"processRender",value:(d=h(c().mark((function t(e){var r,n,o,i;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(r=e.contentId){t.next=3;break}return t.abrupt("return",this.logError("contentId is required"));case 3:if(!(n=this.getSkeletonInstance(r)).isLoading()){t.next=7;break}return console.warn("Skeleton loading already in progress for ".concat(r)),t.abrupt("return",n);case 7:return t.prev=7,t.next=10,this.initializeLoading(r);case 10:if(o=t.sent){t.next=13;break}return t.abrupt("return",n);case 13:return t.next=15,this.createSkeletonElement(e);case 15:return(i=t.sent)&&this.showSkeleton(i,o),t.abrupt("return",n);case 20:return t.prev=20,t.t0=t.catch(7),console.error("[SkeletonLoader] Render failed:",t.t0),t.abrupt("return",n);case 24:case"end":return t.stop()}}),t,this,[[7,20]])}))),function(t){return d.apply(this,arguments)})},{key:"initializeLoading",value:(y=h(c().mark((function e(r){return c().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t.loadingStates.set(r,!0),e.abrupt("return",this.hideContent(r));case 2:case"end":return e.stop()}}),e,this)}))),function(t){return y.apply(this,arguments)})},{key:"hideContent",value:function(t){var e=document.getElementById(t);if(!e)return console.error("Content not found: #".concat(t)),null;var r=getComputedStyle(e).display;return"none"!==r&&(this.defaultDisplayStyles[t]=r),e.style.display="none",e}},{key:"revealContent",value:function(e,r){var n=r.id;t.loadingStates.get(n)&&(e.remove(),r.style.display=this.defaultDisplayStyles[n],t.loadingStates.set(n,!1))}},{key:"showSkeleton",value:function(t,e){this.clearPreviousSkeleton(e.id),t.setAttribute(this.contentRefAttr,e.id),t.style.display="block",e.parentNode.insertBefore(t,e)}},{key:"clearPreviousSkeleton",value:function(t){document.querySelectorAll("[".concat(this.contentRefAttr,'="').concat(t,'"]')).forEach((function(t){return t.remove()}))}},{key:"createSkeletonElement",value:(f=h(c().mark((function t(e){var r,o,i,a,u,f,h,p;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=e.contentId,o=e.mode,i=e.markup,a=void 0===i?null:i,u=e.type,f=s(e,n),h=this.buildParams(l({contentId:r,mode:o,markup:a,type:u},f)),t.prev=2,t.next=5,this.fetchTemplate(h);case 5:return p=t.sent,t.abrupt("return",this.parseTemplateToElement(p,r));case 9:return t.prev=9,t.t0=t.catch(2),console.error("Error creating skeleton:",t.t0),t.abrupt("return",null);case 13:case"end":return t.stop()}}),t,this,[[2,9]])}))),function(t){return f.apply(this,arguments)})},{key:"fetchTemplate",value:(u=h(c().mark((function t(e){var r;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,fetch("".concat(this.apiEndpoint,"?").concat(e.toString()),{method:"GET",headers:{"X-Requested-With":"XMLHttpRequest"}});case 2:if((r=t.sent).ok){t.next=5;break}throw new Error("Failed to fetch skeleton template");case 5:return t.abrupt("return",r.text());case 6:case"end":return t.stop()}}),t,this)}))),function(t){return u.apply(this,arguments)})},{key:"parseTemplateToElement",value:function(t,e){return(new DOMParser).parseFromString(t,"text/html").body.firstElementChild||(console.error("Invalid skeleton template for contentId: ".concat(e)),null)}},{key:"buildParams",value:function(t){var e=t.contentId,r=t.mode,n=t.markup,i=t.type,a=s(t,o),c=new URLSearchParams(l({content_id:e,mode:r,type:i||"default"},this.formatOptions(a)));return n&&c.set("markup",n),c}},{key:"formatOptions",value:function(t){var e=this;return Object.fromEntries(Object.entries(t).map((function(t){var r=function(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var n,o,i,a,c=[],u=!0,l=!1;try{if(i=(r=r.call(t)).next,0===e){if(Object(r)!==r)return;u=!1}else for(;!(u=(n=i.call(r)).done)&&(c.push(n.value),c.length!==e);u=!0);}catch(t){l=!0,o=t}finally{try{if(!u&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(l)throw o}}return c}}(t,e)||function(t,e){if(t){if("string"==typeof t)return a(t,e);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?a(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(t,2),n=r[0],o=r[1];return[e.toSnakeCase(n),o]})))}},{key:"toSnakeCase",value:function(t){return t.replace(/([A-Z])/g,"_$1").toLowerCase()}},{key:"logError",value:function(t){return console.error("[SkeletonLoader] ".concat(t)),null}}],i&&p(e.prototype,i),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,i,u,f,y,d,v,m}();function m(t){return m="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},m(t)}function g(){g=function(){return e};var t,e={},r=Object.prototype,n=r.hasOwnProperty,o=Object.defineProperty||function(t,e,r){t[e]=r.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",c=i.asyncIterator||"@@asyncIterator",u=i.toStringTag||"@@toStringTag";function l(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{l({},"")}catch(t){l=function(t,e,r){return t[e]=r}}function s(t,e,r,n){var i=e&&e.prototype instanceof b?e:b,a=Object.create(i.prototype),c=new I(n||[]);return o(a,"_invoke",{value:T(t,r,c)}),a}function f(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=s;var h="suspendedStart",p="suspendedYield",y="executing",d="completed",v={};function b(){}function w(){}function S(){}var k={};l(k,a,(function(){return this}));var E=Object.getPrototypeOf,L=E&&E(E(A([])));L&&L!==r&&n.call(L,a)&&(k=L);var O=S.prototype=b.prototype=Object.create(k);function x(t){["next","throw","return"].forEach((function(e){l(t,e,(function(t){return this._invoke(e,t)}))}))}function j(t,e){function r(o,i,a,c){var u=f(t[o],t,i);if("throw"!==u.type){var l=u.arg,s=l.value;return s&&"object"==m(s)&&n.call(s,"__await")?e.resolve(s.__await).then((function(t){r("next",t,a,c)}),(function(t){r("throw",t,a,c)})):e.resolve(s).then((function(t){l.value=t,a(l)}),(function(t){return r("throw",t,a,c)}))}c(u.arg)}var i;o(this,"_invoke",{value:function(t,n){function o(){return new e((function(e,o){r(t,n,e,o)}))}return i=i?i.then(o,o):o()}})}function T(e,r,n){var o=h;return function(i,a){if(o===y)throw Error("Generator is already running");if(o===d){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var c=n.delegate;if(c){var u=_(c,n);if(u){if(u===v)continue;return u}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===h)throw o=d,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=y;var l=f(e,r,n);if("normal"===l.type){if(o=n.done?d:p,l.arg===v)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o=d,n.method="throw",n.arg=l.arg)}}}function _(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,_(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),v;var i=f(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,v;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,v):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,v)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function A(e){if(e||""===e){var r=e[a];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,i=function r(){for(;++o<e.length;)if(n.call(e,o))return r.value=e[o],r.done=!1,r;return r.value=t,r.done=!0,r};return i.next=i}}throw new TypeError(m(e)+" is not iterable")}return w.prototype=S,o(O,"constructor",{value:S,configurable:!0}),o(S,"constructor",{value:w,configurable:!0}),w.displayName=l(S,u,"GeneratorFunction"),e.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===w||"GeneratorFunction"===(e.displayName||e.name))},e.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,S):(t.__proto__=S,l(t,u,"GeneratorFunction")),t.prototype=Object.create(O),t},e.awrap=function(t){return{__await:t}},x(j.prototype),l(j.prototype,c,(function(){return this})),e.AsyncIterator=j,e.async=function(t,r,n,o,i){void 0===i&&(i=Promise);var a=new j(s(t,r,n,o),i);return e.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},x(O),l(O,u,"Generator"),l(O,a,(function(){return this})),l(O,"toString",(function(){return"[object Generator]"})),e.keys=function(t){var e=Object(t),r=[];for(var n in e)r.push(n);return r.reverse(),function t(){for(;r.length;){var n=r.pop();if(n in e)return t.value=n,t.done=!1,t}return t.done=!0,t}},e.values=A,I.prototype={constructor:I,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var r in this)"t"===r.charAt(0)&&n.call(this,r)&&!isNaN(+r.slice(1))&&(this[r]=t)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var r=this;function o(n,o){return c.type="throw",c.arg=e,r.next=n,o&&(r.method="next",r.arg=t),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(u&&l){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(u){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!l)throw Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,v):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),v},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),v}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;C(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),v}},e}function b(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function w(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var i=t.apply(e,r);function a(t){b(i,n,o,a,c,"next",t)}function c(t){b(i,n,o,a,c,"throw",t)}a(void 0)}))}}function S(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,k(n.key),n)}}function k(t){var e=function(t){if("object"!=m(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var r=e.call(t,"string");if("object"!=m(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==m(e)?e:e+""}y(v,"loadingStates",new Map),y(v,"activeSkeletons",new Map),(new(function(){function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.SKELETON_CLASS="skeleton-loader--server",this.CONTENT_ID_ATTR="data-content-id",this.contentsDisplayStyles={}}return e=t,r=[{key:"start",value:function(){return this.setupInitialLoading(),this.setupContentsSwap(),this}},{key:"setupInitialLoading",value:function(){var t=this;document.addEventListener("DOMContentLoaded",(function(){t.captureContentsDisplayStyles(),t.hideContents(),t.showSkeletons()}))}},{key:"setupContentsSwap",value:function(){var t=this;window.addEventListener("load",w(g().mark((function e(){return g().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t.revealContent();case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)}))))}},{key:"captureContentsDisplayStyles",value:function(){var e=this;document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(r){var n=r.getAttribute(t.CONTENT_ID_ATTR),o=document.getElementById(n);if(o){var i=getComputedStyle(o).display||"block";e.contentsDisplayStyles[n]=i}else console.warn('Content element with id "'.concat(n,'" not found'))}))}},{key:"showSkeletons",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(t){t.style.display="block"}))}},{key:"revealContent",value:(o=w(g().mark((function t(){return g().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:this.hideSkeletons(),this.showContents();case 2:case"end":return t.stop()}}),t,this)}))),function(){return o.apply(this,arguments)})},{key:"hideSkeletons",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(t){t.style.display="none"}))}},{key:"showContents",value:function(){var e=this;document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(r){var n=r.getAttribute(t.CONTENT_ID_ATTR),o=document.getElementById(n);if(o){var i=e.contentsDisplayStyles[n]||"block";o.style.display=i,o.style.visibility="visible"}else console.warn('Content element with id "'.concat(n,'" not found'))}))}},{key:"hideContents",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(e){var r=e.getAttribute(t.CONTENT_ID_ATTR),n=document.getElementById(r);n?n.style.display="none":console.warn('Content element with id "'.concat(r,'" not found'))}))}}],n=[{key:"SKELETON_CLASS",get:function(){return"skeleton-loader--server"}},{key:"CONTENT_ID_ATTR",get:function(){return"data-content-id"}}],r&&S(e.prototype,r),n&&S(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,r,n,o}())).start();const E=v;return e.default})()));
@@ -0,0 +1 @@
1
+ /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
@@ -0,0 +1,83 @@
1
+ .skeleton-loading {
2
+ background-color: #e0e0e0;
3
+ border-radius: 10px;
4
+ overflow: hidden;
5
+ }
6
+
7
+ /* Animation Keyframes */
8
+ @keyframes skeleton-pulse {
9
+ 0%, 100% { opacity: 1; }
10
+ 50% { opacity: 0.5; }
11
+ }
12
+
13
+ @keyframes skeleton-flow {
14
+ 0% { opacity: 0.6; }
15
+ 50% { opacity: 0.3; }
16
+ 100% { opacity: 0.6; }
17
+ }
18
+
19
+ @keyframes skeleton-shine {
20
+ 0% { background-position: -201% center; }
21
+ 100% { background-position: 198% center; }
22
+ }
23
+
24
+ @keyframes skeleton-gradient {
25
+ 0% { background-position: 0% 50%; }
26
+ 50% { background-position: 100% 50%; }
27
+ 100% { background-position: 0% 50%; }
28
+ }
29
+
30
+ @keyframes skeleton-neon {
31
+ 0% {
32
+ background: #e0e0e0;
33
+ box-shadow: 0 0 5px rgba(224, 224, 224, 0.5);
34
+ }
35
+ 50% {
36
+ background: #e8e8e8;
37
+ box-shadow: 0 0 15px rgba(224, 224, 224, 0.8);
38
+ }
39
+ 100% {
40
+ background: #e0e0e0;
41
+ box-shadow: 0 0 5px rgba(224, 224, 224, 0.5);
42
+ }
43
+ }
44
+
45
+ @keyframes skeleton-breathing {
46
+ 0%, 100% {
47
+ transform: scale(1);
48
+ opacity: 0.7;
49
+ }
50
+ 50% {
51
+ transform: scale(1.02);
52
+ opacity: 0.9;
53
+ }
54
+ }
55
+
56
+ /* Animation Classes */
57
+ .sl-pulse {
58
+ animation: skeleton-pulse 1.5s ease-in-out infinite;
59
+ }
60
+
61
+ .sl-flow {
62
+ animation: skeleton-flow 2s ease-in-out infinite;
63
+ }
64
+
65
+ .sl-shine {
66
+ background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 76%);
67
+ background-size: 200% 100%;
68
+ animation: skeleton-shine 1.5s ease-in-out infinite;
69
+ }
70
+
71
+ .sl-gradient {
72
+ background: linear-gradient(-45deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
73
+ background-size: 400% 400%;
74
+ animation: skeleton-gradient 2s ease-in-out infinite;
75
+ }
76
+
77
+ .sl-neon {
78
+ animation: skeleton-neon 2s ease-in-out infinite;
79
+ }
80
+
81
+ .sl-breathing {
82
+ animation: skeleton-breathing 2s ease-in-out infinite;
83
+ }
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonLoader
4
+ # Handles async requests to generate and render loading skeletons
5
+ class SkeletonLoaderController < ActionController::Base
6
+ def show
7
+ content_id = params[:content_id]
8
+ mode = params[:mode]
9
+ wrapped_content = mode == "custom" ? handle_custom_template(content_id) : handle_predefined_template(content_id)
10
+ render html: wrapped_content
11
+ rescue StandardError => e
12
+ handle_error(e)
13
+ end
14
+
15
+ private
16
+
17
+ def handle_predefined_template(content_id)
18
+ options = process_params
19
+ SkeletonElementGenerator.generate(
20
+ content_id: content_id,
21
+ options: options,
22
+ context: :controller
23
+ )
24
+ end
25
+
26
+ def handle_custom_template(content_id)
27
+ markup = params[:markup]
28
+ SkeletonElementGenerator.generate(
29
+ content_id: content_id,
30
+ context: :controller
31
+ ) { markup }
32
+ end
33
+
34
+ def process_params
35
+ params_to_process = params.to_unsafe_h
36
+ .except("controller", "action", "format", "content_id", "mode", "markup")
37
+ .transform_keys(&:to_sym)
38
+
39
+ convert_numeric_params(params_to_process)
40
+ params_to_process
41
+ end
42
+
43
+ def convert_numeric_params(params)
44
+ %i[scale count width per_row].each do |key|
45
+ params[key] = params[key].to_f if key == :scale && params[key]
46
+ params[key] = params[key].to_i if %i[count width per_row].include?(key) && params[key]
47
+ end
48
+ end
49
+
50
+ def handle_error(error)
51
+ Rails.logger.error "SkeletonLoader Error: #{error.message}"
52
+ Rails.logger.error error.backtrace.join("\n")
53
+
54
+ render json: {
55
+ error: error.message,
56
+ backtrace: Rails.env.development? ? error.backtrace : []
57
+ }, status: :unprocessable_entity
58
+ end
59
+ end
60
+ end