skeleton-loader 0.1.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.
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