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
@@ -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