ultimate-jekyll-manager 1.4.1 → 1.4.3
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.
- package/CHANGELOG.md +22 -0
- package/dist/assets/js/core/auth.js +24 -39
- package/dist/defaults/dist/_alternatives/example-competitor.md +6 -6
- package/dist/defaults/dist/_includes/admin/sections/sidebar.json +2 -2
- package/dist/defaults/dist/_includes/themes/classy/backend/sections/topbar.html +1 -1
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +7 -4
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +13 -13
- package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +1 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +1 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +5 -5
- package/dist/defaults/dist/_layouts/blueprint/auth/oauth2.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/backend/pages/dashboard/index.html +12 -12
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html +5 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/index.html +2 -2
- package/dist/defaults/dist/_updates/v0.0.1.md +3 -0
- package/dist/defaults/dist/pages/test/account/dashboard.html +1 -1
- package/dist/defaults/dist/pages/test/libraries/ads.html +9 -9
- package/dist/defaults/dist/pages/test/libraries/bootstrap.html +6 -6
- package/dist/defaults/dist/pages/test/libraries/firestore.html +1 -1
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +2 -2
- package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +8 -8
- package/dist/defaults/dist/sitemap.html +2 -2
- package/dist/gulp/tasks/imagemin.js +36 -7
- package/dist/utils/attach-log-file.js +24 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
---
|
|
18
|
+
## [1.4.3] - 2026-05-28
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Imagemin: uppercase-extension images (e.g. `IMG_3119.JPG`) now build end to end.** v1.4.2 made the glob case-insensitive so the file was discovered, but `gulp-responsive-modern`'s `lib/format.js` does a case-sensitive `switch` on `path.extname()` and returns the string `'unsupported'` for `.JPG`, which then crashes `sharp.toFormat()`. [src/gulp/tasks/imagemin.js](src/gulp/tasks/imagemin.js) now pipes each file through an in-stream `Transform` that lowercases the extension on the Vinyl path before the responsive plugin sees it (the on-disk source is left untouched).
|
|
23
|
+
- **Log files no longer truncate before the crash that caused them.** [src/utils/attach-log-file.js](src/utils/attach-log-file.js) switched from `fs.createWriteStream` (async-buffered) to synchronous `fs.writeSync` against an open fd. The buffered stream dropped its tail when a gulp task threw and the process exited — so the lines describing the failure never reached `logs/build.log`. Synchronous writes guarantee the full error + stack survive an immediate exit.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Auth: signup-consent gating now keys off the user doc's `flags.signupProcessed` instead of a time window.** [src/assets/js/core/auth.js](src/assets/js/core/auth.js) drops the `SIGNUP_MAX_AGE` (5-minute) heuristic and the client-only `localStorage` flag. `sendUserSignupMetadata` fires whenever the doc shows signup unprocessed (the server is idempotent), and the consent guard only signs a user out once signup has actually been processed — removing the risk of locking users out on a transient metadata-send failure.
|
|
28
|
+
- **Footer language dropdown always renders.** No longer gated on `site.translation.enabled`; falls back to `site.translation.default` (or `"en"`) when no extra languages are configured. [src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html](src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html)
|
|
29
|
+
- **Sentence-case copy normalization** across default pages (pricing, alternatives, admin/test pages, sitemap section labels): "API access", "Flash sale", "Root pages", "…and more:" etc.
|
|
30
|
+
- **Updates feed:** the `v0.0.1` sample entry is marked `draft: true` so it's hidden from the listing and sitemap (dev-only).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
## [1.4.2] - 2026-05-27
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Imagemin: uppercase image extensions (e.g. `IMG_3119.JPG`) no longer break the responsive build.** `gulp-responsive-modern` uses micromatch internally, which is strictly case-sensitive regardless of filesystem. On macOS APFS, gulp's `src()` would discover the file and count it toward expected outputs, but the lowercase-only `**/*.{jpg,jpeg,png}` pattern wouldn't match — producing zero outputs and erroring with "Available images do not match the following config". [src/gulp/tasks/imagemin.js](src/gulp/tasks/imagemin.js) now expands `ALL_IMAGE_GLOB` and `RESPONSIVE_GLOB` to include uppercase variants so consumers don't need to rename camera/phone files.
|
|
38
|
+
|
|
17
39
|
---
|
|
18
40
|
## [1.4.1] - 2026-05-27
|
|
19
41
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
2
2
|
import webManager from 'web-manager';
|
|
3
3
|
|
|
4
|
-
// Constants
|
|
5
|
-
const SIGNUP_MAX_AGE = 5 * 60 * 1000;
|
|
6
|
-
|
|
7
4
|
// Enforce page-load consent guard. When true, any authenticated user whose doc has
|
|
8
5
|
// consent.legal.status !== 'granted' is silently signed out. Keep FALSE until the
|
|
9
6
|
// legacy user migration runs (sets all existing docs to status='granted',
|
|
@@ -81,7 +78,7 @@ export default function () {
|
|
|
81
78
|
// by the on-create auth event). sendUserSignupMetadata is what flips it
|
|
82
79
|
// to 'granted' with the captured consent payload. If we gate first, every
|
|
83
80
|
// fresh signup would be signed out before consent ever lands.
|
|
84
|
-
await sendUserSignupMetadata(
|
|
81
|
+
await sendUserSignupMetadata(state.account);
|
|
85
82
|
|
|
86
83
|
// Consent guard: if the user is authenticated but their account doc shows
|
|
87
84
|
// no legal consent on record, they're an orphan from a reversed Google signup
|
|
@@ -89,16 +86,15 @@ export default function () {
|
|
|
89
86
|
// user knows what happened.
|
|
90
87
|
//
|
|
91
88
|
// Gated by ENFORCE_CONSENT_GUARD (off until the legacy-user migration runs).
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// (
|
|
95
|
-
// out
|
|
96
|
-
//
|
|
89
|
+
// Only fires once signup has been processed — sendUserSignupMetadata above is what
|
|
90
|
+
// writes consent, and it runs whenever flags.signupProcessed is false. If signup
|
|
91
|
+
// hasn't been processed yet (or just failed and will retry next load), we must NOT
|
|
92
|
+
// sign the user out; a processed doc with no legal consent is a genuine orphan
|
|
93
|
+
// (e.g. a reversed Google signup that failed to delete cleanly).
|
|
97
94
|
if (ENFORCE_CONSENT_GUARD) {
|
|
98
|
-
const
|
|
99
|
-
const isFreshAccount = accountAge < SIGNUP_MAX_AGE;
|
|
95
|
+
const signupProcessed = state.account?.flags?.signupProcessed === true;
|
|
100
96
|
const legalStatus = state.account?.consent?.legal?.status;
|
|
101
|
-
if (
|
|
97
|
+
if (signupProcessed && legalStatus && legalStatus !== 'granted') {
|
|
102
98
|
console.warn('[Auth] Signing out user with no legal consent on record');
|
|
103
99
|
await webManager.auth().signOut();
|
|
104
100
|
webManager.utilities().showNotification(
|
|
@@ -260,7 +256,7 @@ function setAnalyticsUserId(user) {
|
|
|
260
256
|
}
|
|
261
257
|
|
|
262
258
|
// Send user metadata to server (affiliate, UTM params, etc.)
|
|
263
|
-
async function sendUserSignupMetadata(
|
|
259
|
+
async function sendUserSignupMetadata(account) {
|
|
264
260
|
try {
|
|
265
261
|
// Skip on auth pages to avoid blocking redirect (metadata will be sent on destination page)
|
|
266
262
|
const pagePath = document.documentElement.getAttribute('data-page-path');
|
|
@@ -269,20 +265,17 @@ async function sendUserSignupMetadata(user) {
|
|
|
269
265
|
return;
|
|
270
266
|
}
|
|
271
267
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
// The user doc's flags.signupProcessed is the single source of truth. We have the full
|
|
269
|
+
// account doc on every page load, so gate on it directly — no account-age window, no
|
|
270
|
+
// client-only localStorage flag. Fire whenever the doc shows signup is unprocessed; the
|
|
271
|
+
// server is idempotent and rejects if it was already processed.
|
|
272
|
+
const signupProcessed = account?.flags?.signupProcessed === true;
|
|
275
273
|
|
|
276
274
|
/* @dev-only:start */
|
|
277
|
-
|
|
278
|
-
// Log account age for debugging
|
|
279
|
-
const ageInMinutes = Math.floor(accountAge / 1000 / 60);
|
|
280
|
-
console.log('[Auth] Account age:', ageInMinutes, 'minutes, signupProcessed:', signupProcessed);
|
|
281
|
-
}
|
|
275
|
+
console.log('[Auth] signupProcessed:', signupProcessed);
|
|
282
276
|
/* @dev-only:end */
|
|
283
277
|
|
|
284
|
-
|
|
285
|
-
if (accountAge >= SIGNUP_MAX_AGE || signupProcessed) {
|
|
278
|
+
if (signupProcessed) {
|
|
286
279
|
return;
|
|
287
280
|
}
|
|
288
281
|
|
|
@@ -312,27 +305,19 @@ async function sendUserSignupMetadata(user) {
|
|
|
312
305
|
body: payload,
|
|
313
306
|
});
|
|
314
307
|
|
|
315
|
-
// Log
|
|
308
|
+
// Log — the server set flags.signupProcessed on the doc, so the next page load's
|
|
309
|
+
// state.account reflects it and this won't fire again. No client-side flag needed.
|
|
316
310
|
console.log('[Auth] User metadata sent successfully:', response);
|
|
317
|
-
|
|
318
|
-
// Mark signup as sent for this user (keep the attribution data for reference)
|
|
319
|
-
webManager.storage().set('flags.signupProcessed', user.uid);
|
|
320
311
|
} catch (error) {
|
|
321
312
|
console.error('[Auth] Error sending user metadata:', error);
|
|
322
|
-
// Don't throw - we don't want to block the signup flow
|
|
313
|
+
// Don't throw - we don't want to block the signup flow. The doc still shows
|
|
314
|
+
// signupProcessed=false, so a refresh / next page load retries automatically.
|
|
323
315
|
|
|
324
316
|
/* @dev-only:start */
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const minutes = Math.floor(msRemaining / 1000 / 60);
|
|
330
|
-
const seconds = Math.floor((msRemaining / 1000) % 60);
|
|
331
|
-
webManager.utilities().showNotification(
|
|
332
|
-
`[DEV] Failed to send signup metadata. User will be signed out by consent guard at ${signoutAt} (in ${minutes}m ${seconds}s) unless retried.`,
|
|
333
|
-
{ type: 'warning', timeout: 0 }
|
|
334
|
-
);
|
|
335
|
-
}
|
|
317
|
+
webManager.utilities().showNotification(
|
|
318
|
+
`[DEV] Failed to send signup metadata. Will retry on next page load (flags.signupProcessed is still false).`,
|
|
319
|
+
{ type: 'warning', timeout: 0 }
|
|
320
|
+
);
|
|
336
321
|
/* @dev-only:end */
|
|
337
322
|
}
|
|
338
323
|
}
|
|
@@ -18,25 +18,25 @@ alternative:
|
|
|
18
18
|
|
|
19
19
|
comparison:
|
|
20
20
|
features:
|
|
21
|
-
- name: "Free
|
|
21
|
+
- name: "Free plan"
|
|
22
22
|
icon: "gift"
|
|
23
23
|
ours:
|
|
24
24
|
value: true
|
|
25
25
|
theirs:
|
|
26
26
|
value: true
|
|
27
|
-
- name: "AI-
|
|
27
|
+
- name: "AI-powered features"
|
|
28
28
|
icon: "sparkles"
|
|
29
29
|
ours:
|
|
30
30
|
value: "Advanced"
|
|
31
31
|
theirs:
|
|
32
32
|
value: "Basic"
|
|
33
|
-
- name: "Real-time
|
|
33
|
+
- name: "Real-time collaboration"
|
|
34
34
|
icon: "users"
|
|
35
35
|
ours:
|
|
36
36
|
value: true
|
|
37
37
|
theirs:
|
|
38
38
|
value: false
|
|
39
|
-
- name: "API
|
|
39
|
+
- name: "API access"
|
|
40
40
|
icon: "code"
|
|
41
41
|
ours:
|
|
42
42
|
value: "Full REST API"
|
|
@@ -60,13 +60,13 @@ alternative:
|
|
|
60
60
|
value: "200+"
|
|
61
61
|
theirs:
|
|
62
62
|
value: "50+"
|
|
63
|
-
- name: "Mobile
|
|
63
|
+
- name: "Mobile app"
|
|
64
64
|
icon: "mobile"
|
|
65
65
|
ours:
|
|
66
66
|
value: true
|
|
67
67
|
theirs:
|
|
68
68
|
value: true
|
|
69
|
-
- name: "Export
|
|
69
|
+
- name: "Export formats"
|
|
70
70
|
icon: "download"
|
|
71
71
|
ours:
|
|
72
72
|
value: "PDF, CSV, JSON"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
icon: 'users',
|
|
20
20
|
dropdown: [
|
|
21
21
|
{
|
|
22
|
-
label: 'All
|
|
22
|
+
label: 'All users',
|
|
23
23
|
href: '/admin/users',
|
|
24
24
|
icon: 'list'
|
|
25
25
|
},
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
header: true,
|
|
49
|
-
label: 'External
|
|
49
|
+
label: 'External tools'
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
label: 'Stackblitz',
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
{% capture action_attributes %}{% if action.attributes %}{% for attr in action.attributes %} {{ attr[0] }}="{{ attr[1] }}"{% endfor %}{% endif %}{% endcapture %}
|
|
37
37
|
<div class="dropdown">
|
|
38
38
|
<button class="btn btn-link position-relative p-2" type="button" data-bs-toggle="dropdown" {{ action_attributes }} aria-expanded="false">
|
|
39
|
-
{% uj_icon action.icon
|
|
39
|
+
{% uj_icon action.icon, "fa-lg text-body" %}
|
|
40
40
|
{% if action.badge %}
|
|
41
41
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill {{ action.badge.class | default: 'bg-danger' }}">
|
|
42
42
|
{{ action.badge.text }}
|
|
@@ -73,8 +73,7 @@
|
|
|
73
73
|
<div class="row mt-4 pt-3 border-top">
|
|
74
74
|
<div class="col-md-6 d-flex align-items-center">
|
|
75
75
|
<!-- Language Dropdown -->
|
|
76
|
-
|
|
77
|
-
<div class="d-inline-block me-3">
|
|
76
|
+
<div class="d-inline-block me-3">
|
|
78
77
|
<div class="dropup uj-language-dropdown">
|
|
79
78
|
<button class="btn btn-sm btn-outline-adaptive dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
80
79
|
<i class="fa fa-sm me-1">
|
|
@@ -85,7 +84,12 @@
|
|
|
85
84
|
</span>
|
|
86
85
|
</button>
|
|
87
86
|
<ul class="dropdown-menu">
|
|
88
|
-
{% assign
|
|
87
|
+
{% assign default_language = site.translation.default | default: "en" %}
|
|
88
|
+
{% if site.translation.languages.size > 0 %}
|
|
89
|
+
{% assign all_languages = site.translation.languages | push: default_language %}
|
|
90
|
+
{% else %}
|
|
91
|
+
{% assign all_languages = "" | split: "" | push: default_language %}
|
|
92
|
+
{% endif %}
|
|
89
93
|
{% for language in all_languages %}
|
|
90
94
|
<li>
|
|
91
95
|
{% capture lang_name %}{% uj_language language, "native" %}{% endcapture %}
|
|
@@ -98,7 +102,6 @@
|
|
|
98
102
|
</ul>
|
|
99
103
|
</div>
|
|
100
104
|
</div>
|
|
101
|
-
{% endif %}
|
|
102
105
|
|
|
103
106
|
<!-- Social Links -->
|
|
104
107
|
{% if data.socials.enabled %}
|
|
@@ -6,7 +6,7 @@ layout: themes/classy/admin/core/minimal-viewport-locked
|
|
|
6
6
|
theme:
|
|
7
7
|
header:
|
|
8
8
|
title:
|
|
9
|
-
content: "Marketing
|
|
9
|
+
content: "Marketing calendar"
|
|
10
10
|
icon: "calendar-days"
|
|
11
11
|
breadcrumbs:
|
|
12
12
|
items:
|
|
@@ -16,7 +16,7 @@ theme:
|
|
|
16
16
|
- label: "Calendar"
|
|
17
17
|
actions:
|
|
18
18
|
items:
|
|
19
|
-
- label: "Create
|
|
19
|
+
- label: "Create campaign"
|
|
20
20
|
icon: "plus"
|
|
21
21
|
color: "primary"
|
|
22
22
|
attributes:
|
|
@@ -24,7 +24,7 @@ theme:
|
|
|
24
24
|
|
|
25
25
|
### REGULAR PAGES ###
|
|
26
26
|
meta:
|
|
27
|
-
title: "Marketing
|
|
27
|
+
title: "Marketing calendar - Admin"
|
|
28
28
|
description: "Schedule and manage marketing campaigns — emails and push notifications"
|
|
29
29
|
breadcrumb: "Calendar"
|
|
30
30
|
|
|
@@ -88,7 +88,7 @@ prerender_icons:
|
|
|
88
88
|
<!-- Campaign Type -->
|
|
89
89
|
<h6 class="mb-3">
|
|
90
90
|
{% uj_icon "bullhorn", "fa-sm me-2 text-info" %}
|
|
91
|
-
Campaign
|
|
91
|
+
Campaign type
|
|
92
92
|
</h6>
|
|
93
93
|
<div class="row mb-4">
|
|
94
94
|
<div class="col-6">
|
|
@@ -102,7 +102,7 @@ prerender_icons:
|
|
|
102
102
|
<input type="radio" class="btn-check" name="campaign.type" id="campaign-type-push" value="push" autocomplete="off">
|
|
103
103
|
<label class="btn btn-outline-adaptive w-100 d-flex align-items-center justify-content-center" for="campaign-type-push">
|
|
104
104
|
{% uj_icon "bell", "fa-sm me-2" %}
|
|
105
|
-
Push
|
|
105
|
+
Push notification
|
|
106
106
|
</label>
|
|
107
107
|
</div>
|
|
108
108
|
</div>
|
|
@@ -126,7 +126,7 @@ prerender_icons:
|
|
|
126
126
|
<div class="tab-pane fade show active" id="panel-edit" role="tabpanel">
|
|
127
127
|
<div class="mb-3">
|
|
128
128
|
<label for="campaign-name" class="form-label">
|
|
129
|
-
Campaign
|
|
129
|
+
Campaign name <span class="text-danger">*</span>
|
|
130
130
|
</label>
|
|
131
131
|
<input type="text"
|
|
132
132
|
class="form-control"
|
|
@@ -221,7 +221,7 @@ prerender_icons:
|
|
|
221
221
|
|
|
222
222
|
<div class="mb-3">
|
|
223
223
|
<label for="campaign-click-action" class="form-label">
|
|
224
|
-
Click
|
|
224
|
+
Click action URL
|
|
225
225
|
</label>
|
|
226
226
|
<input type="url"
|
|
227
227
|
class="form-control"
|
|
@@ -236,7 +236,7 @@ prerender_icons:
|
|
|
236
236
|
<div class="mb-3">
|
|
237
237
|
<label for="campaign-push-tags" class="form-label">
|
|
238
238
|
{% uj_icon "tags", "fa-sm me-1 text-info" %}
|
|
239
|
-
Filter by
|
|
239
|
+
Filter by tags
|
|
240
240
|
<small class="text-muted fw-normal ms-1">(comma-separated)</small>
|
|
241
241
|
</label>
|
|
242
242
|
<input type="text"
|
|
@@ -274,7 +274,7 @@ prerender_icons:
|
|
|
274
274
|
<!-- Discount Code -->
|
|
275
275
|
<div class="mb-3">
|
|
276
276
|
<label for="campaign-discount-code" class="form-label">
|
|
277
|
-
Discount
|
|
277
|
+
Discount code
|
|
278
278
|
<small class="text-muted fw-normal ms-1">(optional)</small>
|
|
279
279
|
</label>
|
|
280
280
|
<input type="text"
|
|
@@ -332,7 +332,7 @@ prerender_icons:
|
|
|
332
332
|
<div class="col-md-3 d-flex align-items-end">
|
|
333
333
|
<button type="button" class="btn btn-sm btn-outline-primary w-100" id="btn-send-now">
|
|
334
334
|
{% uj_icon "paper-plane", "fa-sm me-1" %}
|
|
335
|
-
Send
|
|
335
|
+
Send now
|
|
336
336
|
</button>
|
|
337
337
|
</div>
|
|
338
338
|
</div>
|
|
@@ -484,7 +484,7 @@ prerender_icons:
|
|
|
484
484
|
|
|
485
485
|
<div class="mb-3">
|
|
486
486
|
<label for="campaign-exclude-segments" class="form-label">
|
|
487
|
-
Exclude
|
|
487
|
+
Exclude segments
|
|
488
488
|
<small class="text-muted fw-normal ms-1">(comma-separated IDs)</small>
|
|
489
489
|
</label>
|
|
490
490
|
<input type="text"
|
|
@@ -498,7 +498,7 @@ prerender_icons:
|
|
|
498
498
|
<h6 class="mb-3 mt-4">
|
|
499
499
|
<a class="text-decoration-none" data-bs-toggle="collapse" href="#advanced-settings" role="button" aria-expanded="false">
|
|
500
500
|
{% uj_icon "sliders", "fa-sm me-2 text-info" %}
|
|
501
|
-
Advanced
|
|
501
|
+
Advanced settings
|
|
502
502
|
<small class="text-muted fw-normal ms-1">(optional)</small>
|
|
503
503
|
</a>
|
|
504
504
|
</h6>
|
|
@@ -527,7 +527,7 @@ prerender_icons:
|
|
|
527
527
|
</div>
|
|
528
528
|
|
|
529
529
|
<div class="mb-3">
|
|
530
|
-
<label class="form-label">UTM
|
|
530
|
+
<label class="form-label">UTM overrides</label>
|
|
531
531
|
<div class="row g-2" id="utm-fields">
|
|
532
532
|
<div class="col-md-6">
|
|
533
533
|
<input type="text" class="form-control form-control-sm" name="campaign.utm.utm_source" placeholder="utm_source">
|
|
@@ -6,7 +6,7 @@ layout: themes/classy/admin/core/minimal
|
|
|
6
6
|
theme:
|
|
7
7
|
header:
|
|
8
8
|
title:
|
|
9
|
-
content: "Create
|
|
9
|
+
content: "Create user"
|
|
10
10
|
icon: "user-plus"
|
|
11
11
|
breadcrumbs:
|
|
12
12
|
items:
|
|
@@ -14,20 +14,20 @@ theme:
|
|
|
14
14
|
href: "/admin"
|
|
15
15
|
- label: "Users"
|
|
16
16
|
href: "/admin/users"
|
|
17
|
-
- label: "Create
|
|
17
|
+
- label: "Create user"
|
|
18
18
|
|
|
19
19
|
### REGULAR PAGES ###
|
|
20
20
|
meta:
|
|
21
21
|
title: "Create User - Admin"
|
|
22
22
|
description: "Create a new user account"
|
|
23
|
-
breadcrumb: "Create
|
|
23
|
+
breadcrumb: "Create user"
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
<!-- Back Link -->
|
|
27
27
|
<div class="mb-3">
|
|
28
28
|
<a href="/admin/users" class="text-decoration-none">
|
|
29
29
|
{% uj_icon "arrow-left", "fa-sm me-2" %}
|
|
30
|
-
Back to
|
|
30
|
+
Back to users
|
|
31
31
|
</a>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
@@ -51,7 +51,7 @@ meta:
|
|
|
51
51
|
<div class="form-text">Must be at least 6 characters</div>
|
|
52
52
|
</div>
|
|
53
53
|
<div class="col-md-6 mb-3">
|
|
54
|
-
<label for="confirmPassword" class="form-label">Confirm
|
|
54
|
+
<label for="confirmPassword" class="form-label">Confirm password <span class="text-danger">*</span></label>
|
|
55
55
|
<input type="password" class="form-control" id="confirmPassword" name="user.confirmPassword" required minlength="6">
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
@@ -6,7 +6,7 @@ layout: themes/[ site.theme.id ]/frontend/pages/auth/oauth2
|
|
|
6
6
|
meta:
|
|
7
7
|
title: "OAuth2 Authentication - {{ site.brand.name }}"
|
|
8
8
|
description: "Connect your account using OAuth2 for secure authentication."
|
|
9
|
-
breadcrumb: "OAuth2
|
|
9
|
+
breadcrumb: "OAuth2 authentication"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
{{ content | uj_content_format }}
|
|
@@ -16,21 +16,21 @@ theme:
|
|
|
16
16
|
- label: Dashboard
|
|
17
17
|
actions:
|
|
18
18
|
items:
|
|
19
|
-
- label: New
|
|
19
|
+
- label: New project
|
|
20
20
|
icon: plus
|
|
21
21
|
color: primary
|
|
22
22
|
- label: Reports
|
|
23
23
|
icon: chart-line
|
|
24
24
|
color: adaptive
|
|
25
25
|
dropdown:
|
|
26
|
-
- label: Sales
|
|
26
|
+
- label: Sales report
|
|
27
27
|
icon: dollar-sign
|
|
28
28
|
href: /reports/sales
|
|
29
|
-
- label: Analytics
|
|
29
|
+
- label: Analytics report
|
|
30
30
|
icon: chart-bar
|
|
31
31
|
href: /reports/analytics
|
|
32
32
|
- divider: true
|
|
33
|
-
- label: Export
|
|
33
|
+
- label: Export all
|
|
34
34
|
icon: download
|
|
35
35
|
href: /reports/export
|
|
36
36
|
|
|
@@ -43,21 +43,21 @@ meta:
|
|
|
43
43
|
### PAGE CONFIG ###
|
|
44
44
|
stats_cards:
|
|
45
45
|
- id: "total_revenue"
|
|
46
|
-
title: "Total
|
|
46
|
+
title: "Total revenue"
|
|
47
47
|
value: "$24,300"
|
|
48
48
|
change: "+12.5%"
|
|
49
49
|
change_type: "success"
|
|
50
50
|
icon: "dollar-sign"
|
|
51
51
|
|
|
52
52
|
- id: "new_users"
|
|
53
|
-
title: "New
|
|
53
|
+
title: "New users"
|
|
54
54
|
value: "1,423"
|
|
55
55
|
change: "+5.8%"
|
|
56
56
|
change_type: "success"
|
|
57
57
|
icon: "users"
|
|
58
58
|
|
|
59
59
|
- id: "active_projects"
|
|
60
|
-
title: "Active
|
|
60
|
+
title: "Active projects"
|
|
61
61
|
value: "14"
|
|
62
62
|
change: "2 pending"
|
|
63
63
|
change_type: "warning"
|
|
@@ -107,7 +107,7 @@ quick_actions:
|
|
|
107
107
|
href: "#"
|
|
108
108
|
|
|
109
109
|
- id: "upload_files"
|
|
110
|
-
label: "Upload
|
|
110
|
+
label: "Upload files"
|
|
111
111
|
icon: "upload"
|
|
112
112
|
style: "outline-adaptive"
|
|
113
113
|
href: "#"
|
|
@@ -119,7 +119,7 @@ quick_actions:
|
|
|
119
119
|
href: "#"
|
|
120
120
|
|
|
121
121
|
- id: "view_analytics"
|
|
122
|
-
label: "View
|
|
122
|
+
label: "View analytics"
|
|
123
123
|
icon: "chart-bar"
|
|
124
124
|
style: "outline-adaptive"
|
|
125
125
|
href: "/analytics"
|
|
@@ -132,17 +132,17 @@ quick_actions:
|
|
|
132
132
|
|
|
133
133
|
progress_items:
|
|
134
134
|
- id: "project_alpha"
|
|
135
|
-
name: "Project
|
|
135
|
+
name: "Project alpha"
|
|
136
136
|
progress: 75
|
|
137
137
|
color: "primary"
|
|
138
138
|
|
|
139
139
|
- id: "website_redesign"
|
|
140
|
-
name: "Website
|
|
140
|
+
name: "Website redesign"
|
|
141
141
|
progress: 50
|
|
142
142
|
color: "info"
|
|
143
143
|
|
|
144
144
|
- id: "mobile_app"
|
|
145
|
-
name: "Mobile
|
|
145
|
+
name: "Mobile app"
|
|
146
146
|
progress: 90
|
|
147
147
|
color: "success"
|
|
148
148
|
---
|
|
@@ -40,7 +40,7 @@ story:
|
|
|
40
40
|
description: "Reached customers in over 100 countries and opened international offices"
|
|
41
41
|
- year: "2023"
|
|
42
42
|
title: "Innovation award"
|
|
43
|
-
description: "Recognized as
|
|
43
|
+
description: "Recognized as industry leader and received multiple awards for innovation"
|
|
44
44
|
- year: "{{ site.uj.date.year }}"
|
|
45
45
|
title: "The future"
|
|
46
46
|
description: "Continuing to push boundaries and build the future of business technology"
|
package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html
CHANGED
|
@@ -31,7 +31,7 @@ alternative:
|
|
|
31
31
|
comparison:
|
|
32
32
|
superheadline:
|
|
33
33
|
icon: "scale-balanced"
|
|
34
|
-
text: "Head to
|
|
34
|
+
text: "Head to head"
|
|
35
35
|
headline: "See how we"
|
|
36
36
|
headline_accent: "compare"
|
|
37
37
|
subheadline: "A side-by-side look at {{ site.brand.name }} vs {{ page.resolved.alternative.competitor.name }}"
|
|
@@ -87,7 +87,7 @@ alternative:
|
|
|
87
87
|
stats:
|
|
88
88
|
- number: "50,000+"
|
|
89
89
|
label: "Active users"
|
|
90
|
-
sublabel: "From 120+
|
|
90
|
+
sublabel: "From 120+ countries"
|
|
91
91
|
icon: "users"
|
|
92
92
|
color: "primary"
|
|
93
93
|
- number: "4.9"
|
|
@@ -97,7 +97,7 @@ alternative:
|
|
|
97
97
|
show_stars: true
|
|
98
98
|
- number: "99.99%"
|
|
99
99
|
label: "Uptime"
|
|
100
|
-
sublabel: "Enterprise-
|
|
100
|
+
sublabel: "Enterprise-grade"
|
|
101
101
|
icon: "shield-check"
|
|
102
102
|
color: "success"
|
|
103
103
|
- number: "24/7"
|
|
@@ -130,7 +130,7 @@ alternative:
|
|
|
130
130
|
cta:
|
|
131
131
|
superheadline:
|
|
132
132
|
icon: "rocket"
|
|
133
|
-
text: "Make the
|
|
133
|
+
text: "Make the switch"
|
|
134
134
|
headline: "Ready to leave"
|
|
135
135
|
headline_accent: "{{ page.resolved.alternative.competitor.name }} behind?"
|
|
136
136
|
description: "Join thousands who chose {{ site.brand.name }}. Start your free trial today — no credit card required."
|
|
@@ -10,7 +10,7 @@ theme:
|
|
|
10
10
|
### PAGE CONFIG ###
|
|
11
11
|
# Hero Section
|
|
12
12
|
hero:
|
|
13
|
-
tagline: "Compare &
|
|
13
|
+
tagline: "Compare & choose"
|
|
14
14
|
headline: "{{ site.brand.name }} vs the"
|
|
15
15
|
headline_accent: "competition"
|
|
16
16
|
description: "Not sure if {{ site.brand.name }} is right for you? Compare us head-to-head with the most popular alternatives and see why thousands are making the switch."
|
|
@@ -48,10 +48,10 @@ value_props:
|
|
|
48
48
|
- title: "Lightning fast"
|
|
49
49
|
description: "Built on modern infrastructure for blazing performance. No more waiting around."
|
|
50
50
|
icon: "bolt"
|
|
51
|
-
- title: "AI-
|
|
51
|
+
- title: "AI-powered"
|
|
52
52
|
description: "Smart automation built into every workflow to save you hours every week."
|
|
53
53
|
icon: "sparkles"
|
|
54
|
-
- title: "24/7
|
|
54
|
+
- title: "24/7 support"
|
|
55
55
|
description: "Real humans ready to help whenever you need it. Average response time under 5 minutes."
|
|
56
56
|
icon: "headset"
|
|
57
57
|
|
|
@@ -59,7 +59,7 @@ value_props:
|
|
|
59
59
|
stats:
|
|
60
60
|
- number: "50,000+"
|
|
61
61
|
label: "Active users"
|
|
62
|
-
sublabel: "From 120+
|
|
62
|
+
sublabel: "From 120+ countries"
|
|
63
63
|
icon: "users"
|
|
64
64
|
color: "primary"
|
|
65
65
|
- number: "4.9"
|
|
@@ -69,7 +69,7 @@ stats:
|
|
|
69
69
|
show_stars: true
|
|
70
70
|
- number: "99.99%"
|
|
71
71
|
label: "Uptime"
|
|
72
|
-
sublabel: "Enterprise-
|
|
72
|
+
sublabel: "Enterprise-grade"
|
|
73
73
|
icon: "shield-check"
|
|
74
74
|
color: "success"
|
|
75
75
|
- number: "24/7"
|
|
@@ -28,7 +28,7 @@ downloads:
|
|
|
28
28
|
description: "Determining your operating system..."
|
|
29
29
|
button_text: "Please wait"
|
|
30
30
|
coming_soon_mobile:
|
|
31
|
-
headline: "Coming
|
|
31
|
+
headline: "Coming soon to mobile"
|
|
32
32
|
description: "Enter your email below and we'll send you the desktop version download link."
|
|
33
33
|
not_available:
|
|
34
34
|
headline: "Not available yet"
|
|
@@ -614,7 +614,7 @@ cta:
|
|
|
614
614
|
<div class="alert alert-success d-flex align-items-center mb-4" role="alert">
|
|
615
615
|
{% uj_icon "check-circle", "fs-4 me-3" %}
|
|
616
616
|
<div>
|
|
617
|
-
<h6 class="mb-0">Download
|
|
617
|
+
<h6 class="mb-0">Download started!</h6>
|
|
618
618
|
<small class="text-muted">Your download should begin automatically.</small>
|
|
619
619
|
</div>
|
|
620
620
|
</div>
|
|
@@ -78,7 +78,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
78
78
|
|
|
79
79
|
<!-- Support Section -->
|
|
80
80
|
<div class="text-center mt-5 pt-4 border-top">
|
|
81
|
-
<h5 class="mb-3">Need
|
|
81
|
+
<h5 class="mb-3">Need help?</h5>
|
|
82
82
|
<p class="text-muted mb-2">Our support team is here to assist you</p>
|
|
83
83
|
<div class="d-flex flex-column flex-sm-row justify-content-center gap-3">
|
|
84
84
|
<a href="/contact" class="text-decoration-none link-primary">
|
|
@@ -60,7 +60,7 @@ pricing:
|
|
|
60
60
|
icon: "download"
|
|
61
61
|
# Additional features
|
|
62
62
|
- id: "api_access"
|
|
63
|
-
name: "API
|
|
63
|
+
name: "API access"
|
|
64
64
|
icon: "code"
|
|
65
65
|
- id: "priority_support"
|
|
66
66
|
name: "Priority support"
|
|
@@ -201,7 +201,7 @@ faqs:
|
|
|
201
201
|
15% OFF!
|
|
202
202
|
</span>
|
|
203
203
|
<span id="pricing-promo-text" class="fw-semibold">
|
|
204
|
-
Flash
|
|
204
|
+
Flash sale
|
|
205
205
|
</span>
|
|
206
206
|
<span class="text-white-50">
|
|
207
207
|
Ending in <span id="pricing-promo-countdown" class="fw-semibold text-white">--</span>
|
|
@@ -561,7 +561,7 @@ faqs:
|
|
|
561
561
|
<em>Everything in</em>
|
|
562
562
|
<em>
|
|
563
563
|
<strong>{{ _prev_plan.name }}</strong>
|
|
564
|
-
{%- if has_additional -%},
|
|
564
|
+
{%- if has_additional -%}, and more:{%- endif -%}
|
|
565
565
|
</em>
|
|
566
566
|
</div>
|
|
567
567
|
|
|
@@ -149,7 +149,7 @@ company_values:
|
|
|
149
149
|
<div class="container">
|
|
150
150
|
<div class="text-center mb-5" data-lazy="@class animation-slide-up">
|
|
151
151
|
<h2 class="h2 mb-3">
|
|
152
|
-
Our <span class="">
|
|
152
|
+
Our <span class="">core values</span>
|
|
153
153
|
</h2>
|
|
154
154
|
<p class="fs-5 text-muted">The principles that guide everything we do</p>
|
|
155
155
|
</div>
|
|
@@ -181,7 +181,7 @@ company_values:
|
|
|
181
181
|
{% uj_icon "briefcase", "text-primary display-4 mb-4" %}
|
|
182
182
|
</div>
|
|
183
183
|
<h2 class="h2 mb-4">
|
|
184
|
-
Want to
|
|
184
|
+
Want to join our <span class="">team</span>?
|
|
185
185
|
</h2>
|
|
186
186
|
<p class="fs-5 text-muted mb-4">
|
|
187
187
|
We're always looking for talented, passionate people to join our mission. Check out our open positions and become part of something amazing.
|
|
@@ -7,9 +7,9 @@ permalink: /test/libraries/ads
|
|
|
7
7
|
sitemap:
|
|
8
8
|
include: false
|
|
9
9
|
meta:
|
|
10
|
-
title: "AdSense
|
|
10
|
+
title: "AdSense test page"
|
|
11
11
|
description: "Test page for demonstrating AdSense ad units"
|
|
12
|
-
breadcrumb: "AdSense
|
|
12
|
+
breadcrumb: "AdSense test"
|
|
13
13
|
index: false
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -17,12 +17,12 @@ meta:
|
|
|
17
17
|
<div class="container">
|
|
18
18
|
<div class="row">
|
|
19
19
|
<div class="col-lg-8 mx-auto">
|
|
20
|
-
<h1 class="h2 mb-4">AdSense
|
|
20
|
+
<h1 class="h2 mb-4">AdSense test page</h1>
|
|
21
21
|
<p class="lead mb-5">This page demonstrates different AdSense ad unit types and lazy loading behavior.</p>
|
|
22
22
|
|
|
23
23
|
<!-- Display Ad -->
|
|
24
24
|
<section>
|
|
25
|
-
<h2 class="h4 mb-3">1. Display
|
|
25
|
+
<h2 class="h4 mb-3">1. Display ad</h2>
|
|
26
26
|
{% include /modules/adunits/adsense.html type="display" %}
|
|
27
27
|
</section>
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ meta:
|
|
|
30
30
|
|
|
31
31
|
<!-- In-Article Ad -->
|
|
32
32
|
<section>
|
|
33
|
-
<h2 class="h4 mb-3">2. In-
|
|
33
|
+
<h2 class="h4 mb-3">2. In-article ad</h2>
|
|
34
34
|
{% include /modules/adunits/adsense.html type="in-article" %}
|
|
35
35
|
</section>
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ meta:
|
|
|
38
38
|
|
|
39
39
|
<!-- In-Feed Ad (Image Above) -->
|
|
40
40
|
<section>
|
|
41
|
-
<h2 class="h4 mb-3">3. In-
|
|
41
|
+
<h2 class="h4 mb-3">3. In-feed ad (image above)</h2>
|
|
42
42
|
{% include /modules/adunits/adsense.html type="in-feed" layout="image-above" %}
|
|
43
43
|
</section>
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ meta:
|
|
|
46
46
|
|
|
47
47
|
<!-- In-Feed Ad (Image Side) -->
|
|
48
48
|
<section>
|
|
49
|
-
<h2 class="h4 mb-3">4. In-
|
|
49
|
+
<h2 class="h4 mb-3">4. In-feed ad (image side)</h2>
|
|
50
50
|
{% include /modules/adunits/adsense.html type="in-feed" layout="image-side" %}
|
|
51
51
|
</section>
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ meta:
|
|
|
54
54
|
|
|
55
55
|
<!-- Multiplex Ad -->
|
|
56
56
|
<section>
|
|
57
|
-
<h2 class="h4 mb-3">5. Multiplex
|
|
57
|
+
<h2 class="h4 mb-3">5. Multiplex ad</h2>
|
|
58
58
|
{% include /modules/adunits/adsense.html type="multiplex" %}
|
|
59
59
|
</section>
|
|
60
60
|
|
|
@@ -63,7 +63,7 @@ meta:
|
|
|
63
63
|
|
|
64
64
|
<!-- Lazy loaded display ad -->
|
|
65
65
|
<section>
|
|
66
|
-
<h2 class="h4 mb-3">6. Lazy
|
|
66
|
+
<h2 class="h4 mb-3">6. Lazy loaded display ad (below the fold)</h2>
|
|
67
67
|
<p class="text-muted mb-3">This ad should only load when scrolled into view.</p>
|
|
68
68
|
{% include /modules/adunits/adsense.html type="display" %}
|
|
69
69
|
</section>
|
|
@@ -24,10 +24,10 @@ meta:
|
|
|
24
24
|
<h3 class="mb-3">Rainbow gradient effects</h3>
|
|
25
25
|
<h1 class="text-gradient-rainbow">Static rainbow text</h1>
|
|
26
26
|
<h2 class="text-gradient-rainbow gradient-animated">Animated rainbow text</h2>
|
|
27
|
-
<p class="fs-4">Button
|
|
27
|
+
<p class="fs-4">Button examples:</p>
|
|
28
28
|
<button class="btn btn-gradient-rainbow me-2">Static rainbow button</button>
|
|
29
29
|
<button class="btn btn-gradient-rainbow gradient-animated">Animated rainbow button</button>
|
|
30
|
-
<p class="fs-4 mt-3">Background
|
|
30
|
+
<p class="fs-4 mt-3">Background example:</p>
|
|
31
31
|
<div class="p-3 bg-gradient-rainbow gradient-animated text-light rounded">Animated rainbow background</div>
|
|
32
32
|
|
|
33
33
|
<h3 class="mt-4 mb-3">Core animations</h3>
|
|
@@ -151,7 +151,7 @@ meta:
|
|
|
151
151
|
|
|
152
152
|
<!-- Classy Theme Specific -->
|
|
153
153
|
<section class="p-4 text-light rounded">
|
|
154
|
-
<h2 class="border-bottom border-light pb-2 mb-4">✨ Classy
|
|
154
|
+
<h2 class="border-bottom border-light pb-2 mb-4">✨ Classy theme components</h2>
|
|
155
155
|
|
|
156
156
|
<h3 class="mb-3">Glassy effects</h3>
|
|
157
157
|
<div class="p-5 bg-gradient-rainbow gradient-animated rounded mb-4">
|
|
@@ -312,7 +312,7 @@ meta:
|
|
|
312
312
|
<p class="fs-5">Font size 5</p>
|
|
313
313
|
<p class="fs-6">Font size 6</p>
|
|
314
314
|
|
|
315
|
-
<h3 class="mt-4 mb-3">Font
|
|
315
|
+
<h3 class="mt-4 mb-3">Font weight & style</h3>
|
|
316
316
|
<p class="fw-bold">Bold text</p>
|
|
317
317
|
<p class="fw-bolder">Bolder weight text</p>
|
|
318
318
|
<p class="fw-semibold">Semibold weight text</p>
|
|
@@ -604,7 +604,7 @@ meta:
|
|
|
604
604
|
<input class="form-control" type="file" id="formFile">
|
|
605
605
|
</div>
|
|
606
606
|
|
|
607
|
-
<h3 class="mt-4 mb-3">Form &
|
|
607
|
+
<h3 class="mt-4 mb-3">Form & button size comparison</h3>
|
|
608
608
|
<div class="card">
|
|
609
609
|
<div class="card-body">
|
|
610
610
|
<div class="row mb-3">
|
|
@@ -853,7 +853,7 @@ meta:
|
|
|
853
853
|
</ul>
|
|
854
854
|
</div>
|
|
855
855
|
<div class="col-md-6">
|
|
856
|
-
<h3 class="mb-3">List
|
|
856
|
+
<h3 class="mb-3">List group with badges</h3>
|
|
857
857
|
<ul class="list-group">
|
|
858
858
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
|
859
859
|
A list item
|
|
@@ -25,7 +25,7 @@ sitemap:
|
|
|
25
25
|
</style>
|
|
26
26
|
</head>
|
|
27
27
|
<body>
|
|
28
|
-
<h1>Firestore
|
|
28
|
+
<h1>Firestore version + transport test</h1>
|
|
29
29
|
<div id="ua"></div>
|
|
30
30
|
<div id="status">Loading...</div>
|
|
31
31
|
<div id="sw-status" style="color: #f80; margin-bottom: 12px;"></div>
|
|
@@ -15,7 +15,7 @@ meta:
|
|
|
15
15
|
|
|
16
16
|
<section>
|
|
17
17
|
<div class="container">
|
|
18
|
-
<h1 class="mb-4">FormManager
|
|
18
|
+
<h1 class="mb-4">FormManager test page</h1>
|
|
19
19
|
<p class="text-muted mb-5">Testing different configurations of the FormManager library.</p>
|
|
20
20
|
|
|
21
21
|
<div class="row g-4">
|
|
@@ -204,7 +204,7 @@ meta:
|
|
|
204
204
|
<div class="row">
|
|
205
205
|
<!-- Global settings (no data-input-group = always included) -->
|
|
206
206
|
<div class="col-md-4">
|
|
207
|
-
<h6 class="text-muted mb-3">Global
|
|
207
|
+
<h6 class="text-muted mb-3">Global settings <span class="badge bg-secondary">Always included</span></h6>
|
|
208
208
|
<div class="mb-3">
|
|
209
209
|
<label for="groups-name" class="form-label">settings.name</label>
|
|
210
210
|
<input type="text" class="form-control" id="groups-name" name="settings.name" value="My project" disabled>
|
|
@@ -33,7 +33,7 @@ meta:
|
|
|
33
33
|
|
|
34
34
|
<!-- First visible image (should load immediately) -->
|
|
35
35
|
<section>
|
|
36
|
-
<h2 class="h3 mb-3">1. Above the
|
|
36
|
+
<h2 class="h3 mb-3">1. Above the fold image (lazy loading starts immediately)</h2>
|
|
37
37
|
<p>This image is visible on page load, so it should start loading immediately:</p>
|
|
38
38
|
<img data-lazy="@src https://placehold.co/800x400?text=Immediate+Load"
|
|
39
39
|
class="card-img-top lazy"
|
|
@@ -50,7 +50,7 @@ meta:
|
|
|
50
50
|
|
|
51
51
|
<!-- Lazy loaded images with data-src -->
|
|
52
52
|
<section>
|
|
53
|
-
<h2 class="h3 mb-3">2. Standard
|
|
53
|
+
<h2 class="h3 mb-3">2. Standard lazy loading (data-src)</h2>
|
|
54
54
|
<p>These images use <code>data-src</code> attribute and will load when scrolled into view:</p>
|
|
55
55
|
|
|
56
56
|
<div class="row g-4 mb-4">
|
|
@@ -153,7 +153,7 @@ meta:
|
|
|
153
153
|
|
|
154
154
|
<!-- Lazy Classes -->
|
|
155
155
|
<section>
|
|
156
|
-
<h2 class="h3 mb-3">6. Lazy
|
|
156
|
+
<h2 class="h3 mb-3">6. Lazy loaded classes</h2>
|
|
157
157
|
<p>These elements get classes added when they come into view:</p>
|
|
158
158
|
|
|
159
159
|
<div class="row g-4">
|
|
@@ -194,7 +194,7 @@ meta:
|
|
|
194
194
|
|
|
195
195
|
<!-- Iframes -->
|
|
196
196
|
<section>
|
|
197
|
-
<h2 class="h3 mb-3">7. Lazy
|
|
197
|
+
<h2 class="h3 mb-3">7. Lazy loaded iframes</h2>
|
|
198
198
|
<p>YouTube videos and other iframes that load on scroll:</p>
|
|
199
199
|
|
|
200
200
|
<div class="ratio ratio-16x9 mb-4">
|
|
@@ -242,7 +242,7 @@ meta:
|
|
|
242
242
|
|
|
243
243
|
<!-- Video -->
|
|
244
244
|
<section>
|
|
245
|
-
<h2 class="h3 mb-3">9. Lazy
|
|
245
|
+
<h2 class="h3 mb-3">9. Lazy loaded video</h2>
|
|
246
246
|
<p>HTML5 video that loads when scrolled into view:</p>
|
|
247
247
|
|
|
248
248
|
<video data-lazy="@src https://www.w3schools.com/html/mov_bbb.mp4"
|
|
@@ -257,7 +257,7 @@ meta:
|
|
|
257
257
|
|
|
258
258
|
<!-- Slow Loading Test with Picsum -->
|
|
259
259
|
<section>
|
|
260
|
-
<h2 class="h3 mb-3">10. Slow
|
|
260
|
+
<h2 class="h3 mb-3">10. Slow loading test (loading animation demo)</h2>
|
|
261
261
|
<p>These images from picsum.photos load slowly, so you can see the loading animation:</p>
|
|
262
262
|
|
|
263
263
|
<div class="row g-4">
|
|
@@ -325,7 +325,7 @@ meta:
|
|
|
325
325
|
|
|
326
326
|
<!-- Error handling test -->
|
|
327
327
|
<section>
|
|
328
|
-
<h2 class="h3 mb-3">12. Error
|
|
328
|
+
<h2 class="h3 mb-3">12. Error handling test</h2>
|
|
329
329
|
<p>These images have invalid URLs to test error handling:</p>
|
|
330
330
|
|
|
331
331
|
<div class="row g-4">
|
|
@@ -373,7 +373,7 @@ meta:
|
|
|
373
373
|
|
|
374
374
|
<!-- Dynamic content test -->
|
|
375
375
|
<section>
|
|
376
|
-
<h2 class="h3 mb-3">14. Dynamic
|
|
376
|
+
<h2 class="h3 mb-3">14. Dynamic content test</h2>
|
|
377
377
|
<p>Click the button below to dynamically add new lazy-loaded images to test MutationObserver:</p>
|
|
378
378
|
|
|
379
379
|
<button id="add-dynamic-images" class="btn btn-primary mb-4">Add dynamic images</button>
|
|
@@ -54,7 +54,7 @@ web_manager:
|
|
|
54
54
|
{%- endif -%}
|
|
55
55
|
|
|
56
56
|
{% assign url_parts = page.url | split: '/' %}
|
|
57
|
-
{% assign section_name = "Root
|
|
57
|
+
{% assign section_name = "Root pages" %}
|
|
58
58
|
|
|
59
59
|
{% if url_parts.size > 2 %}
|
|
60
60
|
{% assign section_name = url_parts[1] | replace: '-', ' ' | replace: '_', ' ' | capitalize %}
|
|
@@ -104,7 +104,7 @@ web_manager:
|
|
|
104
104
|
{%- endif -%}
|
|
105
105
|
|
|
106
106
|
{% assign url_parts = page.url | split: '/' %}
|
|
107
|
-
{% assign page_section = "Root
|
|
107
|
+
{% assign page_section = "Root pages" %}
|
|
108
108
|
|
|
109
109
|
{% if url_parts.size > 2 %}
|
|
110
110
|
{% assign page_section = url_parts[1] | replace: '-', ' ' | replace: '_', ' ' | capitalize %}
|
|
@@ -6,6 +6,7 @@ const glob = require('glob').globSync;
|
|
|
6
6
|
const responsive = require('gulp-responsive-modern');
|
|
7
7
|
const sharp = require('sharp');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const { Transform } = require('stream');
|
|
9
10
|
const jetpack = require('fs-jetpack');
|
|
10
11
|
const GitHubCache = require('./utils/github-cache');
|
|
11
12
|
|
|
@@ -25,8 +26,12 @@ let githubCache;
|
|
|
25
26
|
// Supported image extensions
|
|
26
27
|
const ALL_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
27
28
|
const RESPONSIVE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png']);
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Globs include upper- and lower-case variants so micromatch (used by gulp-responsive-modern)
|
|
30
|
+
// matches files like IMG_3119.JPG. micromatch is strictly case-sensitive even on case-insensitive
|
|
31
|
+
// filesystems, so we expand each extension to both cases rather than relying on a nocase flag.
|
|
32
|
+
const expandCases = (exts) => exts.flatMap((ext) => [ext, ext.toUpperCase()]);
|
|
33
|
+
const ALL_IMAGE_GLOB = `*.{${expandCases(ALL_IMAGE_EXTENSIONS).join(',')}}`;
|
|
34
|
+
const RESPONSIVE_GLOB = `*.{${expandCases([...RESPONSIVE_EXTENSIONS]).join(',')}}`;
|
|
30
35
|
|
|
31
36
|
// Glob
|
|
32
37
|
const input = [
|
|
@@ -184,6 +189,7 @@ async function imagemin(complete) {
|
|
|
184
189
|
// above (so `npm start` never blocks on this), letting BrowserSync reload as images land later.
|
|
185
190
|
await new Promise((resolve, reject) => {
|
|
186
191
|
src(filesToProcess, { base: 'src/assets/images' })
|
|
192
|
+
.pipe(lowercaseExtTransform())
|
|
187
193
|
.pipe(responsive({
|
|
188
194
|
[`**/${RESPONSIVE_GLOB}`]: responsiveConfigs
|
|
189
195
|
}, {
|
|
@@ -354,6 +360,24 @@ async function rewriteOversizedSources(files) {
|
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
|
|
363
|
+
// Lowercase the extension on each Vinyl file's path before piping into gulp-responsive-modern.
|
|
364
|
+
// gulp-responsive-modern's lib/format.js uses a case-sensitive switch on path.extname() and returns
|
|
365
|
+
// the string 'unsupported' for anything else, which then crashes sharp.toFormat(). Files saved
|
|
366
|
+
// straight off a camera (IMG_3119.JPG) hit this. Rewriting the Vinyl path in-stream keeps the
|
|
367
|
+
// on-disk file untouched while letting the plugin recognize the format.
|
|
368
|
+
function lowercaseExtTransform() {
|
|
369
|
+
return new Transform({
|
|
370
|
+
objectMode: true,
|
|
371
|
+
transform(file, _enc, cb) {
|
|
372
|
+
const ext = path.extname(file.path);
|
|
373
|
+
if (ext && ext !== ext.toLowerCase()) {
|
|
374
|
+
file.path = file.path.slice(0, -ext.length) + ext.toLowerCase();
|
|
375
|
+
}
|
|
376
|
+
cb(null, file);
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
357
381
|
// Build responsive configurations from PICTURE_SIZES
|
|
358
382
|
function getResponsiveConfigs() {
|
|
359
383
|
const configs = [];
|
|
@@ -548,21 +572,26 @@ function logImageStatistics(stats, startTime, endTime) {
|
|
|
548
572
|
// Size reduction stats
|
|
549
573
|
if (stats.sizeBefore > 0 && stats.sizeAfter > 0) {
|
|
550
574
|
const savedPercent = ((stats.savedBytes / stats.sizeBefore) * 100).toFixed(1);
|
|
575
|
+
const label = stats.savedBytes < 0 ? 'Total added' : 'Total saved';
|
|
551
576
|
logger.log('\n💾 Size Reduction:');
|
|
552
577
|
logger.log(` Original size: ${formatBytes(stats.sizeBefore)}`);
|
|
553
578
|
logger.log(` Optimized size: ${formatBytes(stats.sizeAfter)}`);
|
|
554
|
-
logger.log(`
|
|
579
|
+
logger.log(` ${label}: ${formatBytes(Math.abs(stats.savedBytes))} (${savedPercent}%)`);
|
|
555
580
|
}
|
|
556
581
|
|
|
557
582
|
logger.log('═══════════════════════════════════════\n');
|
|
558
583
|
}
|
|
559
584
|
|
|
560
|
-
// Helper to format bytes
|
|
585
|
+
// Helper to format bytes. Handles negative inputs — when responsive variants (8 per source)
|
|
586
|
+
// sum to more than the cached original, savedBytes goes negative; without the absolute-value
|
|
587
|
+
// guard, Math.log(negative) is NaN and the suffix index becomes NaN -> "NaN undefined".
|
|
561
588
|
function formatBytes(bytes, decimals = 2) {
|
|
562
589
|
if (bytes === 0) return '0 Bytes';
|
|
590
|
+
const sign = bytes < 0 ? '-' : '';
|
|
591
|
+
const abs = Math.abs(bytes);
|
|
563
592
|
const k = 1024;
|
|
564
593
|
const dm = decimals < 0 ? 0 : decimals;
|
|
565
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
566
|
-
const i = Math.floor(Math.log(
|
|
567
|
-
return parseFloat((
|
|
594
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
595
|
+
const i = Math.min(Math.floor(Math.log(abs) / Math.log(k)), sizes.length - 1);
|
|
596
|
+
return sign + parseFloat((abs / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
568
597
|
}
|
|
@@ -9,18 +9,24 @@
|
|
|
9
9
|
// Skipped entirely when Manager.isServer() returns true — CI/cloud runs don't need a logs/
|
|
10
10
|
// directory left behind in the workspace.
|
|
11
11
|
//
|
|
12
|
-
// Truncates fresh on each call (
|
|
12
|
+
// Truncates fresh on each call (O_TRUNC), so a new `npm start` doesn't accumulate stale
|
|
13
13
|
// lines from the previous run.
|
|
14
14
|
//
|
|
15
|
-
// Idempotent: calling twice with the same path just returns the existing
|
|
15
|
+
// Idempotent: calling twice with the same path just returns the existing fd.
|
|
16
|
+
//
|
|
17
|
+
// Uses synchronous fs.writeSync(fd, ...) rather than createWriteStream(). Reason: gulp tasks
|
|
18
|
+
// crash via thrown errors that propagate to process.exit, and createWriteStream's internal
|
|
19
|
+
// buffer was being dropped before the kernel could flush it — so the very lines describing
|
|
20
|
+
// the crash (the most important ones) never made it to disk. Synchronous writes incur a
|
|
21
|
+
// per-line syscall but guarantee the tail of the log survives an immediate exit.
|
|
16
22
|
|
|
17
23
|
const fs = require('fs');
|
|
18
24
|
const path = require('path');
|
|
19
25
|
|
|
20
26
|
const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
|
|
21
27
|
|
|
22
|
-
let
|
|
23
|
-
let activePath
|
|
28
|
+
let activeFd = null;
|
|
29
|
+
let activePath = null;
|
|
24
30
|
let originalStdoutWrite = null;
|
|
25
31
|
let originalStderrWrite = null;
|
|
26
32
|
|
|
@@ -33,38 +39,40 @@ function attachLogFile(name) {
|
|
|
33
39
|
|
|
34
40
|
const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
|
|
35
41
|
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
42
|
+
if (activeFd !== null && activePath === abs) return activeFd;
|
|
43
|
+
if (activeFd !== null) detach();
|
|
38
44
|
|
|
39
45
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
40
|
-
const
|
|
46
|
+
const fd = fs.openSync(abs, 'w');
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
fs.writeSync(fd, `# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
|
|
43
49
|
|
|
44
50
|
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
45
51
|
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
46
52
|
|
|
47
53
|
process.stdout.write = function (chunk, ...rest) {
|
|
48
|
-
try {
|
|
54
|
+
try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
49
55
|
return originalStdoutWrite(chunk, ...rest);
|
|
50
56
|
};
|
|
51
57
|
process.stderr.write = function (chunk, ...rest) {
|
|
52
|
-
try {
|
|
58
|
+
try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
53
59
|
return originalStderrWrite(chunk, ...rest);
|
|
54
60
|
};
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
activePath
|
|
62
|
+
activeFd = fd;
|
|
63
|
+
activePath = abs;
|
|
58
64
|
|
|
59
|
-
return
|
|
65
|
+
return fd;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
function detach() {
|
|
63
69
|
if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
|
|
64
70
|
if (originalStderrWrite) process.stderr.write = originalStderrWrite;
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if (activeFd !== null) {
|
|
72
|
+
try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
activeFd = null;
|
|
75
|
+
activePath = null;
|
|
68
76
|
originalStdoutWrite = null;
|
|
69
77
|
originalStderrWrite = null;
|
|
70
78
|
}
|