skeleton-loader 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +401 -0
- data/app/assets/javascripts/skeleton_loader.js +2 -0
- data/app/assets/javascripts/skeleton_loader.js.LICENSE.txt +1 -0
- data/app/assets/stylesheets/skeleton_loader.css +83 -0
- data/app/controllers/skeleton_loader/skeleton_loader_controller.rb +60 -0
- data/app/javascript/client_skeleton_loader.js +234 -0
- data/app/javascript/server_skeleton_loader.js +113 -0
- data/app/javascript/skeleton_loader.js +6 -0
- data/config/routes.rb +5 -0
- data/lib/generators/skeleton_loader/add_templates_generator.rb +16 -0
- data/lib/generators/skeleton_loader/reset_templates_generator.rb +16 -0
- data/lib/generators/skeleton_loader/templates/_card.html.erb +31 -0
- data/lib/generators/skeleton_loader/templates/_comment.html.erb +61 -0
- data/lib/generators/skeleton_loader/templates/_default.html.erb +23 -0
- data/lib/generators/skeleton_loader/templates/_gallery.html.erb +19 -0
- data/lib/generators/skeleton_loader/templates/_paragraph.html.erb +28 -0
- data/lib/generators/skeleton_loader/templates/_product.html.erb +49 -0
- data/lib/generators/skeleton_loader/templates/_profile.html.erb +28 -0
- data/lib/skeleton-loader.rb +7 -0
- data/lib/skeleton_loader/configuration.rb +68 -0
- data/lib/skeleton_loader/engine.rb +42 -0
- data/lib/skeleton_loader/skeleton_element_generator.rb +49 -0
- data/lib/skeleton_loader/template_path_finder.rb +24 -0
- data/lib/skeleton_loader/template_renderer.rb +41 -0
- data/lib/skeleton_loader/version.rb +5 -0
- data/lib/skeleton_loader/view_helpers.rb +19 -0
- data/lib/skeleton_loader.rb +36 -0
- data/lib/tasks/skeleton_loader_tasks.rake +23 -0
- 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
|
+
}
|
data/config/routes.rb
ADDED
@@ -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>
|