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
@@ -0,0 +1,234 @@
1
+ export default class ClientSkeletonLoader {
2
+ static loadingStates = new Map();
3
+ static activeSkeletons = new Map();
4
+
5
+ constructor() {
6
+ this.defaultDisplayStyles = {};
7
+ this.skeletonClass = 'skeleton-loader--client';
8
+ this.contentRefAttr = 'data-content-id';
9
+ this.apiEndpoint = '/skeleton_loader/templates';
10
+ }
11
+
12
+ /**
13
+ * Creates or retrieves a skeleton instance for a content element
14
+ */
15
+ getSkeletonInstance(contentId) {
16
+ if (!ClientSkeletonLoader.activeSkeletons.has(contentId)) {
17
+ ClientSkeletonLoader.activeSkeletons.set(contentId, this.createSkeletonInstance(contentId));
18
+ }
19
+ return ClientSkeletonLoader.activeSkeletons.get(contentId);
20
+ }
21
+
22
+ /**
23
+ * Creates a new skeleton instance
24
+ */
25
+ createSkeletonInstance(contentId) {
26
+ return {
27
+ isLoading: () => ClientSkeletonLoader.loadingStates.get(contentId) || false,
28
+ reveal: () => {
29
+ const skeletonElement = document.querySelector(`[${this.contentRefAttr}="${contentId}"]`);
30
+ const contentElement = document.getElementById(contentId);
31
+ if (skeletonElement && contentElement) {
32
+ this.revealContent(skeletonElement, contentElement);
33
+ }
34
+ }
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Main render method that handles predefined skeletons
40
+ */
41
+ async render({ contentId, type = 'default', ...options } = {}) {
42
+ return this.processRender({
43
+ contentId,
44
+ mode: 'predefined',
45
+ type,
46
+ ...options
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Custom render method for user-provided markup
52
+ */
53
+ async renderCustom({ contentId, markup } = {}) {
54
+ if (!markup) return this.logError('Custom markup is required');
55
+
56
+ return this.processRender({
57
+ contentId,
58
+ mode: 'custom',
59
+ markup
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Core rendering logic shared between render methods
65
+ */
66
+ async processRender(params) {
67
+ const { contentId } = params;
68
+ if (!contentId) return this.logError('contentId is required');
69
+
70
+ const skeleton = this.getSkeletonInstance(contentId);
71
+ if (skeleton.isLoading()) {
72
+ console.warn(`Skeleton loading already in progress for ${contentId}`);
73
+ return skeleton;
74
+ }
75
+
76
+ try {
77
+ const content = await this.initializeLoading(contentId);
78
+ if (!content) return skeleton;
79
+
80
+ const skeletonElement = await this.createSkeletonElement(params);
81
+ if (skeletonElement) this.showSkeleton(skeletonElement, content);
82
+
83
+ return skeleton;
84
+ } catch (error) {
85
+ console.error('[SkeletonLoader] Render failed:', error);
86
+ return skeleton;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Initialize loading state and prepare content
92
+ */
93
+ async initializeLoading(contentId) {
94
+ ClientSkeletonLoader.loadingStates.set(contentId, true);
95
+ return this.hideContent(contentId);
96
+ }
97
+
98
+ /**
99
+ * Hides the content and stores its original display style
100
+ */
101
+ hideContent(contentId) {
102
+ const content = document.getElementById(contentId);
103
+ if (!content) {
104
+ console.error(`Content not found: #${contentId}`);
105
+ return null;
106
+ }
107
+ const displayStyle = getComputedStyle(content).display;
108
+
109
+ if (displayStyle !== 'none') {
110
+ this.defaultDisplayStyles[contentId] = displayStyle;
111
+ }
112
+ content.style.display = 'none';
113
+ return content;
114
+ }
115
+
116
+ /**
117
+ * Reveals the content and cleans up the skeleton
118
+ */
119
+ revealContent(skeleton, content) {
120
+ const contentId = content.id;
121
+ if (!ClientSkeletonLoader.loadingStates.get(contentId)) {
122
+ return;
123
+ }
124
+
125
+ skeleton.remove();
126
+ content.style.display = this.defaultDisplayStyles[contentId];
127
+ ClientSkeletonLoader.loadingStates.set(contentId, false);
128
+ }
129
+
130
+ /**
131
+ * Displays the skeleton by inserting it before the hidden content
132
+ */
133
+ showSkeleton(skeleton, content) {
134
+ this.clearPreviousSkeleton(content.id);
135
+ skeleton.setAttribute(this.contentRefAttr, content.id);
136
+ skeleton.style.display = 'block';
137
+ content.parentNode.insertBefore(skeleton, content);
138
+ }
139
+
140
+ /**
141
+ * Removes any existing skeleton associated with the content ID
142
+ */
143
+ clearPreviousSkeleton(contentId) {
144
+ const existingSkeletons = document.querySelectorAll(`[${this.contentRefAttr}="${contentId}"]`);
145
+ existingSkeletons.forEach(skeleton => skeleton.remove());
146
+ }
147
+
148
+ /**
149
+ * Creates a skeleton element by fetching the template
150
+ */
151
+ async createSkeletonElement({ contentId, mode, markup = null, type, ...options }) {
152
+ const params = this.buildParams({ contentId, mode, markup, type, ...options });
153
+ try {
154
+ const template = await this.fetchTemplate(params);
155
+ return this.parseTemplateToElement(template, contentId);
156
+ } catch (error) {
157
+ console.error('Error creating skeleton:', error);
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Fetches the skeleton template from the server
164
+ */
165
+ async fetchTemplate(params) {
166
+ const response = await fetch(`${this.apiEndpoint}?${params.toString()}`, {
167
+ method: 'GET',
168
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
169
+ });
170
+
171
+ if (!response.ok) {
172
+ throw new Error('Failed to fetch skeleton template');
173
+ }
174
+
175
+ return response.text();
176
+ }
177
+
178
+ /**
179
+ * Converts a template string into a DOM element
180
+ */
181
+ parseTemplateToElement(template, contentId) {
182
+ const parser = new DOMParser();
183
+ const doc = parser.parseFromString(template, 'text/html');
184
+ const skeletonElement = doc.body.firstElementChild;
185
+
186
+ if (!skeletonElement) {
187
+ console.error(`Invalid skeleton template for contentId: ${contentId}`);
188
+ return null;
189
+ }
190
+ return skeletonElement;
191
+ }
192
+
193
+ /**
194
+ * Builds query parameters for fetching the skeleton template
195
+ */
196
+ buildParams({ contentId, mode, markup, type, ...options }) {
197
+ const params = new URLSearchParams({
198
+ content_id: contentId,
199
+ mode,
200
+ type: type || 'default',
201
+ ...this.formatOptions(options)
202
+ });
203
+
204
+ if (markup) params.set('markup', markup);
205
+ return params;
206
+ }
207
+
208
+ /**
209
+ * Format options to snake_case for Rails
210
+ */
211
+ formatOptions(options) {
212
+ return Object.fromEntries(
213
+ Object.entries(options).map(([key, value]) => [
214
+ this.toSnakeCase(key),
215
+ value
216
+ ])
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Convert camelCase to snake_case
222
+ */
223
+ toSnakeCase(str) {
224
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase();
225
+ }
226
+
227
+ /**
228
+ * Logs an error message and returns null
229
+ */
230
+ logError(message) {
231
+ console.error(`[SkeletonLoader] ${message}`);
232
+ return null;
233
+ }
234
+ }
@@ -0,0 +1,113 @@
1
+ export class ServerSkeletonLoader {
2
+ constructor() {
3
+ this.SKELETON_CLASS = 'skeleton-loader--server';
4
+ this.CONTENT_ID_ATTR = 'data-content-id';
5
+ this.contentsDisplayStyles = {};
6
+ }
7
+
8
+ // Instead of class fields, move static properties inside the class
9
+ static get SKELETON_CLASS() {
10
+ return 'skeleton-loader--server';
11
+ }
12
+
13
+ static get CONTENT_ID_ATTR() {
14
+ return 'data-content-id';
15
+ }
16
+
17
+ // ============= PUBLIC API =============
18
+
19
+ // Start the loader and initialize event listeners
20
+ start() {
21
+ this.setupInitialLoading();
22
+ this.setupContentsSwap();
23
+ return this;
24
+ }
25
+
26
+ // ============= CORE PROCESSING =============
27
+
28
+ // Setup initial loading states for skeletons on DOM load
29
+ setupInitialLoading() {
30
+ document.addEventListener('DOMContentLoaded', () => {
31
+ this.captureContentsDisplayStyles();
32
+ this.hideContents();
33
+ this.showSkeletons();
34
+ });
35
+ }
36
+
37
+ // Swap skeletons with content on full page load
38
+ setupContentsSwap() {
39
+ window.addEventListener('load', async() => await this.revealContent());
40
+ }
41
+
42
+ // ============= CONTENT MANAGEMENT =============
43
+
44
+ // Store the original display styles of content elements for later restoration
45
+ captureContentsDisplayStyles() {
46
+ const skeletons = document.querySelectorAll(`.${ServerSkeletonLoader.SKELETON_CLASS}`);
47
+ skeletons.forEach(skeleton => {
48
+ const contentId = skeleton.getAttribute(ServerSkeletonLoader.CONTENT_ID_ATTR);
49
+ const content = document.getElementById(contentId);
50
+
51
+ if (content) {
52
+ const originalDisplay = getComputedStyle(content).display || 'block';
53
+ this.contentsDisplayStyles[contentId] = originalDisplay;
54
+ } else {
55
+ console.warn(`Content element with id "${contentId}" not found`);
56
+ }
57
+ });
58
+ }
59
+
60
+ // Show skeleton loading elements by setting their display property
61
+ showSkeletons() {
62
+ const skeletons = document.querySelectorAll(`.${ServerSkeletonLoader.SKELETON_CLASS}`);
63
+ skeletons.forEach(skeleton => {
64
+ skeleton.style.display = 'block';
65
+ });
66
+ }
67
+
68
+ // Reveal content by swapping skeletons with actual content elements
69
+ async revealContent() {
70
+ this.hideSkeletons();
71
+ this.showContents();
72
+ }
73
+
74
+ // Hide skeleton loading elements once content is ready to be shown
75
+ hideSkeletons() {
76
+ const skeletons = document.querySelectorAll(`.${ServerSkeletonLoader.SKELETON_CLASS}`);
77
+ skeletons.forEach(skeleton => {
78
+ skeleton.style.display = 'none';
79
+ });
80
+ }
81
+
82
+ // Display actual content elements by restoring their original display styles
83
+ showContents() {
84
+ const skeletons = document.querySelectorAll(`.${ServerSkeletonLoader.SKELETON_CLASS}`);
85
+ skeletons.forEach(skeleton => {
86
+ const contentId = skeleton.getAttribute(ServerSkeletonLoader.CONTENT_ID_ATTR);
87
+ const content = document.getElementById(contentId);
88
+
89
+ if (content) {
90
+ const originalDisplay = this.contentsDisplayStyles[contentId] || 'block';
91
+ content.style.display = originalDisplay;
92
+ content.style.visibility = 'visible'; // Ensure content is visible
93
+ } else {
94
+ console.warn(`Content element with id "${contentId}" not found`);
95
+ }
96
+ });
97
+ }
98
+
99
+ // Hide content elements initially to be replaced with skeleton loaders
100
+ hideContents() {
101
+ const skeletons = document.querySelectorAll(`.${ServerSkeletonLoader.SKELETON_CLASS}`);
102
+ skeletons.forEach(skeleton => {
103
+ const contentId = skeleton.getAttribute(ServerSkeletonLoader.CONTENT_ID_ATTR);
104
+ const content = document.getElementById(contentId);
105
+
106
+ if (content) {
107
+ content.style.display = 'none';
108
+ } else {
109
+ console.warn(`Content element with id "${contentId}" not found`);
110
+ }
111
+ });
112
+ }
113
+ }
@@ -0,0 +1,6 @@
1
+ import ClientSkeletonLoader from './client_skeleton_loader'
2
+ import { ServerSkeletonLoader } from './server_skeleton_loader'
3
+
4
+ new ServerSkeletonLoader().start()
5
+
6
+ export default ClientSkeletonLoader
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ SkeletonLoader::Engine.routes.draw do
4
+ get "templates", to: "skeleton_loader#show"
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonLoader
4
+ module Generators
5
+ # Copies default templates to app/views/skeleton_loader and
6
+ # creates the necessary initializer file.
7
+ class AddTemplatesGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Installs SkeletonLoader default templates"
10
+
11
+ def add_templates
12
+ directory ".", "app/views/skeleton_loader"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonLoader
4
+ module Generators
5
+ # Forces overwrite of any customized templates in app/views/skeleton_loader
6
+ # with the original default versions.
7
+ class ResetTemplatesGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Resets SkeletonLoader templates to default"
10
+
11
+ def reset_templates
12
+ directory ".", "app/views/skeleton_loader", force: true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
3
+ scaled_width = total_width * @scale
4
+ %>
5
+
6
+ <div id="f1" class="skeleton-card" style="padding: <%= 16 * @scale %>px;">
7
+ <div id="f2" style="
8
+ display: grid;
9
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
10
+ gap: <%= 16 * @scale %>px;
11
+ width: <%= scaled_width %>px;
12
+ ">
13
+ <% @count.to_i.times do %>
14
+ <div id="f3" style="width: 100%;">
15
+ <div id="f4" class="skeleton-loading <%= @animation_type %>" style="
16
+ height: <%= 240 * @scale %>px;
17
+ margin-bottom: <%= 16 * @scale %>px;
18
+ "></div>
19
+ <div id="f5" class="skeleton-loading <%= @animation_type %>" style="
20
+ height: <%= 20 * @scale %>px;
21
+ width: 50%;
22
+ margin-bottom: <%= 8 * @scale %>px;
23
+ "></div>
24
+ <div class="skeleton-loading <%= @animation_type %>" style="
25
+ height: <%= 20 * @scale %>px;
26
+ width: 30%;
27
+ "></div>
28
+ </div>
29
+ <% end %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,61 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
3
+ scaled_width = total_width * @scale
4
+ %>
5
+
6
+ <div class="skeleton-comments" style="padding: <%= 16 * @scale %>px;">
7
+ <div style="
8
+ display: grid;
9
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
10
+ gap: <%= 16 * @scale %>px;
11
+ width: <%= scaled_width %>px;
12
+ ">
13
+ <% @count.to_i.times do %>
14
+ <div class="skeleton-comment" style="width: 100%;">
15
+ <div style="
16
+ display: flex;
17
+ align-items: flex-start;
18
+ gap: <%= 16 * @scale %>px;
19
+ ">
20
+ <!-- Avatar circle -->
21
+ <div class="skeleton-loading <%= @animation_type %>" style="
22
+ border-radius: 50%;
23
+ height: <%= 48 * @scale %>px;
24
+ width: <%= 48 * @scale %>px;
25
+ flex-shrink: 0;
26
+ "></div>
27
+
28
+ <!-- Content area -->
29
+ <div style="flex: 1;">
30
+ <!-- Username -->
31
+ <div class="skeleton-loading <%= @animation_type %>" style="
32
+ height: <%= 16 * @scale %>px;
33
+ width: 25%;
34
+ margin-bottom: <%= 8 * @scale %>px;
35
+ "></div>
36
+
37
+ <!-- Comment text lines -->
38
+ <div style="
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: <%= 8 * @scale %>px;
42
+ ">
43
+ <div class="skeleton-loading <%= @animation_type %>" style="
44
+ height: <%= 16 * @scale %>px;
45
+ width: 100%;
46
+ "></div>
47
+ <div class="skeleton-loading <%= @animation_type %>" style="
48
+ height: <%= 16 * @scale %>px;
49
+ width: 83%;
50
+ "></div>
51
+ <div class="skeleton-loading <%= @animation_type %>" style="
52
+ height: <%= 16 * @scale %>px;
53
+ width: 67%;
54
+ "></div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+ </div>
61
+ </div>
@@ -0,0 +1,23 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
3
+ scaled_width = total_width * @scale
4
+ %>
5
+
6
+ <div class="skeleton-default" style="
7
+ padding: <%= 16 * @scale %>px;
8
+ width: <%= scaled_width %>px;
9
+ ">
10
+ <div style="
11
+ display: grid;
12
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
13
+ gap: <%= 16 * @scale %>px;
14
+ ">
15
+ <% @count.to_i.times do %>
16
+ <div style="display: flex; flex-direction: column; gap: <%= 10 * @scale %>px;">
17
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 26 * @scale %>px; width: 75%;"></div>
18
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 128 * @scale %>px; width: 100%;"></div>
19
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 16 * @scale %>px; width: 50%;"></div>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ </div>
@@ -0,0 +1,19 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
3
+ scaled_width = total_width * @scale
4
+ %>
5
+
6
+ <div class="skeleton-gallery" style="
7
+ padding: <%= 16 * @scale %>px;
8
+ width: <%= scaled_width %>px;
9
+ ">
10
+ <div style="
11
+ display: grid;
12
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
13
+ gap: <%= 16 * @scale %>px;
14
+ ">
15
+ <% @count.to_i.times do %>
16
+ <div class="skeleton-loading <%= @animation_type %>" style="padding-bottom: 100%;"></div>
17
+ <% end %>
18
+ </div>
19
+ </div>
@@ -0,0 +1,28 @@
1
+ <%
2
+ Rails.logger.debug("FFFFFFF")
3
+ Rails.logger.debug(@width)
4
+ Rails.logger.debug(@per_row)
5
+ Rails.logger.debug(@count)
6
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
7
+ scaled_width = total_width * @scale
8
+ %>
9
+
10
+ <div class="skeleton-paragraph" style="display:inline-block; padding: <%= 16 * @scale %>px;">
11
+ <div style="
12
+ display: grid;
13
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
14
+ gap: <%= 24 * @scale %>px;
15
+ width: <%= scaled_width %>px;
16
+ ">
17
+ <% @count.to_i.times do %>
18
+ <div style="display: flex; flex-direction: column; gap: <%= 12 * @scale %>px;">
19
+ <% 4.times do |i| %>
20
+ <div class="skeleton-loading <%= @animation_type %>" style="
21
+ height: <%= 20 * @scale %>px;
22
+ width: <%= [100, 83, 67, 75][i % 4] %>%;
23
+ "></div>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,49 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (24 * (@per_row - 1))
3
+
4
+ # Apply container scale
5
+ scaled_width = total_width * @scale
6
+ %>
7
+
8
+ <div class="skeleton-product" style="
9
+ width: <%= scaled_width %>px;
10
+ padding: <%= 16 * @scale %>px;
11
+ ">
12
+ <div style="
13
+ display: grid;
14
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
15
+ gap: <%= 16 * @scale %>px;
16
+ ">
17
+ <% @count.to_i.times do %>
18
+ <div style="display: flex; flex-direction: column;">
19
+ <!-- Main image area -->
20
+ <div class="skeleton-loading <%= @animation_type %>" style="
21
+ height: <%= 256 * @scale %>px;
22
+ width: 100%;
23
+ margin-bottom: <%= 16 * @scale %>px;
24
+ "></div>
25
+
26
+ <!-- Content area -->
27
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: <%= 12 * @scale %>px;">
28
+ <div style="width: 100%; display: flex; flex-direction: column; gap: <%= 12 * @scale %>px;">
29
+ <div class="skeleton-loading <%= @animation_type %>" style="
30
+ height: <%= 20 * @scale %>px;
31
+ width: 50%;
32
+ "></div>
33
+
34
+ <div class="skeleton-loading <%= @animation_type %>" style="
35
+ height: <%= 14 * @scale %>px;
36
+ width: 66%;
37
+ "></div>
38
+ </div>
39
+
40
+ <!-- Price area -->
41
+ <div class="skeleton-loading <%= @animation_type %>" style="
42
+ height: <%= 24 * @scale %>px;
43
+ width: 25%;
44
+ "></div>
45
+ </div>
46
+ </div>
47
+ <% end %>
48
+ </div>
49
+ </div>
@@ -0,0 +1,28 @@
1
+ <%
2
+ total_width = (@width * @per_row) + (16 * (@per_row - 1))
3
+ scaled_width = total_width * @scale
4
+ %>
5
+
6
+ <div class="skeleton-user-profile" style="padding: <%= 16 * @scale %>px;">
7
+ <div style="
8
+ display: grid;
9
+ grid-template-columns: repeat(<%= @per_row %>, <%= @width * @scale %>px);
10
+ gap: <%= 16 * @scale %>px;
11
+ width: <%= scaled_width %>px;
12
+ ">
13
+ <% @count.to_i.times do %>
14
+ <div style="display: flex; gap: <%= 16 * @scale %>px;">
15
+ <div class="skeleton-loading <%= @animation_type %>" style="
16
+ width: <%= 128 * @scale %>px;
17
+ height: <%= 128 * @scale %>px;
18
+ flex-shrink: 0;
19
+ "></div>
20
+ <div style="flex-grow: 1;">
21
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 16 * @scale %>px; width: 75%; margin-bottom: <%= 12 * @scale %>px;"></div>
22
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 16 * @scale %>px; width: 50%; margin-bottom: <%= 12 * @scale %>px;"></div>
23
+ <div class="skeleton-loading <%= @animation_type %>" style="height: <%= 16 * @scale %>px; width: 25%;"></div>
24
+ </div>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,7 @@
1
+ # rubocop:disable Naming/FileName
2
+ # frozen_string_literal: true
3
+
4
+ # Bridge file, redirects to `skeleton_loader.rb`
5
+ require_relative "skeleton_loader"
6
+
7
+ # rubocop:enable Naming/FileName