ultimate-jekyll-manager 1.2.0 → 1.2.2

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.
@@ -0,0 +1,457 @@
1
+ # JavaScript Libraries
2
+
3
+ UJM ships two layers of frontend JS infrastructure:
4
+
5
+ 1. **WebManager** — the singleton site-management library (auth, utilities, sentry, dom).
6
+ 2. **Ultimate Jekyll Libraries** at `src/assets/js/libs/` — helper modules importable via `__main_assets__/js/libs/<name>.js`.
7
+
8
+ ## WebManager
9
+
10
+ Custom library for site management functionality. **It's a singleton** — import it directly from any file:
11
+
12
+ ```javascript
13
+ import webManager from 'web-manager';
14
+ ```
15
+
16
+ This returns the same initialized instance everywhere. Do NOT pass it via params, store in module-level variables, or create new instances.
17
+
18
+ **Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-Works/web-manager/README.md`
19
+
20
+ **Available Utilities:**
21
+ - `webManager.auth()` — Authentication management
22
+ - `webManager.utilities()` — Utility functions (escapeHTML, clipboardCopy, etc.)
23
+ - `webManager.sentry()` — Error tracking
24
+ - `webManager.dom()` — DOM manipulation
25
+ - `webManager.utilities().escapeHTML(text)` — **XSS prevention** — use this instead of writing your own escape function. See [docs/xss-prevention.md](xss-prevention.md).
26
+
27
+ **Important:** Always check the source code or README before assuming a function exists. Do not guess at API methods.
28
+
29
+ ### Subscription Resolution
30
+
31
+ Use `webManager.auth().resolveSubscription(account)` to derive calculated subscription state. This is the **single source of truth** for determining a user's effective plan — do NOT manually check `subscription.status`, `trial.claimed`, or `cancellation.pending` separately.
32
+
33
+ ```javascript
34
+ const resolved = webManager.auth().resolveSubscription(account);
35
+ // Returns: { plan, active, trialing, cancelling }
36
+ ```
37
+
38
+ | Field | Description |
39
+ |-------|-------------|
40
+ | `plan` | Effective plan ID right now (`'basic'` if cancelled/suspended) |
41
+ | `active` | Has active access (active, trialing, or cancelling) |
42
+ | `trialing` | In active trial |
43
+ | `cancelling` | Cancellation pending |
44
+
45
+ Raw subscription data (product.id, status, trial, cancellation) is on `account.subscription` directly — `resolveSubscription()` returns only the calculated/derived fields.
46
+
47
+ The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
48
+
49
+ ## Ultimate Jekyll Libraries
50
+
51
+ Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
52
+
53
+ ### Prerendered Icons Library
54
+
55
+ Provides access to icons defined in page frontmatter and rendered server-side. See [docs/icons.md](icons.md) for when/why to use prerendered icons.
56
+
57
+ **Import:**
58
+
59
+ ```javascript
60
+ import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
61
+ ```
62
+
63
+ **Usage:**
64
+
65
+ ```javascript
66
+ // With classes (drop-in replacement for uj_icon)
67
+ getPrerenderedIcon('apple', 'fa-md me-2');
68
+
69
+ // Without classes (no size class)
70
+ getPrerenderedIcon('apple');
71
+ ```
72
+
73
+ **Reference:** `src/assets/js/libs/prerendered-icons.js`
74
+
75
+ ### Authorized Fetch Library
76
+
77
+ Simplifies authenticated API requests by automatically adding Firebase authentication tokens via Authorization Bearer header.
78
+
79
+ **Import:**
80
+
81
+ ```javascript
82
+ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
83
+ ```
84
+
85
+ **Usage:**
86
+
87
+ ```javascript
88
+ const response = await authorizedFetch(url, options);
89
+ ```
90
+
91
+ **Key Benefits:**
92
+ - No need to manually call `webManager.auth().getIdToken()`
93
+ - Automatic token injection as Authorization Bearer header
94
+ - Centralized authentication handling
95
+ - Automatic usage sync: extracts `bm-properties` header from every response and updates `webManager.bindings()` with fresh usage data under the `usage` key
96
+
97
+ **Options pass-through:** All `wonderful-fetch` options (`response`, `output`, `body`, `timeout`, etc.) are passed through untouched. Internally, `authorizedFetch` uses `output: 'complete'` to read response headers, then returns only the body by default. If the caller passes `output: 'complete'`, they get the full `{ status, headers, body }` response.
98
+
99
+ **Automatic Usage Binding Sync:**
100
+
101
+ After every successful response, `authorizedFetch` reads the `bm-properties` header and updates the `usage` bindings key:
102
+
103
+ ```javascript
104
+ // After an API call, bindings are automatically updated:
105
+ // usage.credits = { monthly: 5, daily: 2, limit: 100 }
106
+ ```
107
+
108
+ This means any `data-wm-bind` elements bound to `usage.*` paths are automatically kept in sync without any manual work. See "Usage Bindings" below.
109
+
110
+ **⚠️ IMPORTANT: Auth State Requirement**
111
+
112
+ `authorizedFetch` requires Firebase Auth to have determined the current user's authentication state before being called. On fresh page loads (e.g., OAuth callback pages, deep links), Firebase Auth needs time to restore the session from IndexedDB/localStorage.
113
+
114
+ **If called before auth state is determined, it will warn: `"No authenticated user found"`**
115
+
116
+ **Solution:** Wait for auth state before calling `authorizedFetch`:
117
+
118
+ ```javascript
119
+ // Wait for auth state to be determined (fires once auth is known)
120
+ webManager.auth().listen({ once: true }, async () => {
121
+ // Now safe to use authorizedFetch
122
+ const response = await authorizedFetch(url, options);
123
+ });
124
+ ```
125
+
126
+ **When this matters:**
127
+ - Pages that load and immediately need to make authenticated API calls
128
+ - OAuth callback pages (user returns from external auth provider)
129
+ - Deep links that require authenticated requests on load
130
+
131
+ **When NOT needed:**
132
+ - User-triggered actions (button clicks, form submissions) — by then auth state is always determined
133
+ - Pages that wait for user interaction before making API calls
134
+
135
+ **Reference:** `src/assets/js/libs/authorized-fetch.js`
136
+
137
+ ### Usage Bindings
138
+
139
+ Usage data is available in the `usage` bindings key. It is populated from two sources:
140
+
141
+ 1. **On page load (auth settle):** `web-manager` reads `account.usage` from Firestore and resolves plan limits from `config.payment.plans`, then sets `usage` bindings with the merged data.
142
+ 2. **After API calls:** `authorizedFetch` reads the `bm-properties` response header and merges fresh usage counters + limits into the existing `usage` bindings.
143
+
144
+ **Bindings structure:**
145
+
146
+ ```javascript
147
+ // usage.credits = { monthly: 5, daily: 2, limit: 100 }
148
+ // usage.requests = { monthly: 20, limit: 500 }
149
+ ```
150
+
151
+ **HTML usage:**
152
+
153
+ ```html
154
+ <!-- Show usage counter: "5/100" -->
155
+ <span data-wm-bind="@show usage.credits">
156
+ <span data-wm-bind="usage.credits.monthly">–</span>/<span data-wm-bind="usage.credits.limit">–</span>
157
+ </span>
158
+ ```
159
+
160
+ **Config requirement:** Plan limits must be defined in `_config.yml` under `web_manager.payment.plans`:
161
+
162
+ ```yaml
163
+ web_manager:
164
+ payment:
165
+ plans:
166
+ - id: basic
167
+ limits:
168
+ credits: 100
169
+ - id: premium
170
+ limits:
171
+ credits: 500
172
+ ```
173
+
174
+ ### Payment Config Library
175
+
176
+ Reads payment configuration (products, processors, prices, limits) from `webManager.config.payment` — populated from `_config.yml` at build time. **Do NOT fetch `/backend-manager/brand` to get payment data.** It's already available instantly via this library.
177
+
178
+ **Import:**
179
+
180
+ ```javascript
181
+ import { getPaymentConfig, getProcessors, getProducts, getProductById, getProductLimits, getCurrency } from '__main_assets__/js/libs/payment-config.js';
182
+ ```
183
+
184
+ **Usage:**
185
+
186
+ ```javascript
187
+ // Get all products
188
+ const products = getProducts();
189
+
190
+ // Find a specific product
191
+ const product = getProductById('plus');
192
+
193
+ // Get product limits
194
+ const limits = getProductLimits('plus'); // { credits: 500, agents: 3, ... }
195
+
196
+ // Get processors (stripe, paypal, etc.)
197
+ const processors = getProcessors();
198
+ ```
199
+
200
+ **Config location in `_config.yml`:**
201
+
202
+ ```yaml
203
+ web_manager:
204
+ payment:
205
+ processors:
206
+ stripe:
207
+ publishableKey: pk_live_...
208
+ paypal:
209
+ clientId: ...
210
+ products:
211
+ - id: basic
212
+ name: Basic
213
+ limits:
214
+ credits: 100
215
+ - id: plus
216
+ name: Plus
217
+ limits:
218
+ credits: 500
219
+ prices:
220
+ monthly: 19
221
+ annually: 190
222
+ ```
223
+
224
+ **How it works:** The `foot.html` Configuration injection serializes all `web_manager` properties into `window.Configuration`, which `webManager.initialize()` stores in `webManager.config`. The payment config is available immediately — no API call needed.
225
+
226
+ **When to still use the brand API:**
227
+ - `oauth2` provider configuration (used by the connections section on the account page)
228
+ - Any data that is NOT in `_config.yml` and only exists server-side
229
+
230
+ **Reference:** `src/assets/js/libs/payment-config.js`
231
+
232
+ ### Pricing Page: Config-Resolved Values
233
+
234
+ The pricing layout automatically resolves prices and feature limits from `_config.yml` when not explicitly set in frontmatter. This means consuming projects can define ONLY display metadata (name, tagline, icon, features list) and let prices/limits come from the single source of truth.
235
+
236
+ **Resolution order (frontmatter wins):**
237
+ 1. `plan.pricing.monthly` / `plan.pricing.annually` from page frontmatter
238
+ 2. `site.web_manager.payment.products[matching_id].prices.monthly` / `.annually` from config
239
+ 3. `0` (default)
240
+
241
+ **Feature value resolution:**
242
+ 1. `feature.value` from page frontmatter
243
+ 2. `site.web_manager.payment.products[matching_id].limits[feature.id]` from config (with `-1` → `"Unlimited"`)
244
+
245
+ **Example: Minimal pricing.md (prices/limits come from config):**
246
+
247
+ ```yaml
248
+ ---
249
+ layout: blueprint/pricing
250
+ permalink: /pricing
251
+
252
+ pricing:
253
+ plans:
254
+ - id: "basic"
255
+ name: "Basic"
256
+ tagline: "best for getting started"
257
+ url: "/dashboard"
258
+ features:
259
+ - id: "credits"
260
+ name: "Credits"
261
+ icon: "sparkles"
262
+ - id: "agents"
263
+ name: "Agents"
264
+ icon: "robot"
265
+ - id: "plus"
266
+ name: "Plus"
267
+ tagline: "best for small websites"
268
+ features:
269
+ - id: "credits"
270
+ name: "Credits"
271
+ icon: "sparkles"
272
+ - id: "agents"
273
+ name: "Agents"
274
+ icon: "robot"
275
+ ---
276
+ ```
277
+
278
+ In this example, `credits` value of 100 and price of $19/mo come from `_config.yml`'s `web_manager.payment.products` — no hardcoding needed.
279
+
280
+ ### FormManager Library
281
+
282
+ Lightweight form state management library with built-in validation, state machine, and event system. See also [docs/page-loading.md → Form Protection Standards](page-loading.md#form-protection-standards) for the form-protection layering rules.
283
+
284
+ **Import:**
285
+
286
+ ```javascript
287
+ import { FormManager } from '__main_assets__/js/libs/form-manager.js';
288
+ ```
289
+
290
+ **Basic Usage:**
291
+
292
+ ```javascript
293
+ const formManager = new FormManager('#my-form', options);
294
+
295
+ formManager.on('submit', async ({ data, $submitButton }) => {
296
+ const response = await fetch('/api', { body: JSON.stringify(data) });
297
+ if (!response.ok) throw new Error('Failed');
298
+ formManager.showSuccess('Form submitted!');
299
+ });
300
+ ```
301
+
302
+ **State Machine:**
303
+
304
+ ```
305
+ initializing → ready ⇄ submitting → ready (or submitted)
306
+ ```
307
+
308
+ **Configuration Options:**
309
+
310
+ ```javascript
311
+ {
312
+ autoReady: true, // Auto-transition to initialState when DOM ready
313
+ initialState: 'ready', // State after autoReady fires
314
+ allowResubmit: true, // Allow resubmission after success (false = 'submitted' state)
315
+ resetOnSuccess: false, // Clear form fields after successful submission
316
+ warnOnUnsavedChanges: true, // Warn user before leaving page with unsaved changes
317
+ submittingText: 'Processing...', // Text shown on submit button during submission
318
+ submittedText: 'Processed!', // Text shown on submit button after success (when allowResubmit: false)
319
+ inputGroup: null // Filter getData() by data-input-group attribute (null = all fields)
320
+ }
321
+ ```
322
+
323
+ **Events:**
324
+
325
+ | Event | Payload | Description |
326
+ |-------|---------|-------------|
327
+ | `submit` | `{ data, $submitButton }` | Form submission (throw error to show failure) |
328
+ | `validation` | `{ data, setError }` | Custom validation before submit |
329
+ | `change` | `{ field, name, value, data }` | Field value changed |
330
+ | `statechange` | `{ state, previousState }` | State transition |
331
+ | `honeypot` | `{ data }` | Honeypot triggered (for spam tracking) |
332
+
333
+ **Validation System:**
334
+
335
+ FormManager runs validation automatically before `submit`:
336
+ 1. **HTML5 validation** — Checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type="email"`, `type="url"`
337
+ 2. **Custom validation** — Use `validation` event for business logic
338
+
339
+ ```javascript
340
+ fm.on('validation', ({ data, setError }) => {
341
+ if (data.age && parseInt(data.age) < 18) {
342
+ setError('age', 'You must be 18 or older');
343
+ }
344
+ });
345
+ ```
346
+
347
+ Errors display with Bootstrap's `is-invalid` class and `.invalid-feedback` elements.
348
+
349
+ **Autofocus:**
350
+
351
+ When the form transitions to `ready` state, FormManager automatically focuses the field with the `autofocus` attribute (if present and not disabled).
352
+
353
+ **Methods:**
354
+
355
+ | Method | Description |
356
+ |--------|-------------|
357
+ | `on(event, callback)` | Register event listener (chainable) |
358
+ | `ready()` | Transition to ready state |
359
+ | `getData()` | Get form data as nested object (supports dot notation, respects input group filter) |
360
+ | `setData(obj)` | Set form values from nested object |
361
+ | `setInputGroup(group)` | Set input group filter (string, array, or null) |
362
+ | `getInputGroup()` | Get current input group filter |
363
+ | `showSuccess(msg)` | Show success notification |
364
+ | `showError(msg)` | Show error notification |
365
+ | `submit()` | Programmatically trigger form submission (fires native submit event) |
366
+ | `reset()` | Reset form and go to ready state |
367
+ | `isDirty()` | Check if form has unsaved changes |
368
+ | `setDirty(bool)` | Set dirty state |
369
+ | `clearFieldErrors()` | Clear all field validation errors |
370
+ | `throwFieldErrors({ field: msg })` | Set and display field errors, throw error |
371
+
372
+ **Nested Field Names (Dot Notation):**
373
+
374
+ Use dot notation in field names for nested data:
375
+
376
+ ```html
377
+ <input name="user.address.city" value="NYC">
378
+ ```
379
+
380
+ Results in:
381
+
382
+ ```javascript
383
+ { user: { address: { city: 'NYC' } } }
384
+ ```
385
+
386
+ **Input Groups:**
387
+
388
+ Filter `getData()` to only return fields matching a specific group. Fields without `data-input-group` are "global" and always included.
389
+
390
+ ```html
391
+ <!-- Global fields (no data-input-group) - always included -->
392
+ <input name="settings.theme" value="dark">
393
+
394
+ <!-- Group-specific fields -->
395
+ <input name="options.url" data-input-group="url" value="https://example.com">
396
+ <input name="options.ssid" data-input-group="wifi" value="MyWiFi">
397
+ <input name="options.password" data-input-group="wifi" value="secret123">
398
+ ```
399
+
400
+ ```javascript
401
+ // Set group filter (accepts string or array)
402
+ formManager.setInputGroup('url'); // Single group
403
+ formManager.setInputGroup(['url', 'wifi']); // Multiple groups
404
+ formManager.setInputGroup(null); // Clear filter (all fields)
405
+
406
+ // Get current filter
407
+ formManager.getInputGroup(); // Returns ['url'] or null
408
+
409
+ // getData() respects the filter
410
+ formManager.setInputGroup('wifi');
411
+ formManager.getData();
412
+ // Returns: { settings: { theme: 'dark' }, options: { ssid: 'MyWiFi', password: 'secret123' } }
413
+ // Note: 'url' field excluded, global 'settings.theme' included
414
+ ```
415
+
416
+ Can also be set via config:
417
+
418
+ ```javascript
419
+ const fm = new FormManager('#form', { inputGroup: 'wifi' });
420
+ ```
421
+
422
+ **Honeypot (Bot Detection):**
423
+
424
+ FormManager automatically rejects submissions if a honeypot field is filled. Honeypot fields are hidden from users but bots fill them automatically.
425
+
426
+ ```html
427
+ <!-- Hidden from users via CSS -->
428
+ <input type="text" name="honey" autocomplete="off" tabindex="-1"
429
+ style="position: absolute; left: -9999px;" aria-hidden="true">
430
+ ```
431
+
432
+ Fields matching `[data-honey]` or `[name="honey"]` are:
433
+ - Excluded from `getData()` output
434
+ - Checked during validation — if filled, submission is rejected with generic error
435
+
436
+ **Checkbox Handling:**
437
+ - **Single checkbox:** Returns `true`/`false`
438
+ - **Checkbox group (same name):** Returns object `{ value1: true, value2: false }`
439
+
440
+ **Multiple Submit Buttons:**
441
+
442
+ Access the clicked button via `$submitButton`:
443
+
444
+ ```html
445
+ <button type="submit" data-action="save">Save</button>
446
+ <button type="submit" data-action="draft">Save Draft</button>
447
+ ```
448
+
449
+ ```javascript
450
+ fm.on('submit', async ({ data, $submitButton }) => {
451
+ const action = $submitButton?.dataset?.action; // 'save' or 'draft'
452
+ });
453
+ ```
454
+
455
+ **Reference:** `src/assets/js/libs/form-manager.js`
456
+ **Test Page:** `src/assets/js/pages/test/libraries/form-manager/index.js`
457
+ **Example:** `src/assets/js/pages/contact/index.js`
@@ -0,0 +1,69 @@
1
+ # UJ Powertools (Jekyll Plugin)
2
+
3
+ Ultimate Jekyll uses the `jekyll-uj-powertools` gem for custom Liquid functionality.
4
+
5
+ **Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-works/jekyll-uj-powertools/README.md`
6
+
7
+ ## Available Features
8
+
9
+ - **Filters:** `uj_strip_ads`, `uj_json_escape`, `uj_title_case`, `uj_content_format`, `uj_hash`
10
+ - **Tags:** `iftruthy`, `iffalsy`, `uj_icon`, `uj_logo`, `uj_image`, `uj_member`, `uj_post`, `uj_readtime`, `uj_social`, `uj_translation_url`, `uj_fake_comments`, `uj_language`
11
+ - **Global Variables:** `site.uj.cache_breaker`
12
+ - **Page Variables:** `page.random_id`, `page.extension`, `page.layout_data`, `page.resolved`
13
+
14
+ **Always check the README before assuming functionality.**
15
+
16
+ ## Key Liquid Functions
17
+
18
+ ### `uj_content_format`
19
+
20
+ Formats content by first liquifying it, then markdownifying it (if markdown file).
21
+
22
+ ### `uj_hash`
23
+
24
+ Returns a deterministic number between 0 and max (exclusive) based on the input string's MD5 hash. Same input always produces the same output.
25
+
26
+ ```liquid
27
+ {{ "some-string" | uj_hash: 1000 }} => 0-999 (stable across builds)
28
+ {{ site.url | uj_hash: 2 }} => 0 or 1
29
+ ```
30
+
31
+ ### `iftruthy` / `iffalsy`
32
+
33
+ Custom tags that check JavaScript truthiness (not null, undefined, or empty string).
34
+
35
+ ```liquid
36
+ {% iftruthy variable %}
37
+ <!-- Content -->
38
+ {% endiftruthy %}
39
+ ```
40
+
41
+ **Limitations:**
42
+ - Does NOT support logical operators
43
+ - Does NOT support `else` statements
44
+ - CAN contain nested sub-statements
45
+
46
+ ### `page.resolved`
47
+
48
+ A deeply merged object containing all site, layout, and page variables. Precedence: page > layout > site. Enables a system of defaults with progressive overrides.
49
+
50
+ ### `uj_icon`
51
+
52
+ Inserts Font Awesome icons. See [docs/icons.md](icons.md) for the full reference (sizes, when to use vs prerendered icons in JS, etc.).
53
+
54
+ ```liquid
55
+ {% uj_icon icon-name, "fa-md" %}
56
+ {% uj_icon "rocket", "fa-3xl" %}
57
+ ```
58
+
59
+ ### `asset_path` Override
60
+
61
+ Override default page-specific CSS/JS path derivation:
62
+
63
+ ```yaml
64
+ ---
65
+ asset_path: blog/post
66
+ ---
67
+ ```
68
+
69
+ Uses `/assets/css/pages/{{ asset_path }}.bundle.css` instead of deriving from `page.canonical.path`. Useful when multiple pages share assets (e.g., all blog posts). See also [docs/layouts-and-pages.md](layouts-and-pages.md).
@@ -0,0 +1,31 @@
1
+ # Layouts and Pages
2
+
3
+ ## Page Types
4
+
5
+ - **One-off pages** (e.g., `/categories`, `/sitemap`) — Create as pages without custom layouts; use existing layouts
6
+ - **Repeating page types** (e.g., blog posts, category pages) — Create a dedicated layout (e.g., `_layouts/category.html`)
7
+
8
+ ## Layout Requirements
9
+
10
+ All layouts and pages must eventually require a theme entry point:
11
+
12
+ ```yaml
13
+ layout: themes/[ site.theme.id ]/frontend/core/base
14
+ ```
15
+
16
+ **Note:** The `[ site.theme.id ]` syntax is correct and allows dynamic theme selection.
17
+
18
+ ## Asset Path Configuration
19
+
20
+ For pages sharing the same assets, use the `asset_path` frontmatter variable:
21
+
22
+ ```yaml
23
+ ---
24
+ # Instead of deriving path from page.canonical.path
25
+ asset_path: categories/category
26
+ ---
27
+ ```
28
+
29
+ **Example:**
30
+ - One-off page: `pages/categories.html` → `src/assets/css/pages/categories/index.scss`
31
+ - Repeating layout: `_layouts/category.html` → `src/assets/css/pages/categories/category.scss` (set `asset_path: categories/category` in layout frontmatter)
@@ -0,0 +1,58 @@
1
+ # Lazy Loading System
2
+
3
+ Ultimate Jekyll uses a custom lazy loading system powered by web-manager.
4
+
5
+ ## Syntax
6
+
7
+ ```html
8
+ data-lazy="@type value"
9
+ ```
10
+
11
+ ## Supported Types
12
+
13
+ ### `@src` — Lazy load src attribute
14
+
15
+ ```html
16
+ <img data-lazy="@src /assets/images/hero.jpg" alt="Hero">
17
+ <iframe data-lazy="@src https://example.com/embed"></iframe>
18
+ ```
19
+
20
+ ### `@srcset` — Lazy load srcset attribute
21
+
22
+ ```html
23
+ <img data-lazy="@srcset /img/small.jpg 480w, /img/large.jpg 1024w">
24
+ ```
25
+
26
+ ### `@bg` — Lazy load background images
27
+
28
+ ```html
29
+ <div data-lazy="@bg /assets/images/background.jpg"></div>
30
+ ```
31
+
32
+ ### `@class` — Lazy add CSS classes
33
+
34
+ ```html
35
+ <div data-lazy="@class animation-fade-in">Content</div>
36
+ ```
37
+
38
+ ### `@html` — Lazy inject HTML content
39
+
40
+ ```html
41
+ <div data-lazy="@html <p>Lazy loaded content</p>"></div>
42
+ ```
43
+
44
+ ### `@script` — Lazy load external scripts
45
+
46
+ ```html
47
+ <div data-lazy='@script {"src": "https://example.com/widget.js", "attributes": {"async": true}}'></div>
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - Automatic cache busting via `buildTime`
53
+ - IntersectionObserver for performance (50px threshold)
54
+ - Loading state CSS classes: `lazy-loading`, `lazy-loaded`, `lazy-error`
55
+ - Intelligent handling of video/audio sources
56
+ - Automatic DOM re-scanning for dynamic elements
57
+
58
+ **Implementation:** `src/assets/js/core/lazy-loading.js`
@@ -0,0 +1,59 @@
1
+ # Local Development
2
+
3
+ The local development server URL is stored in `.temp/_config_browsersync.yml` in the consuming project's root directory. Read this file to determine the correct URL for browsing and testing. By default, use `https://192.168.86.69:4000`.
4
+
5
+ ## Connecting to Local Firebase Emulators
6
+
7
+ Set the `FIREBASE_EMULATOR_CONNECT` environment variable to `true` to connect the frontend to local Firebase services (Auth, Firestore, Functions, etc.):
8
+
9
+ ```bash
10
+ FIREBASE_EMULATOR_CONNECT=true npm start
11
+ ```
12
+
13
+ This value is written to `.temp/_config_browsersync.yml` under `web_manager.env.FIREBASE_EMULATOR_CONNECT` and made available to the frontend at build time.
14
+
15
+ ## PurgeCSS
16
+
17
+ PurgeCSS runs automatically in production builds and can be enabled locally with `UJ_PURGECSS=true`. Consuming projects can add custom safelist patterns via `config/ultimate-jekyll-manager.json` under `sass.purgecss.safelist`:
18
+
19
+ ```json5
20
+ {
21
+ sass: {
22
+ purgecss: {
23
+ safelist: {
24
+ standard: [], // Matches against the full class name
25
+ deep: [], // Matches including child selectors (e.g., pseudo-selectors like :checked)
26
+ greedy: [], // Matches anywhere in the selector string
27
+ keyframes: [], // Preserves @keyframes animations by name
28
+ },
29
+ },
30
+ },
31
+ }
32
+ ```
33
+
34
+ **All entries are regex strings** — each gets converted to `new RegExp(entry)`. This means:
35
+
36
+ | Pattern | Matches | Does NOT match |
37
+ |---------|---------|----------------|
38
+ | `"^dot$"` | `dot` | `dotted`, `polkadot` |
39
+ | `"^chat-"` | `chat-bubble`, `chat-input` | `live-chat` |
40
+ | `"fw-semibold"` | `fw-semibold`, `fw-semibold-custom` | (matches loosely) |
41
+
42
+ **Use `^` and `$` anchors for exact matches.** Without them, the pattern matches any class *containing* the string.
43
+
44
+ **Example:**
45
+
46
+ ```json5
47
+ {
48
+ sass: {
49
+ purgecss: {
50
+ safelist: {
51
+ standard: ["^dot$", "^fw-semibold$", "^chat-"],
52
+ deep: [":focus-within"],
53
+ greedy: ["^chat-"],
54
+ keyframes: ["chat-typing-bounce"],
55
+ },
56
+ },
57
+ },
58
+ }
59
+ ```