ultimate-jekyll-manager 1.4.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CLAUDE-ATTRIBUTION.md +215 -0
  3. package/CLAUDE.md +7 -6
  4. package/README.md +1 -0
  5. package/dist/assets/css/pages/test/libraries/layers/index.scss +28 -0
  6. package/dist/assets/js/modules/redirect.js +5 -4
  7. package/dist/assets/js/pages/download/index.js +1 -1
  8. package/dist/assets/js/pages/feedback/index.js +1 -1
  9. package/dist/assets/js/pages/test/libraries/layers/index.js +11 -0
  10. package/dist/assets/themes/_template/README.md +50 -0
  11. package/dist/assets/themes/_template/_config.scss +60 -0
  12. package/dist/assets/themes/_template/_theme.js +13 -4
  13. package/dist/assets/themes/_template/_theme.scss +16 -4
  14. package/dist/assets/themes/_template/css/base/_root.scss +19 -0
  15. package/dist/assets/themes/_template/css/components/_components.scss +23 -0
  16. package/dist/assets/themes/classy/README.md +18 -6
  17. package/dist/assets/themes/neobrutalism/README.md +98 -0
  18. package/dist/assets/themes/neobrutalism/_config.scss +139 -0
  19. package/dist/assets/themes/neobrutalism/_theme.js +27 -0
  20. package/dist/assets/themes/neobrutalism/_theme.scss +33 -0
  21. package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
  22. package/dist/assets/themes/neobrutalism/css/base/_root.scss +80 -0
  23. package/dist/assets/themes/neobrutalism/css/base/_typography.scss +77 -0
  24. package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +25 -0
  25. package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
  26. package/dist/assets/themes/neobrutalism/css/components/_cards.scss +69 -0
  27. package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
  28. package/dist/assets/themes/neobrutalism/css/components/_infinite-scroll.scss +94 -0
  29. package/dist/assets/themes/neobrutalism/css/layout/_general.scss +200 -0
  30. package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +153 -0
  31. package/dist/assets/themes/neobrutalism/js/initialize-tooltips.js +20 -0
  32. package/dist/assets/themes/neobrutalism/js/navbar-scroll.js +29 -0
  33. package/dist/assets/themes/neobrutalism/pages/index.scss +227 -0
  34. package/dist/assets/themes/neobrutalism/pages/pricing/index.scss +267 -0
  35. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.js +9 -0
  36. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.scss +7 -0
  37. package/dist/build.js +2 -5
  38. package/dist/commands/install.js +1 -1
  39. package/dist/commands/setup.js +41 -0
  40. package/dist/defaults/CLAUDE.md +5 -1
  41. package/dist/defaults/dist/_includes/core/head.html +17 -0
  42. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +4 -4
  43. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +2 -0
  44. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
  45. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/core/base.html +31 -0
  46. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/index.html +345 -0
  47. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +483 -0
  48. package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
  49. package/dist/defaults/src/_config.yml +2 -0
  50. package/dist/defaults/test/_init.js +10 -0
  51. package/dist/gulp/tasks/defaults.js +8 -0
  52. package/dist/gulp/tasks/sass.js +43 -2
  53. package/dist/gulp/tasks/translation.js +11 -0
  54. package/dist/gulp/tasks/utils/manage-test-layers.js +97 -0
  55. package/dist/index.js +30 -4
  56. package/dist/test/runner.js +62 -0
  57. package/dist/test/suites/build/manager.test.js +11 -4
  58. package/dist/test/suites/build/mode-helpers.test.js +54 -2
  59. package/dist/utils/mode-helpers.js +65 -40
  60. package/docs/assets.md +6 -1
  61. package/docs/environment-detection.md +85 -0
  62. package/docs/test-framework.md +48 -3
  63. package/docs/themes.md +451 -0
  64. package/package.json +2 -1
  65. package/docs/cross-context-helpers.md +0 -75
  66. package/package copy.json +0 -75
