ultimate-jekyll-manager 1.4.2 → 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 +29 -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/core/auth.js +24 -39
- 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/_alternatives/example-competitor.md +6 -6
- package/dist/defaults/dist/_includes/admin/sections/sidebar.json +2 -2
- package/dist/defaults/dist/_includes/core/head.html +17 -0
- package/dist/defaults/dist/_includes/themes/classy/backend/sections/topbar.html +1 -1
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +9 -6
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +13 -13
- package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +1 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +1 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +5 -5
- package/dist/defaults/dist/_layouts/blueprint/auth/oauth2.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/backend/pages/dashboard/index.html +12 -12
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html +5 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +4 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/index.html +2 -2
- package/dist/defaults/dist/_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/_updates/v0.0.1.md +3 -0
- package/dist/defaults/dist/pages/test/account/dashboard.html +1 -1
- package/dist/defaults/dist/pages/test/libraries/ads.html +9 -9
- package/dist/defaults/dist/pages/test/libraries/bootstrap.html +6 -6
- package/dist/defaults/dist/pages/test/libraries/firestore.html +1 -1
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +2 -2
- package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
- package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +8 -8
- package/dist/defaults/dist/sitemap.html +2 -2
- 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/imagemin.js +30 -5
- 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/attach-log-file.js +24 -16
- 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,35 @@ 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
|
+
|
|
31
|
+
---
|
|
32
|
+
## [1.4.3] - 2026-05-28
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Imagemin: uppercase-extension images (e.g. `IMG_3119.JPG`) now build end to end.** v1.4.2 made the glob case-insensitive so the file was discovered, but `gulp-responsive-modern`'s `lib/format.js` does a case-sensitive `switch` on `path.extname()` and returns the string `'unsupported'` for `.JPG`, which then crashes `sharp.toFormat()`. [src/gulp/tasks/imagemin.js](src/gulp/tasks/imagemin.js) now pipes each file through an in-stream `Transform` that lowercases the extension on the Vinyl path before the responsive plugin sees it (the on-disk source is left untouched).
|
|
37
|
+
- **Log files no longer truncate before the crash that caused them.** [src/utils/attach-log-file.js](src/utils/attach-log-file.js) switched from `fs.createWriteStream` (async-buffered) to synchronous `fs.writeSync` against an open fd. The buffered stream dropped its tail when a gulp task threw and the process exited — so the lines describing the failure never reached `logs/build.log`. Synchronous writes guarantee the full error + stack survive an immediate exit.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Auth: signup-consent gating now keys off the user doc's `flags.signupProcessed` instead of a time window.** [src/assets/js/core/auth.js](src/assets/js/core/auth.js) drops the `SIGNUP_MAX_AGE` (5-minute) heuristic and the client-only `localStorage` flag. `sendUserSignupMetadata` fires whenever the doc shows signup unprocessed (the server is idempotent), and the consent guard only signs a user out once signup has actually been processed — removing the risk of locking users out on a transient metadata-send failure.
|
|
42
|
+
- **Footer language dropdown always renders.** No longer gated on `site.translation.enabled`; falls back to `site.translation.default` (or `"en"`) when no extra languages are configured. [src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html](src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html)
|
|
43
|
+
- **Sentence-case copy normalization** across default pages (pricing, alternatives, admin/test pages, sitemap section labels): "API access", "Flash sale", "Root pages", "…and more:" etc.
|
|
44
|
+
- **Updates feed:** the `v0.0.1` sample entry is marked `draft: true` so it's hidden from the listing and sitemap (dev-only).
|
|
45
|
+
|
|
17
46
|
---
|
|
18
47
|
## [1.4.2] - 2026-05-27
|
|
19
48
|
|
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
2
2
|
import webManager from 'web-manager';
|
|
3
3
|
|
|
4
|
-
// Constants
|
|
5
|
-
const SIGNUP_MAX_AGE = 5 * 60 * 1000;
|
|
6
|
-
|
|
7
4
|
// Enforce page-load consent guard. When true, any authenticated user whose doc has
|
|
8
5
|
// consent.legal.status !== 'granted' is silently signed out. Keep FALSE until the
|
|
9
6
|
// legacy user migration runs (sets all existing docs to status='granted',
|
|
@@ -81,7 +78,7 @@ export default function () {
|
|
|
81
78
|
// by the on-create auth event). sendUserSignupMetadata is what flips it
|
|
82
79
|
// to 'granted' with the captured consent payload. If we gate first, every
|
|
83
80
|
// fresh signup would be signed out before consent ever lands.
|
|
84
|
-
await sendUserSignupMetadata(
|
|
81
|
+
await sendUserSignupMetadata(state.account);
|
|
85
82
|
|
|
86
83
|
// Consent guard: if the user is authenticated but their account doc shows
|
|
87
84
|
// no legal consent on record, they're an orphan from a reversed Google signup
|
|
@@ -89,16 +86,15 @@ export default function () {
|
|
|
89
86
|
// user knows what happened.
|
|
90
87
|
//
|
|
91
88
|
// Gated by ENFORCE_CONSENT_GUARD (off until the legacy-user migration runs).
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// (
|
|
95
|
-
// out
|
|
96
|
-
//
|
|
89
|
+
// Only fires once signup has been processed — sendUserSignupMetadata above is what
|
|
90
|
+
// writes consent, and it runs whenever flags.signupProcessed is false. If signup
|
|
91
|
+
// hasn't been processed yet (or just failed and will retry next load), we must NOT
|
|
92
|
+
// sign the user out; a processed doc with no legal consent is a genuine orphan
|
|
93
|
+
// (e.g. a reversed Google signup that failed to delete cleanly).
|
|
97
94
|
if (ENFORCE_CONSENT_GUARD) {
|
|
98
|
-
const
|
|
99
|
-
const isFreshAccount = accountAge < SIGNUP_MAX_AGE;
|
|
95
|
+
const signupProcessed = state.account?.flags?.signupProcessed === true;
|
|
100
96
|
const legalStatus = state.account?.consent?.legal?.status;
|
|
101
|
-
if (
|
|
97
|
+
if (signupProcessed && legalStatus && legalStatus !== 'granted') {
|
|
102
98
|
console.warn('[Auth] Signing out user with no legal consent on record');
|
|
103
99
|
await webManager.auth().signOut();
|
|
104
100
|
webManager.utilities().showNotification(
|
|
@@ -260,7 +256,7 @@ function setAnalyticsUserId(user) {
|
|
|
260
256
|
}
|
|
261
257
|
|
|
262
258
|
// Send user metadata to server (affiliate, UTM params, etc.)
|
|
263
|
-
async function sendUserSignupMetadata(
|
|
259
|
+
async function sendUserSignupMetadata(account) {
|
|
264
260
|
try {
|
|
265
261
|
// Skip on auth pages to avoid blocking redirect (metadata will be sent on destination page)
|
|
266
262
|
const pagePath = document.documentElement.getAttribute('data-page-path');
|
|
@@ -269,20 +265,17 @@ async function sendUserSignupMetadata(user) {
|
|
|
269
265
|
return;
|
|
270
266
|
}
|
|
271
267
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
// The user doc's flags.signupProcessed is the single source of truth. We have the full
|
|
269
|
+
// account doc on every page load, so gate on it directly — no account-age window, no
|
|
270
|
+
// client-only localStorage flag. Fire whenever the doc shows signup is unprocessed; the
|
|
271
|
+
// server is idempotent and rejects if it was already processed.
|
|
272
|
+
const signupProcessed = account?.flags?.signupProcessed === true;
|
|
275
273
|
|
|
276
274
|
/* @dev-only:start */
|
|
277
|
-
|
|
278
|
-
// Log account age for debugging
|
|
279
|
-
const ageInMinutes = Math.floor(accountAge / 1000 / 60);
|
|
280
|
-
console.log('[Auth] Account age:', ageInMinutes, 'minutes, signupProcessed:', signupProcessed);
|
|
281
|
-
}
|
|
275
|
+
console.log('[Auth] signupProcessed:', signupProcessed);
|
|
282
276
|
/* @dev-only:end */
|
|
283
277
|
|
|
284
|
-
|
|
285
|
-
if (accountAge >= SIGNUP_MAX_AGE || signupProcessed) {
|
|
278
|
+
if (signupProcessed) {
|
|
286
279
|
return;
|
|
287
280
|
}
|
|
288
281
|
|
|
@@ -312,27 +305,19 @@ async function sendUserSignupMetadata(user) {
|
|
|
312
305
|
body: payload,
|
|
313
306
|
});
|
|
314
307
|
|
|
315
|
-
// Log
|
|
308
|
+
// Log — the server set flags.signupProcessed on the doc, so the next page load's
|
|
309
|
+
// state.account reflects it and this won't fire again. No client-side flag needed.
|
|
316
310
|
console.log('[Auth] User metadata sent successfully:', response);
|
|
317
|
-
|
|
318
|
-
// Mark signup as sent for this user (keep the attribution data for reference)
|
|
319
|
-
webManager.storage().set('flags.signupProcessed', user.uid);
|
|
320
311
|
} catch (error) {
|
|
321
312
|
console.error('[Auth] Error sending user metadata:', error);
|
|
322
|
-
// Don't throw - we don't want to block the signup flow
|
|
313
|
+
// Don't throw - we don't want to block the signup flow. The doc still shows
|
|
314
|
+
// signupProcessed=false, so a refresh / next page load retries automatically.
|
|
323
315
|
|
|
324
316
|
/* @dev-only:start */
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const minutes = Math.floor(msRemaining / 1000 / 60);
|
|
330
|
-
const seconds = Math.floor((msRemaining / 1000) % 60);
|
|
331
|
-
webManager.utilities().showNotification(
|
|
332
|
-
`[DEV] Failed to send signup metadata. User will be signed out by consent guard at ${signoutAt} (in ${minutes}m ${seconds}s) unless retried.`,
|
|
333
|
-
{ type: 'warning', timeout: 0 }
|
|
334
|
-
);
|
|
335
|
-
}
|
|
317
|
+
webManager.utilities().showNotification(
|
|
318
|
+
`[DEV] Failed to send signup metadata. Will retry on next page load (flags.signupProcessed is still false).`,
|
|
319
|
+
{ type: 'warning', timeout: 0 }
|
|
320
|
+
);
|
|
336
321
|
/* @dev-only:end */
|
|
337
322
|
}
|
|
338
323
|
}
|
|
@@ -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
|
+
);
|