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