@@ -0,0 +1,483 @@
1
+ ---
2
+ ### ALL PAGES ###
3
+ layout: themes/[ site.theme.id ]/frontend/core/base
4
+
5
+ ### PAGE CONFIG ###
6
+ # Hero Section
7
+ hero:
8
+ headline: "The right plans,"
9
+ headline_accent: "for the right price"
10
+ subheadline: "Simple and affordable pricing. No hidden fees, no surprises."
11
+
12
+ # Pricing Section
13
+ pricing:
14
+ price_per_unit:
15
+ enabled: true
16
+ feature_id: "credits"
17
+ label: "credit"
18
+ definitions:
19
+ - id: "credits"
20
+ definition: "Credits are used to generate content. Each generation consumes 1 credit."
21
+ - id: "exports"
22
+ definition: "Export your creations in various formats. Watermarked exports include a small logo."
23
+ - id: "api_access"
24
+ definition: "Access our REST API to integrate with your own applications and workflows."
25
+ - id: "priority_support"
26
+ definition: "Get faster response times and dedicated support from our team."
27
+ - id: "team_collaboration"
28
+ definition: "Invite team members to collaborate on projects and share resources."
29
+ - id: "custom_branding"
30
+ definition: "Remove our branding and add your own logo to exports."
31
+ plans:
32
+ - id: "basic"
33
+ name: "Basic"
34
+ tagline: "best for getting started"
35
+ url: "/dashboard"
36
+ features:
37
+ - id: "credits"
38
+ name: "Credits"
39
+ icon: "sparkles"
40
+ - id: "exports"
41
+ name: "Exports"
42
+ icon: "download"
43
+ - id: "storage"
44
+ name: "Logo storage"
45
+ icon: "clock"
46
+ - id: "plus"
47
+ name: "Plus"
48
+ tagline: "best for individuals"
49
+ url: null
50
+ features:
51
+ - id: "credits"
52
+ name: "Credits"
53
+ icon: "sparkles"
54
+ - id: "exports"
55
+ name: "Exports"
56
+ icon: "download"
57
+ - id: "api_access"
58
+ name: "API access"
59
+ icon: "code"
60
+ - id: "priority_support"
61
+ name: "Priority support"
62
+ icon: "headset"
63
+ - id: "pro"
64
+ name: "Pro"
65
+ tagline: "best for small businesses"
66
+ url: null
67
+ popular: true
68
+ features:
69
+ - id: "credits"
70
+ name: "Credits"
71
+ icon: "sparkles"
72
+ - id: "exports"
73
+ name: "Exports"
74
+ icon: "download"
75
+ - id: "max"
76
+ name: "Max"
77
+ tagline: "best for growing businesses"
78
+ url: null
79
+ features:
80
+ - id: "credits"
81
+ name: "Credits"
82
+ icon: "sparkles"
83
+ - id: "exports"
84
+ name: "Exports"
85
+ icon: "download"
86
+ - id: "team_collaboration"
87
+ name: "Team collaboration"
88
+ icon: "users"
89
+ - id: "custom_branding"
90
+ name: "Custom branding"
91
+ icon: "palette"
92
+
93
+ # Feature Comparison Section
94
+ feature_comparison:
95
+ superheadline:
96
+ icon: "sparkles"
97
+ text: "Features"
98
+ headline: "Compare all"
99
+ headline_accent: "plans"
100
+ subheadline: "See exactly what's included in each plan"
101
+
102
+ # Social Proof Section
103
+ social_proof:
104
+ items:
105
+ - number: "5M"
106
+ label: "Customers"
107
+ color: "blue"
108
+ - number: "#1"
109
+ label: "Product of the month"
110
+ color: "pink"
111
+ - number: "4.8"
112
+ label: "Capterra"
113
+ color: "yellow"
114
+ - number: "4.8"
115
+ label: "G2"
116
+ color: "green"
117
+
118
+ # CTA Section
119
+ cta:
120
+ superheadline:
121
+ icon: "comments"
122
+ text: "Support"
123
+ headline: "Need help finding the right plan for your"
124
+ headline_accent: "needs?"
125
+ subheadline: "Talk to our support team 24/7"
126
+ button:
127
+ text: "Talk to us"
128
+ icon: "headset"
129
+ href: "/contact"
130
+
131
+ # FAQs Section
132
+ faqs:
133
+ superheadline:
134
+ icon: "messages-question"
135
+ text: "FAQs"
136
+ headline: "Frequently asked"
137
+ headline_accent: "questions"
138
+ subheadline: "Everything you need to know about {{ site.brand.name }} billing & subscriptions."
139
+ items:
140
+ - question: "Can I cancel at any time?"
141
+ answer: "Yes, you can cancel anytime, no questions asked. However, we would highly appreciate it if you could give us some feedback so we can improve."
142
+ - question: "Is there a free trial?"
143
+ answer: "Yes, there is a 14-day free trial of the paid plans. You can cancel your subscription at any time during the trial period for a full refund."
144
+ - question: "What is your refund policy?"
145
+ answer: "We offer a 14-day free trial of the premium plans. You can cancel any time during your trial for a full refund. After the trial period, you can cancel and you may receive a prorated refund for the remaining time on your subscription."
146
+ ---
147
+
148
+ {% comment %}
149
+ NEOBRUTALISM PRICING
150
+ Same data contract + price-resolution Liquid as the classy pricing page, but a
151
+ neobrutalist STRUCTURE: ink-framed plan blocks where the popular plan is an
152
+ oversized accent block, a hard square billing toggle, oversized color-block
153
+ social-proof, and a full-bleed CTA. See docs/themes.md.
154
+ {% endcomment %}
155
+
156
+ <!-- ============================================ -->
157
+ <!-- HERO -->
158
+ <!-- ============================================ -->
159
+ <section class="pricing-hero">
160
+ <div class="container">
161
+ <span class="kicker mb-4">{% uj_icon "tag", "me-1" %}Pricing</span>
162
+ <h1 class="pricing-title">
163
+ {{ page.resolved.hero.headline }}
164
+ <span class="text-accent">{{ page.resolved.hero.headline_accent }}</span>
165
+ </h1>
166
+ {% iftruthy page.resolved.hero.subheadline %}
167
+ <p class="fs-4 fw-medium mb-0">{{ page.resolved.hero.subheadline }}</p>
168
+ {% endiftruthy %}
169
+ </div>
170
+ </section>
171
+
172
+ <!-- ============================================ -->
173
+ <!-- PLANS -->
174
+ <!-- ============================================ -->
175
+ <section class="pricing-plans pt-0">
176
+ <div class="container">
177
+ <!-- Billing toggle: hard square segmented control -->
178
+ <div class="billing-toggle" role="group" aria-label="Billing toggle">
179
+ <input type="radio" class="btn-check" name="billing" id="monthly" autocomplete="off" data-billing="monthly">
180
+ <label class="billing-option" for="monthly">Monthly</label>
181
+ <input type="radio" class="btn-check" name="billing" id="annually" autocomplete="off" checked data-billing="annually">
182
+ <label class="billing-option" for="annually">Annually <span class="billing-save">up to 20% off</span></label>
183
+ </div>
184
+
185
+ <p class="text-center fw-medium mb-5">
186
+ {% uj_icon "shield-check", "me-1 text-success" %}
187
+ Every paid plan backed by a 7-day money-back guarantee
188
+ </p>
189
+
190
+ {% assign plan_count = page.resolved.pricing.plans | size %}
191
+
192
+ {% comment %} Detect common features across ALL plans (same logic as classy) {% endcomment %}
193
+ {% assign common_feature_ids = "" | split: "," %}
194
+ {% if plan_count > 0 %}
195
+ {% assign first_plan = page.resolved.pricing.plans[0] %}
196
+ {% for first_feature in first_plan.features %}
197
+ {% assign is_common = true %}
198
+ {% for check_plan in page.resolved.pricing.plans %}
199
+ {% assign found_in_plan = false %}
200
+ {% for check_feature in check_plan.features %}
201
+ {% if check_feature.id == first_feature.id %}{% assign found_in_plan = true %}{% break %}{% endif %}
202
+ {% endfor %}
203
+ {% unless found_in_plan %}{% assign is_common = false %}{% break %}{% endunless %}
204
+ {% endfor %}
205
+ {% if is_common %}{% assign common_feature_ids = common_feature_ids | push: first_feature.id %}{% endif %}
206
+ {% endfor %}
207
+ {% endif %}
208
+
209
+ <div class="pricing-plan-grid">
210
+ {% for plan in page.resolved.pricing.plans %}
211
+ {% comment %} Look up matching config product (same as classy) {% endcomment %}
212
+ {% assign _config_product = nil %}
213
+ {% for p in site.web_manager.payment.products %}
214
+ {% if p.id == plan.id %}{% assign _config_product = p %}{% break %}{% endif %}
215
+ {% endfor %}
216
+
217
+ {% if plan.pricing.monthly or plan.pricing.monthly == 0 %}
218
+ {% assign _plan_monthly = plan.pricing.monthly %}
219
+ {% elsif _config_product.prices.monthly or _config_product.prices.monthly == 0 %}
220
+ {% assign _plan_monthly = _config_product.prices.monthly %}
221
+ {% else %}{% assign _plan_monthly = 0 %}{% endif %}
222
+
223
+ {% if plan.pricing.annually or plan.pricing.annually == 0 %}
224
+ {% assign _plan_annually = plan.pricing.annually %}
225
+ {% elsif _config_product.prices.annually or _config_product.prices.annually == 0 %}
226
+ {% assign _plan_annually = _config_product.prices.annually %}
227
+ {% else %}{% assign _plan_annually = 0 %}{% endif %}
228
+
229
+ <article class="card pricing-plan {% if plan.popular %}pricing-plan--popular{% endif %}">
230
+ {% if plan.popular %}
231
+ <span class="pricing-plan-flag">Most Popular</span>
232
+ {% endif %}
233
+
234
+ <h3 class="pricing-plan-name">{{ plan.name }}</h3>
235
+ <p class="pricing-plan-tagline">{{ plan.tagline }}</p>
236
+
237
+ <!-- Price -->
238
+ <div class="pricing-plan-price">
239
+ {% if _plan_monthly == 0 %}
240
+ <span class="pricing-plan-amount">Free</span>
241
+ {% else %}
242
+ <span class="pricing-plan-currency">$</span><span class="pricing-plan-amount amount" data-monthly="{{ _plan_monthly }}" data-annually="{{ _plan_annually | divided_by: 12 | round }}">{{ _plan_annually | divided_by: 12 | round }}</span><span class="pricing-plan-per">/mo</span>
243
+ {% endif %}
244
+ </div>
245
+
246
+ {% if page.resolved.pricing.price_per_unit.enabled %}
247
+ {% comment %} Resolve the per-unit value; only render if it's a usable positive number {% endcomment %}
248
+ {% assign _ppu_value = nil %}
249
+ {% if _plan_monthly > 0 %}
250
+ {% for feature in plan.features %}
251
+ {% if feature.id == page.resolved.pricing.price_per_unit.feature_id %}
252
+ {% assign _ppu_value = feature.value %}
253
+ {% if _config_product and _ppu_value == nil %}
254
+ {% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _ppu_value = _lim[1] %}{% break %}{% endif %}{% endfor %}
255
+ {% endif %}
256
+ {% endif %}
257
+ {% endfor %}
258
+ {% endif %}
259
+ <p class="pricing-plan-ppu">
260
+ {% if _plan_monthly > 0 and _ppu_value and _ppu_value > 0 %}
261
+ {% assign monthly_price_per_unit = _plan_monthly | times: 1.0 | divided_by: _ppu_value | round: 2 %}
262
+ {% assign annual_monthly_price = _plan_annually | divided_by: 12.0 %}
263
+ {% assign annual_price_per_unit = annual_monthly_price | divided_by: _ppu_value | round: 2 %}
264
+ <span class="price-per-unit" data-monthly="${{ monthly_price_per_unit }}" data-annually="${{ annual_price_per_unit }}">${{ annual_price_per_unit }}</span> per {{ page.resolved.pricing.price_per_unit.label }}
265
+ {% elsif _plan_monthly == 0 %}
266
+ Perfect for trying out
267
+ {% endif %}
268
+ </p>
269
+ {% endif %}
270
+
271
+ <!-- CTA: all paid plans use the solid blue button; only the FREE plan
272
+ uses the outline variant so it reads as the entry-level option. -->
273
+ <div class="d-grid mb-3">
274
+ {% if _plan_monthly == 0 %}{% assign _cta_class = "btn-outline-primary" %}{% else %}{% assign _cta_class = "btn-primary" %}{% endif %}
275
+ {% iftruthy plan.url %}
276
+ <a href="{{ plan.url }}" class="btn {{ _cta_class }} btn-lg fw-bold">
277
+ {% if _plan_monthly == 0 %}Get started{% elsif plan.trial.days > 0 %}Get free trial{% else %}Get started{% endif %}
278
+ </a>
279
+ {% endiftruthy %}
280
+ {% iffalsy plan.url %}
281
+ <button class="btn {{ _cta_class }} btn-lg fw-bold" data-plan-id="{{ plan.id }}">
282
+ {% if plan.trial.days > 0 %}Get free trial{% else %}Get started{% endif %}
283
+ </button>
284
+ {% endiffalsy %}
285
+ </div>
286
+
287
+ <!-- Billing info -->
288
+ <p class="pricing-plan-billing">
289
+ {% if _plan_monthly == 0 %}
290
+ No credit card required
291
+ {% else %}
292
+ <span class="billing-info" data-monthly="Billed ${{ _plan_monthly | uj_commaify }} monthly" data-annually="Billed ${{ _plan_annually | uj_commaify }} annually">
293
+ Billed ${{ _plan_annually | uj_commaify }} annually
294
+ </span>
295
+ {% endif %}
296
+ </p>
297
+
298
+ <!-- Guarantee (every plan) -->
299
+ <p class="pricing-plan-guarantee">
300
+ {% if _plan_monthly > 0 %}
301
+ {% uj_icon "shield-check", "me-1 text-success" %}7-day money-back guarantee
302
+ {% else %}
303
+ {% uj_icon "rocket", "me-1 text-success" %}Upgrade any time
304
+ {% endif %}
305
+ </p>
306
+
307
+ <hr class="pricing-plan-rule">
308
+
309
+ {% comment %}
310
+ Feature list — same tier-inheritance logic as classy:
311
+ 1. Common features (vary by plan, shown on every card)
312
+ 2. Basic (first plan): "What you get:" + its unique features
313
+ 3. Other plans: "Everything in <prev>, and more:" + only the ADDED features
314
+ Helpers below render a value + the name (wrapped in a tooltip span when the
315
+ feature has a definition).
316
+ {% endcomment %}
317
+
318
+ <!-- 1. Common features -->
319
+ <ul class="pricing-plan-features">
320
+ {% for feature in plan.features %}
321
+ {% if common_feature_ids contains feature.id %}
322
+ {% assign _feature_value = feature.value %}
323
+ {% if _config_product and _feature_value == nil %}
324
+ {% assign _config_limit = nil %}{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _config_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
325
+ {% if _config_limit == -1 %}{% assign _feature_value = "Unlimited" %}{% elsif _config_limit %}{% assign _feature_value = _config_limit %}{% endif %}
326
+ {% endif %}
327
+ {% assign feature_definition = nil %}
328
+ {% for def in page.resolved.pricing.definitions %}{% if def.id == feature.id %}{% assign feature_definition = def.definition %}{% break %}{% endif %}{% endfor %}
329
+ <li>
330
+ <span class="pricing-plan-feature-icon">{% uj_icon feature.icon, "fa-sm" %}</span>
331
+ <span>
332
+ {% if _feature_value == "Unlimited" %}Unlimited{% elsif _feature_value == "24/7" %}{{ _feature_value }}{% elsif _feature_value == true or _feature_value == "Included" or _feature_value == "Available" or _feature_value == "Full" %}{% else %}{{ _feature_value | uj_commaify }}{% endif %}
333
+ {% iftruthy feature_definition %}<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="{{ feature_definition }}">{{ feature.name }}</span>{% endiftruthy %}
334
+ {% iffalsy feature_definition %}{{ feature.name }}{% endiffalsy %}
335
+ </span>
336
+ </li>
337
+ {% endif %}
338
+ {% endfor %}
339
+ </ul>
340
+
341
+ {% comment %} Does this plan add any non-common (unique) features? {% endcomment %}
342
+ {% assign has_additional = false %}
343
+ {% for feature in plan.features %}{% unless common_feature_ids contains feature.id %}{% assign has_additional = true %}{% break %}{% endunless %}{% endfor %}
344
+
345
+ <!-- 2/3. Inheritance label + the plan's additional features -->
346
+ {% if forloop.index == 1 %}
347
+ {% if has_additional %}
348
+ <p class="pricing-plan-inherit">What you get:</p>
349
+ {% endif %}
350
+ {% else %}
351
+ {% assign _prev_index = forloop.index0 | minus: 1 %}
352
+ {% assign _prev_plan = page.resolved.pricing.plans[_prev_index] %}
353
+ <p class="pricing-plan-inherit">Everything in <strong>{{ _prev_plan.name }}</strong>{% if has_additional %}, and more:{% endif %}</p>
354
+ {% endif %}
355
+
356
+ {% if has_additional %}
357
+ <ul class="pricing-plan-features">
358
+ {% for feature in plan.features %}
359
+ {% unless common_feature_ids contains feature.id %}
360
+ {% assign _feature_value = feature.value %}
361
+ {% if _config_product and _feature_value == nil %}
362
+ {% assign _config_limit = nil %}{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _config_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
363
+ {% if _config_limit == -1 %}{% assign _feature_value = "Unlimited" %}{% elsif _config_limit %}{% assign _feature_value = _config_limit %}{% endif %}
364
+ {% endif %}
365
+ {% assign feature_definition = nil %}
366
+ {% for def in page.resolved.pricing.definitions %}{% if def.id == feature.id %}{% assign feature_definition = def.definition %}{% break %}{% endif %}{% endfor %}
367
+ <li>
368
+ <span class="pricing-plan-feature-icon">{% uj_icon feature.icon, "fa-sm" %}</span>
369
+ <span>
370
+ {% if _feature_value == "Unlimited" %}Unlimited{% elsif _feature_value == "24/7" %}{{ _feature_value }}{% elsif _feature_value == true or _feature_value == "Included" or _feature_value == "Available" or _feature_value == "Full" %}{% else %}{{ _feature_value | uj_commaify }}{% endif %}
371
+ {% iftruthy feature_definition %}<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="{{ feature_definition }}">{{ feature.name }}</span>{% endiftruthy %}
372
+ {% iffalsy feature_definition %}{{ feature.name }}{% endiffalsy %}
373
+ </span>
374
+ </li>
375
+ {% endunless %}
376
+ {% endfor %}
377
+ </ul>
378
+ {% endif %}
379
+ </article>
380
+ {% endfor %}
381
+ </div>
382
+
383
+ <!-- Enterprise block -->
384
+ <div class="card enterprise-panel">
385
+ <div>
386
+ <h3 class="enterprise-panel-title">Enterprise</h3>
387
+ <p class="mb-0 fw-medium">Custom solutions for large organizations. Advanced security and flexible pricing based on your needs.</p>
388
+ </div>
389
+ <button class="btn btn-primary btn-lg fw-bold flex-shrink-0" data-plan-id="enterprise">
390
+ {% uj_icon "envelope", "me-2" %}Contact us
391
+ </button>
392
+ </div>
393
+ </div>
394
+ </section>
395
+
396
+ <!-- ============================================ -->
397
+ <!-- SOCIAL PROOF — oversized color blocks -->
398
+ <!-- ============================================ -->
399
+ <section class="stats">
400
+ <div class="container">
401
+ <div class="stats-grid">
402
+ {% for item in page.resolved.social_proof.items %}
403
+ {% assign stat_bg = "text-bg-primary" %}
404
+ {% case item.color %}
405
+ {% when "pink" %}{% assign stat_bg = "text-bg-secondary" %}
406
+ {% when "green" %}{% assign stat_bg = "text-bg-success" %}
407
+ {% when "yellow" %}{% assign stat_bg = "text-bg-warning" %}
408
+ {% when "info" %}{% assign stat_bg = "text-bg-info" %}
409
+ {% else %}{% assign stat_bg = "text-bg-primary" %}
410
+ {% endcase %}
411
+ <div class="stat-block {{ stat_bg }}">
412
+ <span class="stat-block-num">{{ item.number }}</span>
413
+ <span class="stat-block-label">{{ item.label }}</span>
414
+ </div>
415
+ {% endfor %}
416
+ </div>
417
+ </div>
418
+ </section>
419
+
420
+ <!-- ============================================ -->
421
+ <!-- FAQ -->
422
+ <!-- ============================================ -->
423
+ <section class="faq">
424
+ <div class="container">
425
+ <div class="row justify-content-center">
426
+ <div class="col-lg-9">
427
+ <header class="section-head mb-5">
428
+ {% iftruthy page.resolved.faqs.superheadline.text %}
429
+ <span class="kicker mb-3">{% uj_icon page.resolved.faqs.superheadline.icon, "me-1" %}{{ page.resolved.faqs.superheadline.text }}</span>
430
+ {% endiftruthy %}
431
+ <h2 class="section-title">
432
+ {{ page.resolved.faqs.headline }}
433
+ <span class="text-accent">{{ page.resolved.faqs.headline_accent }}</span>
434
+ </h2>
435
+ </header>
436
+
437
+ {% if page.resolved.faqs.items %}
438
+ <div class="accordion" id="faqAccordion">
439
+ {% for faq in page.resolved.faqs.items %}
440
+ <div class="accordion-item mb-3">
441
+ <h2 class="accordion-header">
442
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq{{ forloop.index }}" aria-expanded="false" aria-controls="faq{{ forloop.index }}">
443
+ <span class="fw-bold">{{ faq.question }}</span>
444
+ </button>
445
+ </h2>
446
+ <div id="faq{{ forloop.index }}" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
447
+ <div class="accordion-body">{{ faq.answer }}</div>
448
+ </div>
449
+ </div>
450
+ {% endfor %}
451
+ </div>
452
+ {% endif %}
453
+ </div>
454
+ </div>
455
+ </div>
456
+ </section>
457
+
458
+ <!-- ============================================ -->
459
+ <!-- CTA -->
460
+ <!-- ============================================ -->
461
+ <section class="cta">
462
+ <div class="container">
463
+ <div class="cta-panel">
464
+ {% iftruthy page.resolved.cta.superheadline.text %}
465
+ <span class="kicker kicker--invert mb-4">{% uj_icon page.resolved.cta.superheadline.icon, "me-1" %}{{ page.resolved.cta.superheadline.text }}</span>
466
+ {% endiftruthy %}
467
+ <h2 class="cta-title">
468
+ {{ page.resolved.cta.headline }}
469
+ <span class="text-accent">{{ page.resolved.cta.headline_accent }}</span>
470
+ </h2>
471
+ {% iftruthy page.resolved.cta.subheadline %}
472
+ <p class="cta-desc">{{ page.resolved.cta.subheadline }}</p>
473
+ {% endiftruthy %}
474
+ <div class="d-flex flex-wrap gap-3 justify-content-center">
475
+ <a href="{{ page.resolved.cta.button.href }}" class="btn btn-warning btn-lg">
476
+ {% uj_icon page.resolved.cta.button.icon, "me-2" %}{{ page.resolved.cta.button.text }}
477
+ </a>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </section>
482
+
483
+ {{ content | uj_content_format }}
@@ -0,0 +1,57 @@
1
+ ---
2
+ ### ALL PAGES ###
3
+ layout: themes/[ site.theme.id ]/frontend/core/minimal
4
+ permalink: /test/libraries/layers
5
+
6
+ ### REGULAR PAGES ###
7
+ sitemap:
8
+ include: false
9
+ meta:
10
+ title: "Asset layers"
11
+ description: "Live status of the Global → Theme → Consumer page-asset cascade (CSS + JS)."
12
+ breadcrumb: "Asset layers"
13
+ index: false
14
+ ---
15
+
16
+ <!--
17
+ ASSET-LAYER TEST PANEL
18
+ Shows the three-layer page-asset cascade (CSS and JS), loaded in this order:
19
+ 1. Global — the framework's own page-specific file
20
+ 2. Theme — the active theme's page-specific file
21
+ 3. Consumer — the consuming project's page-specific file
22
+ Each dot starts RED. A layer turns ITS OWN dot green when it loads (CSS via a
23
+ selector, JS by setting the dot's color). A RED dot = that layer has no file
24
+ for this page (the normal state for layers nobody customized).
25
+
26
+ The Consumer layer only loads if the consuming project has its own files at
27
+ src/assets/{css,js}/pages/test/libraries/layers/index.*. To prove it live
28
+ without committing anything, run with the UJ_TEST_LAYERS flag (see docs/themes.md
29
+ → "Asset-layer test panel"), which generates those files at build start and
30
+ auto-removes them on the next run.
31
+ -->
32
+ <section class="py-5">
33
+ <div class="container" style="max-width: 720px;">
34
+ <h1 class="mb-2">Asset layers</h1>
35
+ <p class="text-muted mb-4">
36
+ Each <strong>page-specific</strong> asset loads in three layers, in order:
37
+ <strong>Global → Theme → Consumer</strong> — all three are the same
38
+ <code>pages/&lt;path&gt;</code> file, just from different sources. Each layer turns its
39
+ own dot <strong>green</strong> when it loads; a <strong>red</strong> dot means that
40
+ layer has no file for this page (the normal state for layers nobody customized).
41
+ </p>
42
+
43
+ <h2 class="h5 mb-3">Page-specific CSS</h2>
44
+ <ul class="layer-list list-unstyled mb-4">
45
+ <li class="layer-row"><span class="layer-dot" data-layer="css-global"></span> Global <span class="text-muted">(framework default)</span></li>
46
+ <li class="layer-row"><span class="layer-dot" data-layer="css-theme"></span> Theme <span class="text-muted">(active theme)</span></li>
47
+ <li class="layer-row"><span class="layer-dot" data-layer="css-consumer"></span> Consumer <span class="text-muted">(your project)</span></li>
48
+ </ul>
49
+
50
+ <h2 class="h5 mb-3">Page-specific JS</h2>
51
+ <ul class="layer-list list-unstyled mb-0">
52
+ <li class="layer-row"><span class="layer-dot" data-layer="js-global"></span> Global <span class="text-muted">(framework default)</span></li>
53
+ <li class="layer-row"><span class="layer-dot" data-layer="js-theme"></span> Theme <span class="text-muted">(active theme)</span></li>
54
+ <li class="layer-row"><span class="layer-dot" data-layer="js-consumer"></span> Consumer <span class="text-muted">(your project)</span></li>
55
+ </ul>
56
+ </div>
57
+ </section>
@@ -277,6 +277,8 @@ manifest:
277
277
  translation:
278
278
  enabled: false
279
279
  default: "en"
280
+ exclude:
281
+ - "blog"
280
282
  languages:
281
283
  - "zh"
282
284
  - "es"
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test lifecycle hook for this project. Runs once before any suite (not a test itself).
3
+ * See ultimate-jekyll-manager/docs/test-framework.md → "test/_init.js".
4
+ */
5
+
6
+ module.exports = ({ projectRoot }) => ({
7
+ // Seed any fixture a suite needs before it runs.
8
+ async setup() {
9
+ },
10
+ });
@@ -8,6 +8,7 @@ const path = require('path');
8
8
  const { minimatch } = require('minimatch');
9
9
  const { template } = require('node-powertools');
10
10
  const createTemplateTransform = require('./utils/template-transform');
11
+ const manageTestLayers = require('./utils/manage-test-layers');
11
12
  const argv = require('yargs')(process.argv.slice(2)).parseSync();
12
13
  const JSON5 = require('json5');
13
14
 
@@ -391,6 +392,13 @@ function defaults(complete, changedFile) {
391
392
  logger.log('Starting...');
392
393
  Manager.logMemory(logger, 'Start');
393
394
 
395
+ // Test-layer fixtures (dev only): clean any prior generated files, and generate
396
+ // fresh ones into the consumer src when UJ_TEST_LAYERS=true — at build START so
397
+ // sass + jekyll pick them up. Only on a full run, not per-changed-file in watch.
398
+ if (!changedFile) {
399
+ manageTestLayers(Manager, logger);
400
+ }
401
+
394
402
  // Use changedFile if provided, otherwise use all inputs
395
403
  const filesToProcess = changedFile ? [changedFile] : input;
396
404
  logger.log('input', filesToProcess)
@@ -44,6 +44,24 @@ const bundleFiles = [
44
44
  'src/assets/css/bundles/*.scss',
45
45
  ];
46
46
 
47
+ // Build the active theme's page-CSS globs, but ONLY for `pages` dirs that exist —
48
+ // gulp's src() throws ENOENT when it scandirs a missing directory. Project theme
49
+ // takes priority over the package theme (matches the __theme__ resolution rule).
50
+ function themePageGlobs() {
51
+ if (!config.theme.id) {
52
+ return [];
53
+ }
54
+
55
+ const candidates = [
56
+ path.resolve(rootPathProject, 'src/assets/themes', config.theme.id, 'pages'),
57
+ path.resolve(rootPathPackage, 'dist/assets/themes', config.theme.id, 'pages'),
58
+ ];
59
+
60
+ return candidates
61
+ .filter((dir) => jetpack.exists(dir))
62
+ .map((dir) => `${dir}/**/*.scss`);
63
+ }
64
+
47
65
  // Glob
48
66
  const input = [
49
67
  // Bundle files (admin, and any future bundles)
@@ -56,10 +74,23 @@ const input = [
56
74
  `${rootPathPackage}/dist/assets/css/pages/**/*.scss`,
57
75
  'src/assets/css/pages/**/*.scss',
58
76
 
77
+ // Theme page-specific CSS (theme-aware page styles).
78
+ // Compiles to a SEPARATE bundle (pages/<path>/index.<themeId>.bundle.css)
79
+ // that head.html links IN ADDITION to the base + consumer page bundles.
80
+ // Missing = nothing loads (component styles handle it) — no fallback needed.
81
+ //
82
+ // Only include a glob whose base `pages` dir exists — gulp's src() throws ENOENT
83
+ // if it scandirs a non-existent directory. Most consumers don't shadow the theme,
84
+ // so the project-side path usually won't exist.
85
+ ...themePageGlobs(),
86
+
59
87
  // Files to exclude
60
88
  // '!dist/**',
61
89
  ];
62
90
 
91
+ // Marker appended to theme page bundles so head.html can find them by theme id.
92
+ const THEME_PAGE_SUFFIX = config.theme.id ? `.${config.theme.id}` : '';
93
+
63
94
  // Additional files to watch (but not compile as entry points)
64
95
  const watchInput = [
65
96
  // Watch the paths we're compiling
@@ -345,7 +376,17 @@ function sass(complete) {
345
376
  .pipe(cleanCSS({
346
377
  format: Manager.actLikeProduction() ? 'compressed' : 'beautify',
347
378
  }))
348
- .pipe(rename((file) => {
379
+ .pipe(rename((file, vinyl) => {
380
+ // Theme page CSS originates from .../themes/<id>/pages/... — tag the bundle
381
+ // with the theme id (e.g. index.neobrutalism.bundle.css) so it compiles to
382
+ // its own file that head.html links alongside the base + consumer bundles.
383
+ const sourcePath = (vinyl && vinyl.history[0]) || '';
384
+ const isThemePage = THEME_PAGE_SUFFIX
385
+ && sourcePath.replace(/\\/g, '/').includes(`/themes/${config.theme.id}/pages/`);
386
+ if (isThemePage) {
387
+ file.basename += THEME_PAGE_SUFFIX;
388
+ }
389
+
349
390
  // Add bundle to the name
350
391
  file.basename += '.bundle';
351
392
 
@@ -355,7 +396,7 @@ function sass(complete) {
355
396
  bundleNames.push('main'); // main.scss is always a root bundle
356
397
 
357
398
  // Check if this is a root-level bundle
358
- const baseName = file.basename.replace('.bundle', '');
399
+ const baseName = file.basename.replace('.bundle', '').replace(THEME_PAGE_SUFFIX, '');
359
400
  const isBundle = bundleNames.includes(baseName);
360
401
 
361
402
  // Check