ultimate-jekyll-manager 1.4.3 → 1.5.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 (65) hide show
  1. package/CHANGELOG.md +14 -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 +7 -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
package/CHANGELOG.md CHANGED
@@ -14,6 +14,20 @@ 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.5.0] - 2026-06-02
19
+
20
+ ### Added
21
+
22
+ - **Neobrutalism theme** — a complete second shipped theme (alongside `classy`), not a recolor: hard ink borders, offset "press" shadows, chunky display type, flat color-blocks, square controls. Custom homepage + pricing layouts. It restyles **standard Bootstrap classes** (`.btn`, `.card`, `.text-bg-*`) and a **universal semantic layout vocabulary** (`.section-hero`, `.showcase-row`, `.stat-block`, `.pricing-plan`, `.cta-panel`, …) — **no theme-prefixed classes** — so the same markup is swappable across themes (change `theme.id`, done). [src/assets/themes/neobrutalism/](src/assets/themes/neobrutalism/)
23
+ - **Theme system: three-layer page-asset cascade (Global → Theme → Consumer) for BOTH CSS and JS.**
24
+ - Theme page CSS: `themes/<id>/pages/<path>/index.scss` compiles to a theme-suffixed bundle (`index.<themeId>.bundle.css`) via [src/gulp/tasks/sass.js](src/gulp/tasks/sass.js), linked by [head.html](src/defaults/dist/_includes/core/head.html) with `{% iffile %}`. Missing = nothing loads (component styles handle it), no fallback needed.
25
+ - Theme page JS: `__theme__/pages/<path>/index.js` loaded as the `#theme` layer in [src/index.js](src/index.js), executed `main → theme → project` (a `webpackInclude: /\.js$/` guard stops `.scss` from being pulled into the JS import context).
26
+ - Per-page HTML layout overrides under `_layouts/themes/<id>/` reuse classy's `page.resolved.*` frontmatter data contract and fall back to classy when a layout is absent.
27
+ - **Theme-authoring template** at [src/assets/themes/_template/](src/assets/themes/_template/) + a full **[docs/themes.md](docs/themes.md)** reference for building a theme inside UJM or in a consumer project (selection/resolution, classy fallback, page CSS/JS layers, the no-prefix vocabulary, adaptive-button + interactive-accent gotchas).
28
+ - **Single interactive-accent token** (`--nb-accent-interactive` = `var(--bs-primary)`) drives all hover/active states (blue), distinct from the yellow signature accent reserved for static blocks. Adaptive buttons (`btn-adaptive`/`-inverse`) get explicit theme overrides so they render solid and press like every other button.
29
+ - **Asset-layer test harness** — `UJ_TEST_LAYERS` flag + [manage-test-layers.js](src/gulp/tasks/utils/manage-test-layers.js) generate real consumer-side test files; the `/test/libraries/layers` page renders the Global/Theme/Consumer cascade as red/green dots to prove all three layers load in order. `npm run start:test-layers` enables it.
30
+
17
31
  ---
18
32
  ## [1.4.3] - 2026-05-28
19
33
 
