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.
- package/CHANGELOG.md +14 -0
- package/CLAUDE-ATTRIBUTION.md +215 -0
- package/CLAUDE.md +7 -6
- package/README.md +1 -0
- package/dist/assets/css/pages/test/libraries/layers/index.scss +28 -0
- package/dist/assets/js/modules/redirect.js +5 -4
- package/dist/assets/js/pages/download/index.js +1 -1
- package/dist/assets/js/pages/feedback/index.js +7 -1
- package/dist/assets/js/pages/test/libraries/layers/index.js +11 -0
- package/dist/assets/themes/_template/README.md +50 -0
- package/dist/assets/themes/_template/_config.scss +60 -0
- package/dist/assets/themes/_template/_theme.js +13 -4
- package/dist/assets/themes/_template/_theme.scss +16 -4
- package/dist/assets/themes/_template/css/base/_root.scss +19 -0
- package/dist/assets/themes/_template/css/components/_components.scss +23 -0
- package/dist/assets/themes/classy/README.md +18 -6
- package/dist/assets/themes/neobrutalism/README.md +98 -0
- package/dist/assets/themes/neobrutalism/_config.scss +139 -0
- package/dist/assets/themes/neobrutalism/_theme.js +27 -0
- package/dist/assets/themes/neobrutalism/_theme.scss +33 -0
- package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
- package/dist/assets/themes/neobrutalism/css/base/_root.scss +80 -0
- package/dist/assets/themes/neobrutalism/css/base/_typography.scss +77 -0
- package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +25 -0
- package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
- package/dist/assets/themes/neobrutalism/css/components/_cards.scss +69 -0
- package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
- package/dist/assets/themes/neobrutalism/css/components/_infinite-scroll.scss +94 -0
- package/dist/assets/themes/neobrutalism/css/layout/_general.scss +200 -0
- package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +153 -0
- package/dist/assets/themes/neobrutalism/js/initialize-tooltips.js +20 -0
- package/dist/assets/themes/neobrutalism/js/navbar-scroll.js +29 -0
- package/dist/assets/themes/neobrutalism/pages/index.scss +227 -0
- package/dist/assets/themes/neobrutalism/pages/pricing/index.scss +267 -0
- package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.js +9 -0
- package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.scss +7 -0
- package/dist/build.js +2 -5
- package/dist/commands/install.js +1 -1
- package/dist/commands/setup.js +41 -0
- package/dist/defaults/CLAUDE.md +5 -1
- package/dist/defaults/dist/_includes/core/head.html +17 -0
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +2 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/core/base.html +31 -0
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/index.html +345 -0
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +483 -0
- package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
- package/dist/defaults/src/_config.yml +2 -0
- package/dist/defaults/test/_init.js +10 -0
- package/dist/gulp/tasks/defaults.js +8 -0
- package/dist/gulp/tasks/sass.js +43 -2
- package/dist/gulp/tasks/translation.js +11 -0
- package/dist/gulp/tasks/utils/manage-test-layers.js +97 -0
- package/dist/index.js +30 -4
- package/dist/test/runner.js +62 -0
- package/dist/test/suites/build/manager.test.js +11 -4
- package/dist/test/suites/build/mode-helpers.test.js +54 -2
- package/dist/utils/mode-helpers.js +65 -40
- package/docs/assets.md +6 -1
- package/docs/environment-detection.md +85 -0
- package/docs/test-framework.md +48 -3
- package/docs/themes.md +451 -0
- package/package.json +2 -1
- 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;
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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
|
-
|
|
59
|
-
const
|
|
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 (
|
|
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]
|
|
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')}
|
|
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
|
-
//
|
|
2
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
13
|
-
$primary: #FF0000;
|
|
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;
|
|
22
|
+
$classy-bg-dark: #1A1A1A; // Change dark mode background
|
|
16
23
|
$font-family-sans-serif: 'Inter', sans-serif; // Change font
|
|
17
24
|
|
|
18
|
-
// 2.
|
|
19
|
-
@
|
|
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:
|