@@ -0,0 +1,215 @@
1
+ # Attribution Tracking System — Design & Implementation Plan
2
+
3
+ > Working design doc for the unified attribution system. Captures UTM/ITM tags, affiliate codes, and ad click IDs with a per-type "first-touch with 30-day refresh" model, plus auto-sync to the server when authenticated.
4
+
5
+ ## Goals
6
+
7
+ An all-in-one system to track how users arrive at the site so we can:
8
+ 1. Do UTM tracking (external marketing attribution)
9
+ 2. Do ITM tracking (internal mechanisms — exit popups, extension prompts, etc.)
10
+ 3. Capture ad click IDs for server-side conversion events (GA, Meta CAPI, TikTok Events API)
11
+ 4. Properly attribute recurring purchases that happen offline (subscription renewals)
12
+
13
+ ---
14
+
15
+ ## Design Decisions (settled)
16
+
17
+ - **Attribution Model**: First-touch with 30-day refresh, **per-type basis**
18
+ - Each category (UTM, ITM, affiliate, adClicks) has independent freshness
19
+ - If no data exists for a type → save it
20
+ - If data exists AND is < 30 days old → preserve (first-touch protection)
21
+ - If data exists AND is >= 30 days old → overwrite (new journey)
22
+ - Categories are independent: can overwrite stale UTM while keeping fresh affiliate
23
+ - **Freshness TTL**: 30 days globally for all types
24
+ - **Ad click IDs**: lumped into a single `adClicks` object (user only arrives from one ad at a time; they share one timestamp)
25
+ - **Server Sync**: on attribution change (if signed in) + signup + checkout
26
+ - `_meta.needsSync` flag for deferred sync when user later signs in (mirrors `notifications.syncSubscription()` pattern in web-manager)
27
+ - **Storage**: User doc (rolling latest) + Subscription doc (frozen snapshot at purchase)
28
+ - **`getFresh()` returns ALL fresh data** — server decides what's recent enough to attribute. Subscription gets a frozen snapshot used for ALL recurring conversions, even years later.
29
+ - **No backwards compatibility** — replace the old shape cleanly, remove the ad-hoc 30-day check.
30
+
31
+ ---
32
+
33
+ ## Storage Structure
34
+
35
+ ```javascript
36
+ // localStorage key: "attribution"
37
+ {
38
+ utm: {
39
+ tags: { utm_source, utm_medium, utm_campaign, utm_term, utm_content },
40
+ timestamp: "ISO string",
41
+ url: "full landing URL",
42
+ page: "/path"
43
+ },
44
+ itm: {
45
+ tags: { itm_source, itm_medium, itm_campaign, itm_content },
46
+ timestamp: "ISO string",
47
+ url: "full URL",
48
+ page: "/path"
49
+ },
50
+ affiliate: {
51
+ code: "partner123",
52
+ timestamp: "ISO string",
53
+ url: "full URL",
54
+ page: "/path"
55
+ },
56
+ adClicks: {
57
+ // All ad click IDs lumped together (user only arrives from one ad at a time)
58
+ fbclid: "from URL param",
59
+ fbc: "from _fbc cookie",
60
+ gclid: "from URL param",
61
+ ttclid: "from URL param",
62
+ timestamp: "ISO string",
63
+ url: "full URL",
64
+ page: "/path"
65
+ },
66
+ _meta: {
67
+ needsSync: false, // true if attribution changed while signed out
68
+ lastSynced: "ISO string" // last successful server sync
69
+ }
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Data Captured
76
+
77
+ | Category | Source | Params |
78
+ |---|---|---|
79
+ | **UTM** | URL query | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` |
80
+ | **ITM** | URL query (internal links, e.g. exit popup) | `itm_source`, `itm_medium`, `itm_campaign`, `itm_content` |
81
+ | **Affiliate** | URL query | `aff` or `ref` |
82
+ | **Ad Clicks** | URL query + cookie | `fbclid`, `fbc` (`_fbc` cookie), `gclid`, `ttclid` |
83
+
84
+ ITM tags are emitted by internal mechanisms. Example: `exit-popup.js` already sets `itm_source=website&itm_medium=modal&itm_campaign=exit-popup&itm_content=<pathname>` on its CTA link. Those land as a query string on the next page and get captured like UTM.
85
+
86
+ ---
87
+
88
+ ## Files to Modify
89
+
90
+ ### 1. `src/assets/js/core/query-strings.js` — Core refactor
91
+
92
+ Currently captures UTM + affiliate only (no freshness, no ITM, no ad clicks, no sync). Becomes the single owner of attribution capture + sync.
93
+
94
+ Add:
95
+ 1. Constants: `ATTRIBUTION_KEY = 'attribution'`, `FRESHNESS_DAYS = 30`, `FRESHNESS_MS = 30 * 24 * 60 * 60 * 1000`
96
+ 2. `shouldPreserveAttribution(existingData)` — returns `true` if existing data is < 30 days old (preserve), `false` if missing or stale (allow overwrite)
97
+ 3. `processUTMParams()` / `processITMParams()` / `processAffiliateParams()` / `processAdClickParams()` — each:
98
+ - Reads its params, quits if none present
99
+ - Applies `shouldPreserveAttribution()` first-touch check
100
+ - Writes its category + metadata (timestamp/url/page)
101
+ - Returns `true` if it changed anything
102
+ 4. `getFbcCookie()` — parse `document.cookie` for `_fbc`
103
+ 5. `getAttribution()` — raw stored object (all, regardless of age)
104
+ 6. `getFreshAttribution()` — only categories < 30 days old, excludes `_meta`. This is the canonical thing sent to the server.
105
+ 7. `syncAttribution()`:
106
+ - If user signed in → POST fresh attribution to backend-manager, set `_meta.lastSynced`, clear `_meta.needsSync`
107
+ - If not signed in → set `_meta.needsSync = true`
108
+ 8. Auth-state listener for deferred sync: on sign-in, if `_meta.needsSync` is true, call `syncAttribution()`
109
+ 9. Public API on `webManager._ujLibrary.attribution`:
110
+ ```javascript
111
+ {
112
+ get: () => getAttribution(), // raw, all ages — debugging
113
+ getFresh: () => getFreshAttribution(), // < 30 days — USE THIS
114
+ sync: () => syncAttribution(), // manual sync (usually automatic)
115
+ clear: () => clearAttribution() // wipe all attribution
116
+ }
117
+ ```
118
+
119
+ `processQueryStrings()` orchestrates: run each processor, OR their return values; if anything changed, save to storage and call `syncAttribution()`.
120
+
121
+ ### 2. `src/assets/js/core/auth.js` — Use fresh attribution in signup
122
+
123
+ `sendUserSignupMetadata(account)` currently reads raw `webManager.storage().get('attribution', {})` (line ~283). Change to:
124
+ ```javascript
125
+ const attribution = webManager.uj().attribution.getFresh();
126
+ ```
127
+ Everything else (the `flags.signupProcessed` SSOT gate, `/backend-manager/user/signup` endpoint, consent payload) stays as-is. Sync-on-change in query-strings.js is complementary — signup still sends attribution + context + consent in one shot.
128
+
129
+ ### 3. `src/assets/js/pages/payment/checkout/modules/api.js` — Use fresh attribution
130
+
131
+ Line ~60 currently sends raw storage:
132
+ ```javascript
133
+ attribution: webManager.storage().get('attribution', {}),
134
+ ```
135
+ Change to:
136
+ ```javascript
137
+ attribution: webManager.uj().attribution.getFresh(),
138
+ ```
139
+ (The old ad-hoc 30-day UTM check from the former `session.js` is already gone — this just swaps raw → fresh so stale categories are dropped before the server snapshots them onto the subscription.)
140
+
141
+ ---
142
+
143
+ ## Sync Flow
144
+
145
+ ```
146
+ User lands with ?utm_source=google (or itm_/fbclid/etc.)
147
+
148
+
149
+ processQueryStrings()
150
+ - per-type freshness check (first-touch w/ 30-day refresh)
151
+ - update localStorage
152
+ - did anything change?
153
+ │ yes
154
+
155
+ Is user signed in?
156
+ ├── YES → syncAttribution() now → set _meta.lastSynced
157
+ └── NO → set _meta.needsSync = true
158
+
159
+ ▼ (later) user signs in
160
+ auth-state listener
161
+ - _meta.needsSync? → syncAttribution() → clear flag
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Server-Side Data Flow (informational — backend out of scope here)
167
+
168
+ ```
169
+ USER DOC /users/{uid}
170
+ attribution: { utm, itm, affiliate, adClicks } ← rolling update on each sync
171
+
172
+ │ on purchase, snapshot →
173
+
174
+ SUBSCRIPTION DOC /users/{uid}/subscriptions/{subId}
175
+ attribution: { ... } ← FROZEN at purchase time
176
+ → used for ALL recurring billing conversion events (even years later)
177
+ ```
178
+
179
+ When a renewal fires months later, the server reads the subscription's frozen `attribution.adClicks.fbclid` (or gclid/ttclid) to send a server-side conversion event, attributing the recurring revenue to the original campaign.
180
+
181
+ ---
182
+
183
+ ## Public API Reference
184
+
185
+ ```javascript
186
+ webManager.uj().attribution.get() // raw stored attribution (all ages)
187
+ webManager.uj().attribution.getFresh() // only < 30 days — send this to server
188
+ webManager.uj().attribution.sync() // manual sync (normally automatic)
189
+ webManager.uj().attribution.clear() // wipe all attribution
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Testing Checklist
195
+
196
+ - [ ] UTM params captured on landing
197
+ - [ ] ITM params captured (via exit-popup CTA link)
198
+ - [ ] Affiliate code captured (`aff` / `ref`)
199
+ - [ ] Ad click IDs captured (`fbclid`, `gclid`, `ttclid`)
200
+ - [ ] `_fbc` cookie read correctly
201
+ - [ ] First-touch protected per-type (revisit doesn't overwrite fresh data)
202
+ - [ ] 30-day refresh per-type (stale category overwritten, fresh ones kept)
203
+ - [ ] `getFresh()` excludes stale categories and `_meta`
204
+ - [ ] Sync fires when signed in + attribution changes
205
+ - [ ] `_meta.needsSync` set when signed out + attribution changes
206
+ - [ ] Deferred sync fires on sign-in when `needsSync` is true
207
+ - [ ] Signup (`auth.js`) sends `getFresh()` attribution
208
+ - [ ] Checkout (`api.js`) sends `getFresh()` attribution
209
+
210
+ ---
211
+
212
+ ## Open Questions / Notes
213
+
214
+ - Confirm the backend `user/signup` and checkout endpoints accept the new nested `adClicks` / `itm` shape (no backwards-compat shim — server should be updated in lockstep).
215
+ - Decide the sync command/endpoint for the standalone `syncAttribution()` call (e.g. `user/attribution` vs reusing signup). Signup already carries attribution, so sync-on-change is mainly for users who change attribution *after* signup but *before* checkout.
package/CLAUDE.md CHANGED
@@ -8,7 +8,7 @@ Ultimate Jekyll Manager (UJM) is a comprehensive framework for building modern J
8
8
 
9
9
  - One-line bootstrap per context (build / frontend / service-worker)
10
10
  - Multi-stage gulp pipeline (15 tasks: defaults / distribute / webpack / sass / imagemin / jekyll / jsonToHtml / preprocess / audit / translation / minifyHtml / serve / setup / developmentRebuild)
11
- - Default Jekyll layouts + themes (`classy` shipped; per-theme SCSS load paths)
11
+ - Default Jekyll layouts + themes (`classy` + `neobrutalism` shipped; new themes inherit classy's layouts via the build-time fallback see [docs/themes.md](docs/themes.md))
12
12
  - Frontend ES-module Manager with dynamic per-page module loading
13
13
  - Service worker with Firebase Messaging + cache management
14
14
  - A built-in **three-layer test framework** (build / page / boot)
@@ -48,7 +48,7 @@ The only things that ARE safe to run inside UJM itself:
48
48
 
49
49
  1. `npm install` — install UJM's own deps
50
50
  2. `npm start` (≡ `npm run prepare:watch`) — copies `src/` → `dist/` on file change
51
- 3. Test in a consumer project: from inside the consumer, run `npx mgr install local` (swaps UJM to the local repo via the `install` CLI). Reverse with `npx mgr install prod`.
51
+ 3. Test in a consumer project: from inside the consumer, run `npx mgr install dev` to swap UJM to this local repo required whenever you edit the framework source and want the consumer to pick up the changes (the consumer otherwise keeps its installed `node_modules/ultimate-jekyll-manager`). Reverse with `npx mgr install live`.
52
52
  4. `npm test` — runs UJM's own 60 test suites
53
53
 
54
54
  ## Architecture
@@ -63,7 +63,7 @@ UJM exposes three Manager entry points:
63
63
  | Frontend (browser ES module) | `import Manager from 'ultimate-jekyll-manager'` | `new Manager().initialize()` → wires webManager + loads page module |
64
64
  | Service worker | `importScripts('/build.js')` then construct `Manager` | Manages cache + Firebase Messaging |
65
65
 
66
- All three Managers mix in shared helpers via `attachTo(Manager)` from [src/utils/mode-helpers.js](src/utils/mode-helpers.js): `isDevelopment()`, `isProduction()`, `isTesting()`, `getVersion()`. See [docs/cross-context-helpers.md](docs/cross-context-helpers.md).
66
+ All three Managers mix in shared helpers via `attachTo(Manager)` from [src/utils/mode-helpers.js](src/utils/mode-helpers.js): `isDevelopment()`, `isProduction()`, `isTesting()`, `getVersion()`. `getEnvironment()` returns `'development' | 'testing' | 'production'` (mutually exclusive — testing wins over dev); gate side effects on the INTENTIONAL check (`isProduction()` for prod-only, `isDevelopment() || isTesting()` for local-or-test) — never `!isDevelopment()`. See [docs/environment-detection.md](docs/environment-detection.md).
67
67
 
68
68
  ### Gulp pipeline
69
69
 
@@ -100,8 +100,8 @@ ES module class. Constructor stores `this.webManager`. `initialize()`:
100
100
 
101
101
  1. Calls `webManager.initialize(window.Configuration)`
102
102
  2. Reads `document.documentElement.dataset.pagePath` + `.assetPath`
103
- 3. Loads (in parallel) `__main_assets__/js/ultimate-jekyll-manager.js` + page-specific modules from both `__main_assets__/js/pages/<path>/index.js` (UJM defaults) AND `__project_assets__/js/pages/<path>/index.js` (consumer overrides)
104
- 4. Sequentially executes loaded modules (stops on first error)
103
+ 3. Loads (in parallel) `__main_assets__/js/ultimate-jekyll-manager.js` + page-specific modules from three layers: `__main_assets__/js/pages/<path>/index.js` (UJM default), `__theme__/pages/<path>/index.js` (active theme), and `__project_assets__/js/pages/<path>/index.js` (consumer). Missing at any layer is a no-op. See [docs/themes.md](docs/themes.md#5-page-specific-js-theme-aware-additive--mirrors-page-css).
104
+ 4. Sequentially executes loaded modules in order **main → theme → project** (stops on first error)
105
105
 
106
106
  Webpack aliases:
107
107
  - `__main_assets__` → UJM's `dist/assets/`
@@ -185,7 +185,7 @@ Deep references live in `docs/`. Treat docs as a first-class deliverable. **When
185
185
 
186
186
  - [docs/test-framework.md](docs/test-framework.md) — three-layer test harness reference (build / page / boot)
187
187
  - [docs/test-boot-layer.md](docs/test-boot-layer.md) — boot layer deep-dive (_site/ discovery, HTTP server, fixture vs consumer)
188
- - [docs/cross-context-helpers.md](docs/cross-context-helpers.md) — `isTesting`/`isDevelopment`/`isProduction`/`getVersion`
188
+ - [docs/environment-detection.md](docs/environment-detection.md) — `isTesting`/`isDevelopment`/`isProduction`/`getVersion`
189
189
  - [docs/jekyll-plugin.md](docs/jekyll-plugin.md) — UJ Powertools gem: filters, tags, page variables (`page.resolved`, `uj_icon`, `uj_hash`, `iftruthy`, etc.)
190
190
  - [docs/audit.md](docs/audit.md) — workflow for fixing issues raised by `gulp/tasks/audit.js`
191
191
 
@@ -197,6 +197,7 @@ Deep references live in `docs/`. Treat docs as a first-class deliverable. **When
197
197
 
198
198
  ### Pages, layouts, content
199
199
 
200
+ - [docs/themes.md](docs/themes.md) — theme system: selection + resolution (SCSS loadPaths, `__theme__`, classy layout fallback), shared vs per-theme layers, authoring a theme inside UJM OR in a consumer project, live validation
200
201
  - [docs/layouts-and-pages.md](docs/layouts-and-pages.md) — page types, layout chain, `asset_path` frontmatter
201
202
  - [docs/images.md](docs/images.md) — `@post/` shortcut for blog post images, BEM admin/post image handling, imagemin pipeline + source-size constraints + `UJ_IMAGEMIN_REWRITE_SOURCES` cleanup flag
202
203
  - [docs/icons.md](docs/icons.md) — Font Awesome conventions, `{% uj_icon %}` vs prerendered icons in JS, size reference
package/README.md CHANGED
@@ -28,6 +28,7 @@
28
28
  * **SEO Optimized**: Ultimate Jekyll is fully SEO optimized.
29
29
  * **Blazingly Fast**: Ultimate Jekyll is blazingly fast.
30
30
  * **NPM & Gulp**: Ultimate Jekyll is fueled by an intuitive incorporation of npm and gulp.
31
+ * **Themes**: Pick a shipped theme with `theme.id` (`classy` or `neobrutalism`), or author your own. New themes inherit all default page layouts automatically and only restyle/override what differs. See [docs/themes.md](docs/themes.md).
31
32
  * **Built-in test framework**: three layers (`build` / `page` / `boot`) — plain Node, headless Chromium tab, headless Chromium against real `_site/` with SW registration verification.
32
33
 
33
34
  ## 🚀 Getting started
@@ -0,0 +1,28 @@
1
+ /**
2
+ * /test — Global (framework default) page CSS — the first layer.
3
+ * Owns the base dot styling (every dot starts RED) and turns the
4
+ * "css-global" dot green to prove this layer loaded.
5
+ */
6
+
7
+ // Base: every layer dot is red until its layer turns it green.
8
+ .layer-row {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 0.6rem;
12
+ font-size: 1.05rem;
13
+ padding: 0.35rem 0;
14
+ }
15
+
16
+ .layer-dot {
17
+ display: inline-block;
18
+ width: 1.1rem;
19
+ height: 1.1rem;
20
+ border-radius: 50%;
21
+ background: #e5484d; // red = layer not loaded
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ // This layer loaded → its own dot goes green.
26
+ .layer-dot[data-layer="css-global"] {
27
+ background: #30a46c; // green
28
+ }
@@ -54,9 +54,10 @@ const performRedirect = () => {
54
54
  }
55
55
  }
56
56
 
57
- // Determine redirect delay
58
- const isDevelopment = config.environment === 'development';
59
- const timeout = isDevelopment ? 3000 : 1;
57
+ // Determine redirect delay — slow in any non-production environment (development OR
58
+ // testing) so the redirect is observable; near-instant in production.
59
+ const isNonProduction = config.environment !== 'production';
60
+ const timeout = isNonProduction ? 3000 : 1;
60
61
 
61
62
  // Build redirect URL
62
63
  let redirectUrl;
@@ -111,7 +112,7 @@ const performRedirect = () => {
111
112
  console.groupEnd();
112
113
 
113
114
  // Show user-friendly message in development
114
- if (isDevelopment) {
115
+ if (config.environment === 'development') {
115
116
  console.log(`[Redirect] Delaying redirect by ${timeout}ms for development mode`);
116
117
  }
117
118
 
@@ -38,7 +38,7 @@ const config = {
38
38
  selectors: {
39
39
  platformButtons: '.platform-btn',
40
40
  platformDownloads: '[data-platform]',
41
- downloadButtons: '.tab-pane[data-platform] .btn-primary:not([type="submit"])',
41
+ downloadButtons: '.tab-pane[data-platform] [data-download]',
42
42
  },
43
43
  };
44
44
 
@@ -17,6 +17,12 @@ export default () => {
17
17
  setupForm();
18
18
  setupRatingButtons();
19
19
 
20
+ // TEMP: expose for manual modal testing — remove before commit
21
+ window.__testReviewModal = () => showReviewModal('https://www.trustpilot.com/review/example.com', {
22
+ positive: 'This product is amazing and saved me hours!',
23
+ comments: 'Highly recommend to anyone.',
24
+ });
25
+
20
26
  // Resolve after initialization
21
27
  return resolve();
22
28
  });
@@ -120,7 +126,7 @@ function showReviewModal(reviewURL, data) {
120
126
  // Extract site name for display
121
127
  try {
122
128
  const siteName = new URL(fullURL).hostname.replace('www.', '');
123
- $link.innerHTML = `${getPrerenderedIcon('arrow-up-right-from-square', 'me-2')} Write a Review on ${webManager.utilities().escapeHTML(siteName)}`;
129
+ $link.innerHTML = `${getPrerenderedIcon('arrow-up-right-from-square', 'me-2')} Post your review on ${webManager.utilities().escapeHTML(siteName)}`;
124
130
  } catch (e) {
125
131
  // Use default text
126
132
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * /test — Global (framework default) page JS — the first layer.
3
+ * Turns the "js-global" dot green to prove this layer loaded + ran.
4
+ */
5
+ export default ({ manager, options }) => {
6
+ const dot = document.querySelector('.layer-dot[data-layer="js-global"]');
7
+ if (dot) {
8
+ dot.style.background = '#30a46c'; // green
9
+ }
10
+ console.log('[test-layer] global JS ran → js-global dot green');
11
+ };
@@ -0,0 +1,50 @@
1
+ # Theme Template
2
+
3
+ A minimal, copy-paste starting point for a **brand-new UJM theme** — whether
4
+ you're adding one inside UJM (`src/assets/themes/<id>/`) or creating one in your
5
+ own consumer project (`<project>/src/assets/themes/<id>/`).
6
+
7
+ > Full guide: [`docs/themes.md`](../../../../docs/themes.md).
8
+
9
+ ## Create a theme from this template
10
+
11
+ 1. **Copy this folder** to your theme id (the `_` prefix excludes this template
12
+ from selection, so rename it):
13
+ - Inside UJM: `src/assets/themes/my-theme/`
14
+ - In a consumer: `<project>/src/assets/themes/my-theme/`
15
+ 2. **Select it** in your consumer's `src/_config.yml`:
16
+ ```yaml
17
+ theme:
18
+ id: "my-theme"
19
+ ```
20
+ 3. **Customize** `_config.scss` (tokens), `css/` (styles), and `_theme.js`
21
+ (behaviors). Restyle Bootstrap's own classes (`.btn`, `.card`, `.navbar`,
22
+ `.form-control`) so the shared layouts pick up your look with no HTML edits.
23
+ 4. **Layouts/includes are inherited automatically.** You do NOT need to copy the
24
+ ~40 page layouts — UJM's build copies any missing layout/include from the
25
+ `classy` theme and rewrites the paths to your theme id. Override a layout only
26
+ when its *markup* (not just CSS) must differ — create
27
+ `src/defaults/dist/_layouts/themes/my-theme/<path>` (in UJM) or
28
+ `src/_layouts/themes/my-theme/<path>` (in a consumer) for just that file.
29
+ A common one: override `frontend/core/base.html` to load your theme's fonts.
30
+
31
+ ## What's here
32
+
33
+ ```
34
+ _template/
35
+ ├── _config.scss ← design tokens (!default) + Bootstrap forward
36
+ ├── _theme.scss ← SCSS entry (config → root → styles → bootstrap overrides)
37
+ ├── _theme.js ← JS entry (Bootstrap UMD + DOM-ready behaviors)
38
+ ├── README.md ← this file
39
+ └── css/
40
+ ├── base/_root.scss ← SCSS → CSS-variable bridge (light/dark)
41
+ └── components/_components.scss ← restyle Bootstrap classes here
42
+ ```
43
+
44
+ ## Principles (see docs/themes.md for the full list)
45
+
46
+ - **Tokens are `!default`** so consumers can override without forking your theme.
47
+ - **Bridge to CSS variables** in `_root.scss` for free dark-mode switching.
48
+ - **Don't duplicate the shared layers** — `core/` CSS (animations, alerts, lazy
49
+ loading) and `bootstrap/overrides` are injected for every theme already.
50
+ - **Namespace your own components** (`.mytheme-*`) to avoid collisions.
@@ -0,0 +1,60 @@
1
+ // <Theme Name> Configuration
2
+ // ALL customizable variables live here with !default so consuming projects can
3
+ // override any of them BEFORE the theme is imported. Keep this file as the
4
+ // single source of truth for your theme's design tokens.
5
+
6
+ // ============================================
7
+ // Bootstrap Color Overrides
8
+ // ============================================
9
+ $primary: #3B82F6 !default;
10
+ $secondary: #6B7280 !default;
11
+ $success: #10B981 !default;
12
+ $info: #06B6D4 !default;
13
+ $warning: #F59E0B !default;
14
+ $danger: #EF4444 !default;
15
+ $light: #F9FAFB !default;
16
+ $dark: #111827 !default;
17
+
18
+ // ============================================
19
+ // Surfaces (light + dark mode)
20
+ // ============================================
21
+ // Define your page/card backgrounds for each mode. _root.scss bridges these
22
+ // into CSS variables so dark-mode switching needs no recompilation.
23
+ $theme-bg-light: #FFFFFF !default;
24
+ $theme-surface-light: #F3F4F6 !default;
25
+ $theme-bg-dark: #0B0B0F !default;
26
+ $theme-surface-dark: #16161C !default;
27
+
28
+ // ============================================
29
+ // Typography
30
+ // ============================================
31
+ $font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !default;
32
+ $headings-font-weight: 700 !default;
33
+
34
+ // ============================================
35
+ // Component sizing
36
+ // ============================================
37
+ // Match Classy's avatar map so the shared layouts/includes render correctly.
38
+ $avatar-sizes: (
39
+ null: 3rem, 2xs: 0.5rem, xs: 1.5rem, sm: 2rem, md: 2.5rem,
40
+ lg: 3.5rem, xl: 5rem, 2xl: 7.5rem, 3xl: 10rem, 4xl: 12.5rem, 5xl: 15rem
41
+ ) !default;
42
+
43
+ // ============================================
44
+ // Forward Bootstrap with our configuration
45
+ // ============================================
46
+ // Pass your overridden tokens into Bootstrap so .btn/.card/.form-control etc.
47
+ // are generated with your colors. Add/remove forwards as your theme needs.
48
+ @forward '../bootstrap/scss/bootstrap.scss' with (
49
+ $primary: $primary,
50
+ $secondary: $secondary,
51
+ $success: $success,
52
+ $info: $info,
53
+ $warning: $warning,
54
+ $danger: $danger,
55
+ $light: $light,
56
+ $dark: $dark,
57
+ $font-family-sans-serif: $font-family-sans-serif,
58
+ $headings-font-weight: $headings-font-weight,
59
+ $enable-negative-margins: true
60
+ );
@@ -1,5 +1,14 @@
1
- // Import the theme entry point
2
- import './js/__INDEX__.js';
1
+ // <Theme Name> JS entry point
2
+ // Loaded at runtime via webpack's __theme__ alias. Exposes Bootstrap globally
3
+ // and runs your theme behaviors once the DOM is ready.
4
+ import bootstrap from '__main_assets__/themes/bootstrap/js/index.umd.js';
5
+ import { ready as domReady } from 'web-manager/modules/dom.js';
3
6
 
4
- // Add any custom code here
5
- // ...
7
+ // Make Bootstrap available globally (used by UJM utilities + components)
8
+ window.bootstrap = bootstrap;
9
+
10
+ // Initialize theme behaviors when the DOM is ready
11
+ domReady().then(() => {
12
+ // Add your theme's initializers here, e.g.:
13
+ // import('./js/navbar-scroll.js').then(m => m.default());
14
+ });
@@ -1,5 +1,17 @@
1
- // Import the theme entry point
2
- @use './scss/__INDEX__.scss' as *;
1
+ // <Theme Name> SCSS entry point
2
+ // Import order matters: config (loads Bootstrap with your tokens) → root vars →
3
+ // your styles → universal Bootstrap overrides (shared across all UJ themes).
3
4
 
4
- // Add any custom code here
5
- // ...
5
+ // Forward config so consuming projects can override your !default tokens
6
+ @forward 'config';
7
+ @use 'config' as *;
8
+
9
+ // CSS custom properties (light/dark bridge)
10
+ @import 'css/base/root';
11
+
12
+ // Your theme's styles (add base/layout/component partials as you grow)
13
+ @import 'css/components/components';
14
+
15
+ // Universal Bootstrap overrides shared by every UJ theme
16
+ // (avatars, color-shades, soft-colors, spacing, etc.). Must come AFTER Bootstrap.
17
+ @import '../bootstrap/overrides';
@@ -0,0 +1,19 @@
1
+ // <Theme Name> — CSS Custom Properties
2
+ // Bridges SCSS config into runtime CSS variables. Components should read these
3
+ // var(--*) tokens (never the raw SCSS values) so dark mode is one override block.
4
+
5
+ :root,
6
+ [data-bs-theme="light"] {
7
+ --bs-body-bg: #{$theme-bg-light};
8
+ --bs-body-bg-rgb: #{red($theme-bg-light)}, #{green($theme-bg-light)}, #{blue($theme-bg-light)};
9
+ --bs-secondary-bg: #{$theme-surface-light};
10
+ // Add your own tokens, e.g.:
11
+ // --theme-card-bg: #{$theme-surface-light};
12
+ }
13
+
14
+ [data-bs-theme="dark"] {
15
+ --bs-body-bg: #{$theme-bg-dark};
16
+ --bs-body-bg-rgb: #{red($theme-bg-dark)}, #{green($theme-bg-dark)}, #{blue($theme-bg-dark)};
17
+ --bs-secondary-bg: #{$theme-surface-dark};
18
+ // --theme-card-bg: #{$theme-surface-dark};
19
+ }
@@ -0,0 +1,23 @@
1
+ // <Theme Name> — Components
2
+ // Restyle Bootstrap's own classes here so all inherited markup (nav, hero,
3
+ // cards, forms, buttons) picks up your look with NO HTML changes. Add files
4
+ // like _buttons.scss / _cards.scss as your theme grows and @import them in
5
+ // _theme.scss. This starter keeps everything in one file to stay minimal.
6
+
7
+ // Example: give buttons your theme's personality
8
+ .btn {
9
+ font-weight: 600;
10
+ // ...your button styling...
11
+ }
12
+
13
+ // Example: cards
14
+ .card {
15
+ background: var(--bs-secondary-bg);
16
+ // ...your card styling...
17
+ }
18
+
19
+ // Example: a theme-namespaced helper for your own page layouts
20
+ .theme-box {
21
+ background: var(--bs-secondary-bg);
22
+ padding: 1.5rem;
23
+ }
@@ -2,21 +2,28 @@
2
2
 
3
3
  ## How to Customize in Your Consuming Project
4
4
 
5
- The Classy theme is designed to be fully customizable. All theme variables use `!default` which means you can override them BEFORE importing the theme.
5
+ The Classy theme is designed to be fully customizable. All theme variables use `!default` which means you can override them BEFORE the theme is imported.
6
+
7
+ > **How the import actually resolves:** Your project's `src/assets/css/main.scss`
8
+ > does `@use 'ultimate-jekyll-manager' as *;`. That entry point `@forward`s the
9
+ > theme, and UJM's SASS `loadPaths` resolve `theme` to whichever
10
+ > `src/assets/themes/<theme.id>/_theme.scss` wins — your project's copy first,
11
+ > then UJM's packaged copy. You never import the theme file by path directly.
12
+ > See [`docs/themes.md`](../../../../docs/themes.md) for the full mechanism.
6
13
 
7
14
  ### Example: Customizing Colors in Your Project
8
15
 
9
16
  In your consuming project's `src/assets/css/main.scss`:
10
17
 
11
18
  ```scss
12
- // 1. Override Classy theme variables BEFORE importing the theme
13
- $primary: #FF0000; // Change primary color to red
19
+ // 1. Override Classy theme variables FIRST (they are !default, so yours win)
20
+ $primary: #FF0000; // Change primary color to red
14
21
  $classy-bg-light: #F5F5F5; // Change light mode background
15
- $classy-bg-dark: #1A1A1A; // Change dark mode background
22
+ $classy-bg-dark: #1A1A1A; // Change dark mode background
16
23
  $font-family-sans-serif: 'Inter', sans-serif; // Change font
17
24
 
18
- // 2. Now import the Classy theme - it will use YOUR values
19
- @import '~ultimate-jekyll-manager/src/assets/themes/classy/theme';
25
+ // 2. Import the framework it loads the theme using YOUR values above
26
+ @use 'ultimate-jekyll-manager' as *;
20
27
 
21
28
  // 3. Add your custom styles below
22
29
  .my-custom-class {
@@ -24,6 +31,11 @@ $font-family-sans-serif: 'Inter', sans-serif; // Change font
24
31
  }
25
32
  ```
26
33
 
34
+ To customize beyond variables — change actual component styles or markup —
35
+ **shadow the theme**: create `src/assets/themes/classy/` in your project. UJM's
36
+ loadPaths resolve your copy before the packaged one. (To build a wholly new
37
+ look, author a new theme instead — see `docs/themes.md`.)
38
+
27
39
  ## Available Customizable Variables
28
40
 
29
41
  See `_config.scss` for the full list of variables you can override: