<p align="center">
  <a href="https://itwcreativeworks.com">
    <img src="https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/logo/itw-creative-works-brandmark-black-x.svg" width="100px">
  </a>
</p>

<p align="center">
  <img src="https://img.shields.io/github/package-json/v/itw-creative-works/ultimate-jekyll-manager.svg">
  <br>
  <img src="https://img.shields.io/librariesio/release/npm/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/bundlephobia/min/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/codeclimate/maintainability-percentage/itw-creative-works/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/npm/dm/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/node/v/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/website/https/itwcreativeworks.com.svg">
  <img src="https://img.shields.io/github/license/itw-creative-works/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/github/contributors/itw-creative-works/ultimate-jekyll-manager.svg">
  <img src="https://img.shields.io/github/last-commit/itw-creative-works/ultimate-jekyll-manager.svg">
  <br>
  <br>
  <a href="https://itwcreativeworks.com">Site</a> | <a href="https://www.npmjs.com/package/ultimate-jekyll-manager">NPM Module</a> | <a href="https://github.com/itw-creative-works/ultimate-jekyll-manager">GitHub Repo</a>
  <br>
  <br>
  <strong>Ultimate Jekyll</strong> is a template that helps you jumpstart your Jekyll sites and is fueled by an intuitive incorporation of npm, gulp, and is fully SEO optimized and blazingly fast.
</p>

## 🦄 Features
* **SEO Optimized**: Ultimate Jekyll is fully SEO optimized.
* **Blazingly Fast**: Ultimate Jekyll is blazingly fast.
* **NPM & Gulp**: Ultimate Jekyll is fueled by an intuitive incorporation of npm and gulp.
* **Built-in test framework**: three layers (`build` / `page` / `boot`) — plain Node, headless Chromium tab, headless Chromium against real `_site/` with SW registration verification.

## 🚀 Getting started
1. [Create a repo](https://github.com/itw-creative-works/ultimate-jekyll/generate) from the **Ultimate Jekyll** template.
2. Clone the repo to your local machine.
3. Run these commands to get everything setup and sync'd!
```bash
npm start
```

## 🧪 Testing

UJM ships a built-in three-layer test harness. Write tests under `test/<layer>/*.test.js` and run with:

```bash
npx mgr test                   # all layers
npx mgr test --layer build     # plain Node, fast
npx mgr test --layer page      # headless Chromium tab against harness HTML
npx mgr test --layer boot      # headless Chromium against built _site/
```

Test files use Jest-compatible matchers:

```js
// test/build/config.test.js
const Manager = require('ultimate-jekyll-manager/build');

module.exports = {
  layer: 'build',
  description: 'config has brand.id',
  run: async (ctx) => {
    const cfg = Manager.getConfig('project');
    ctx.expect(cfg.brand.id).toBeTruthy();
  },
};
```

Boot tests run against your actually-built `_site/` after `npm run build`:

```js
// test/boot/site.test.js
module.exports = {
  layer: 'boot',
  description: 'home renders + SW registers',
  inspect: async ({ site, page, expect }) => {
    await page.goto(site.baseUrl + '/');
    expect((await page.title()).length).toBeGreaterThan(0);
  },
};
```

Full guide: [docs/test-framework.md](docs/test-framework.md). Boot layer deep-dive: [docs/test-boot-layer.md](docs/test-boot-layer.md).

## 📦 How to sync with the template
1. Simply run `npm start` in Terminal to get all the latest updates from the **Ultimate Jekyll template** and launch your website in the browser.

## 🌎 Publishing your website
1. Change the `url` in `_config.yml` to your domain.
2. Push your changes to GitHub using `npm run dist` in Terminal.

## ⛳️ Flags
* `--browser=false` - Disables the browser from opening when running `npm start`.
```bash
npm start -- --browser=false
```
* `--debug=true` - Enables logging of extra information when running `npm start`.
```bash
npm start -- --debug=true
```
* `--ujPluginDevMode=true` - Enables the development mode for the [Ultimate Jekyll Ruby plugin](https://github.com/itw-creative-works/jekyll-uj-powertools).
```bash
npm start -- --ujPluginDevMode=true
```
* `--profile` - Enables Jekyll build profiling to see how long each phase takes.
```bash
npm start -- --profile
```
* `--all-posts` - Disables the development post limit (15 posts) and builds with all posts. Useful when you need to test with full blog content.
```bash
npm start -- --all-posts
```

### Other ENV variables
```bash
UJ_PURGECSS=true # Enables PurgeCSS to remove unused CSS (normally only happens in production builds)
UJ_IMAGEMIN_CACHE=false # Disables the GitHub-backed imagemin cache (forces local processing)
UJ_IMAGEMIN_REWRITE_SOURCES=true # One-off cleanup: shrinks oversized source images (>4096px) in place. See docs/images.md
```

## Running Specific Tasks
You can run specific tasks using the `npm run gulp` command with the appropriate task name.

Some of these require environment variables to be set and other tasks to be run first.

Here are some examples:

### Run the `audit` task:
```bash
# Run the audit task
npx mgr audit

# Run with a Lighthouse URL (defaults to "/" if not provided)
npx mgr audit -- --lighthouseUrl="/contact"

# Add autoExit to continue developing and testing AFTER the audit
npx mgr audit -- --lighthouseUrl="/contact" --autoExit=false
```

### Run the `translation` task:
```bash
# Test translation with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_TRANSLATION_CACHE=true \
npx mgr translation

# Test with only 1 file
UJ_TRANSLATION_ONLY="index.html" \
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_TRANSLATION_CACHE=true \
npx mgr translation
```

### Run the `imagemin` task:
Test image optimization with GitHub cache in development mode:
```bash
# Test with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_IMAGEMIN_CACHE=true \
npx mgr imagemin

# Or run locally without cache
npx mgr imagemin
```
The imagemin task will:
- Process images from `src/assets/images/**/*.{jpg,jpeg,png}`
- Generate multiple sizes (1024px, 425px) and WebP formats
- Cache processed images in `cache-imagemin` branch (when using GitHub cache)
- Skip already processed images on subsequent runs

**Keep sources reasonably sized.** Source images larger than ~4096px on the longest side can stall `sharp`/`gulp-responsive-modern` in a way gulp can't detect, causing them to silently fail to land in `_site/`. Cap images at the upload step where possible. For one-off cleanup of an existing repo, run with `UJ_IMAGEMIN_REWRITE_SOURCES=true npm run build` — see [docs/images.md](docs/images.md#cleanup-for-existing-oversized-sources-uj_imagemin_rewrite_sources) for details.

<!-- Developing -->
## 🛠 Developing
1. Clone the repo to your local machine.
2. Run these commands
```bash
npm install
npm run prepare:watch
```

### Run the `blogify` task:
Create 12 test blog posts in the `_posts` directory with the `blogify` task. This is useful for testing and development purposes.
```bash
npx mgr blogify
```

## Page Frontmatter
You can add the following frontmatter to your pages to customize their behavior:

### All pages
```yaml
---
# Layout and Internals
layout: themes/[ site.theme.id ]/frontend/core/minimal # The layout to use for the page, usually 'default' or 'page'
permalink: /path/to/page # The URL path for the page, can be relative

# Control the page's meta tags
meta:
  index: true # Set to false to disable indexing by search engines
  title: 'Page Title' # Custom meta title for the page
  description: 'Page description goes here.' # Custom meta description for the page
  breadcrumb: '' # Custom breadcrumb for the page

# Control the page's theme and layout
theme:
  nav:
    enabled: true # Enable theme's nav on the page
  footer:
    enabled: true # Enable theme's footer on the page
  body:
    class: '' # Add custom classes to the body tag
    main:
      class: '' # Add custom classes to the main tag
  head:
    content: '' # Injected at the end of the head tag
  foot:
    content: '' # Injected at the end of the foot tag (inside <body>)
---
```

### Post pages
```yaml
---
# Post pages
post:
  title: "Post Title" # Custom post title for the page
  description: "Post description goes here." # Custom post description for the page
  author: "author-id" # ID of the author from _data/authors.yml
  id: 1689484669 # Unique ID for the post, used for permalink
---
```

### Team Member pages
```yaml
---
# Team Member pages
member:
  id: "member-id" # ID of the team member from _data/team.yml
  name: "Member Name" # Name of the team member
---
```

### Special Class
`uj-signin-btn`: Automatically handles signin (just add `data-provider="google.com"` to the button)
`uj-signup-btn`: Automatically handles signup (just add `data-provider="google.com"` to the button)

`uj-language-dropdown`:
`uj-language-dropdown-item`

### Utility Classes

#### Max-Width Utilities
Ultimate Jekyll includes max-width utility classes based on Bootstrap's breakpoint sizes. These classes constrain an element's maximum width to match Bootstrap's standard responsive breakpoints:

- `.mw-sm` - Sets max-width to 576px
- `.mw-md` - Sets max-width to 768px
- `.mw-lg` - Sets max-width to 992px
- `.mw-xl` - Sets max-width to 1200px
- `.mw-xxl` - Sets max-width to 1400px

**Usage Examples:**
```html
<!-- Constrain a form to medium width -->
<form class="mw-md">
  <!-- Form content stays readable at max 768px wide -->
</form>

<!-- Limit content width for better readability -->
<div class="container mw-lg">
  <!-- Content won't exceed 992px even on larger screens -->
</div>

<!-- Combine with margin utilities for centering -->
<div class="mw-sm mx-auto">
  <!-- Content is max 576px wide and centered -->
</div>
```

These utilities are particularly useful for:
- Improving readability by preventing text from spanning too wide
- Creating consistent content widths across different sections
- Constraining forms, cards, and modals to reasonable sizes
- Maintaining design consistency with Bootstrap's grid system

### HTML Element Attributes

The `<html>` element has data attributes for JavaScript/CSS targeting:

| Attribute | Values |
|-----------|--------|
| `data-theme-id` | Theme ID (e.g., `classy`) |
| `data-theme-target` | `frontend`, `backend`, `docs` |
| `data-bs-theme` | `light`, `dark` |
| `data-page-path` | Page permalink (e.g., `/about`) |
| `data-asset-path` | Custom asset path or empty |
| `data-environment` | `development`, `production` |
| `data-platform` | `windows`, `mac`, `linux`, `ios`, `android`, `chromeos`, `unknown` |
| `data-device` | `mobile` (<768px), `tablet` (768-1199px), `desktop` (>=1200px) |
| `data-runtime` | `web`, `extension`, `electron`, `node` |
| `aria-busy` | `true` (loading), `false` (ready) |

### Appearance Switching

Ultimate Jekyll supports dark/light/system theme switching with user preference persistence.

**JavaScript API:**
```javascript
webManager.uj().appearance.get();        // Returns 'dark', 'light', 'system', or null
webManager.uj().appearance.set('dark');  // Save and apply preference
webManager.uj().appearance.toggle();     // Toggle dark/light
webManager.uj().appearance.cycle();      // Cycle: dark → light → system
```

**HTML Dropdown Example:**
```html
<div class="dropdown">
  <button class="btn dropdown-toggle" data-bs-toggle="dropdown">
    <span data-appearance-icon="light" hidden>{% uj_icon "sun" %}</span>
    <span data-appearance-icon="dark" hidden>{% uj_icon "moon-stars" %}</span>
    <span data-appearance-icon="system" hidden>{% uj_icon "circle-half-stroke" %}</span>
    <span data-appearance-current></span>
  </button>
  <ul class="dropdown-menu">
    <li><a href="#" data-appearance-set="light">Light</a></li>
    <li><a href="#" data-appearance-set="dark">Dark</a></li>
    <li><a href="#" data-appearance-set="system">System</a></li>
  </ul>
</div>
```

### Page Loading Protection System

Ultimate Jekyll includes an automatic protection system that prevents users from clicking buttons before JavaScript is fully loaded, eliminating race conditions and errors.

#### How It Works
1. Pages start with `data-page-loading="true"` on the HTML element
2. Certain buttons are automatically protected from clicks during this state
3. When JavaScript finishes loading, the attribute is removed and buttons become clickable

#### Protected Elements
During page load, these elements are automatically protected:
- All form buttons (`<button>`, `<input type="submit">`, etc.)
- Elements with `.btn` class (Bootstrap buttons)
- Elements with `.btn-action` class (custom action triggers)

#### Using `.btn-action` Class
Add the `.btn-action` class to protect custom elements that trigger important actions:

```html
<!-- These will be protected during page load -->
<a href="/api/delete" class="custom-link btn-action">Delete Item</a>
<div onclick="saveData()" class="btn-action">Save</div>

<!-- Regular navigation links are NOT protected -->
<a href="/about">About Us</a>
<button data-bs-toggle="modal">Show Modal</button>
```

**Use `.btn-action` for:** API calls, form submissions, data modifications, payments, destructive actions
**Don't use for:** Navigation, UI toggles, modals, accordions, harmless interactions

#### Form Protection Standards

All JS-managed forms use a layered protection strategy:

1. **`onsubmit="return false"`** on every `<form>` managed by FormManager — prevents native submission before JS loads
2. **Button initial state** — buttons dependent on async data start `hidden` (revealed by `data-wm-bind`); auth buttons start `disabled` (enabled by FormManager's `ready()`)
3. **FormManager `autoReady`** — use `autoReady: false` when async work happens before form init, call `ready()` explicitly after

**Exception:** Traditional forms with an `action` attribute that intentionally navigate should NOT include `onsubmit="return false"`.

### Ad Units (Verts)

UJ provides ad unit includes that display Google AdSense ads with automatic fallback to in-house promo-server ads when AdSense is blocked or unfilled.

#### AdSense Include (with fallback)
```liquid
{% include /modules/adunits/adsense.html type="in-article" %}
{% include /modules/adunits/adsense.html type="display" vert-size="rectangle" %}
{% include /modules/adunits/adsense.html type="display" vert-size="300" %}
```

| Parameter | Default | Description |
|-----------|---------|-------------|
| `type` | `display` | Ad type: `display`, `in-article`, `in-feed`, `multiplex` |
| `vert-size` | (unconstrained) | Max height preset or pixel value |
| `slot` | From site config | Override the ad slot ID |
| `style` | `""` | Custom inline CSS |

#### Promo Server Include (direct, no AdSense)
```liquid
{% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" %}
{% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" vert-size="banner" %}
```

| Parameter | Default | Description |
|-----------|---------|-------------|
| `vert-id` | `""` | Path to the vert on promo-server |
| `vert-size` | (unconstrained) | Max height preset or pixel value |
| `style` | `""` | Custom inline CSS |

#### Size Presets

| Preset | Max Height | Typical Use |
|--------|-----------|-------------|
| `banner` | 150px | Horizontal banner ads |
| `leaderboard` | 90px | Wide horizontal ads |
| `rectangle` | 250px | Medium rectangle, in-content ads |
| `large-rectangle` | 600px | Large rectangle, sidebar ads |
| `skyscraper` | 600px | Tall sidebar ads |

Raw pixel values also accepted: `vert-size="300"` → 300px max-height. Omit `vert-size` for unconstrained rendering.

### Special Query Parameters

#### Authentication
* `authReturnUrl`: Redirects to this URL after authentication.

#### Testing Parameters

##### Account Page (`/account`)
* `_dev_subscription`: Override subscription data for testing billing states. The product ID is automatically patched to match a real product from the backend. Available values:
  - `_dev_subscription=active`: Active paid subscription
  - `_dev_subscription=trialing`: Free trial in progress
  - `_dev_subscription=suspended`: Payment failed, access revoked
  - `_dev_subscription=cancellation-requested`: Active but cancellation pending
  - `_dev_subscription=cancelled`: Subscription ended
* `_dev_prefill=true`: Adds fake test data for development:
  - Inserts fake referral data in the Referrals section
  - Inserts fake session data in the Security section (active sessions)

##### Checkout Page (`/payment/checkout`)
* `_dev_brandId`: Override the brand ID for testing (e.g., `_dev_brandId=test-app`)
* `_dev_trialEligible`: Force trial eligibility status:
  - `_dev_trialEligible=true`: User is eligible for trial
  - `_dev_trialEligible=false`: User is not eligible for trial
* `_dev_cardProcessor`: Force a specific card payment processor (e.g., `_dev_cardProcessor=stripe` or `_dev_cardProcessor=chargebee`)

## JavaScript API

### Ultimate Jekyll Libraries

Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed in your page modules.

#### Prerendered Icons Library

The prerendered icons library provides access to icons defined in page frontmatter. Icons are rendered server-side for optimal performance.

**Import:**
```javascript
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
```

**Function: `getPrerenderedIcon(iconName, classes)`**

A drop-in replacement for `uj_icon` in JavaScript contexts. The second argument works the same as `uj_icon`'s second argument.

**Parameters:**
- `iconName` (string) - Name of the icon to retrieve (matches `data-icon` attribute in frontmatter)
- `classes` (string, optional) - CSS classes for the `<i>` wrapper (e.g. `"fa-md me-2"`). Without this, the icon has no size class.

**Returns:**
- (string) Icon HTML or empty string if not found

**Example:**
```javascript
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';

// With size + classes (same as {% uj_icon "apple", "fa-xl" %})
$el.innerHTML = getPrerenderedIcon('apple', 'fa-xl');

// In a button (same as {% uj_icon "play", "fa-md me-1" %})
$btn.innerHTML = `${getPrerenderedIcon('play', 'fa-md me-1')} Run Now`;

// Without classes (no size class on the <i> wrapper)
$el.innerHTML = getPrerenderedIcon('apple');
```

**Setup:**
Define icons in your page frontmatter (names only, no classes):
```yaml
---
prerender_icons:
  - name: "apple"
  - name: "android"
  - name: "chrome"
---
```

**Available Icon Sizes (passed as second argument):**
- `fa-2xs` - Extra extra small
- `fa-xs` - Extra small
- `fa-sm` - Small
- `fa-md` - Medium (default base size)
- `fa-lg` - Large
- `fa-xl` - Extra large
- `fa-2xl` - 2x extra large
- `fa-3xl` - 3x extra large
- `fa-4xl` - 4x extra large
- `fa-5xl` - 5x extra large

Icons are automatically rendered in the page HTML and can be retrieved by importing the library function.

#### Authorized Fetch Library

The authorized fetch library simplifies authenticated API requests by automatically adding Firebase authentication tokens.

**Import:**
```javascript
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
```

**Function: `authorizedFetch(url, options)`**

**Parameters:**
- `url` (string) - The API endpoint URL
- `options` (Object) - Request options for wonderful-fetch (method, body, timeout, etc.)

**Returns:**
- (Promise) - The response from the API

**Example:**
```javascript
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';

// Make an authenticated API call
const response = await authorizedFetch(serverApiURL, {
  method: 'POST',
  timeout: 30000,
  response: 'json',
  tries: 2,
  body: {
    command: 'user:get-data',
    payload: { id: 'example' }
  }
});
```

**How It Works:**
1. Retrieves the current Firebase user's ID token automatically
2. Adds the token to the request as an `Authorization: Bearer <token>` header
3. Makes the request using wonderful-fetch
4. Throws an error if no authenticated user is found

**Benefits:**
- No need to manually call `webManager.auth().getIdToken()`
- No need to add `authenticationToken` to request body
- Centralized authentication handling
- Consistent authentication across all API calls

#### FormManager Library

Lightweight form state management library with built-in validation, state machine, and event system.

**Import:**
```javascript
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
```

**Basic Usage:**
```javascript
const formManager = new FormManager('#my-form', {
  allowResubmit: true,      // Allow form to be submitted again after success
  resetOnSuccess: false,    // Don't clear fields after success
  warnOnUnsavedChanges: false // Don't warn on page leave
});

// Handle form submission
formManager.on('submit', async ({ data, $submitButton }) => {
  const response = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(data)
  });

  if (!response.ok) {
    throw new Error('Submission failed');
  }

  formManager.showSuccess('Form submitted successfully!');
});
```

**State Machine:**
```
initializing → ready ⇄ submitting → ready (or submitted)
```

**Events:**

| Event | Payload | Description |
|-------|---------|-------------|
| `submit` | `{ data, $submitButton }` | Form submission. Throw an error to show failure message. |
| `validation` | `{ data, setError }` | Custom validation before submit. Use `setError(fieldName, message)` to add errors. |
| `change` | `{ field, name, value, data }` | Called when any field value changes. |
| `statechange` | `{ state, previousState }` | Called when form state changes. |
| `honeypot` | `{ data }` | Called when honeypot is triggered (for spam tracking). |

**Validation:**

FormManager runs validation automatically before the `submit` event:

1. **HTML5 Validation** - Automatically checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type="email"`, `type="url"`
2. **Custom Validation** - Use the `validation` event for business logic

```javascript
formManager.on('validation', ({ data, setError }) => {
  // Custom validation runs AFTER HTML5 validation
  if (data.age && parseInt(data.age) < 18) {
    setError('age', 'You must be 18 or older');
  }

  if (data.password !== data.confirmPassword) {
    setError('confirmPassword', 'Passwords do not match');
  }
});
```

Validation errors are displayed using Bootstrap's `is-invalid` class and `.invalid-feedback` elements. The first field with an error is automatically focused.

**Autofocus:**

When the form transitions to `ready` state, FormManager automatically focuses any field with the `autofocus` attribute:

```html
<input type="text" name="email" autofocus>
```

**Methods:**

| Method | Description |
|--------|-------------|
| `on(event, callback)` | Register event listener (chainable) |
| `ready()` | Manually transition to ready state (for `autoReady: false`) |
| `getData()` | Get form data as nested object |
| `setData(obj)` | Populate form from a nested object |
| `showSuccess(msg)` | Show success notification |
| `showError(msg)` | Show error notification |
| `reset()` | Reset form and state |
| `isDirty()` | Check if form has unsaved changes |
| `clearFieldErrors()` | Clear all validation error displays |
| `throwFieldErrors({ field: msg })` | Set errors and throw (for use in submit handler) |

**Nested Field Names (Dot Notation):**

Use dot notation in field `name` attributes for nested data structures:

```html
<input name="user.name" value="John">
<input name="user.address.city" value="NYC">
<input name="user.address.zip" value="10001">
```

Produces:
```javascript
{
  user: {
    name: 'John',
    address: {
      city: 'NYC',
      zip: '10001'
    }
  }
}
```

**Honeypot (Bot Detection):**

FormManager automatically rejects submissions if a honeypot field is filled. Fields matching `[data-honey]` or `[name="honey"]` are excluded from `getData()` and trigger rejection if filled.

```html
<!-- Hidden from users via CSS -->
<input type="text" name="honey" autocomplete="off" tabindex="-1"
       style="position: absolute; left: -9999px;" aria-hidden="true">
```

**Checkbox Handling:**

- **Single checkbox:** Returns `true` or `false`
- **Checkbox group (multiple with same name):** Returns object with each value as key

```html
<!-- Single checkbox -->
<input type="checkbox" name="subscribe" checked>
<!-- Result: { subscribe: true } -->

<!-- Checkbox group -->
<input type="checkbox" name="features" value="darkmode" checked>
<input type="checkbox" name="features" value="analytics">
<input type="checkbox" name="features" value="beta" checked>
<!-- Result: { features: { darkmode: true, analytics: false, beta: true } } -->
```

**Multiple Submit Buttons:**

Access the clicked submit button to handle different actions:

```html
<button type="submit" data-action="save">Save</button>
<button type="submit" data-action="draft">Save as Draft</button>
```

```javascript
formManager.on('submit', async ({ data, $submitButton }) => {
  const action = $submitButton?.dataset?.action;

  if (action === 'draft') {
    await saveDraft(data);
    formManager.showSuccess('Draft saved!');
  } else {
    await saveAndPublish(data);
    formManager.showSuccess('Published!');
  }
});
```

**Manual Ready Mode:**

For forms that need async initialization (e.g., loading data from API):

```javascript
const formManager = new FormManager('#my-form', { autoReady: false });

// Load data, then mark ready
const userData = await fetchUserData();
formManager.setData(userData);
formManager.ready(); // Now form is interactive
```

**Configuration Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `autoReady` | `true` | Auto-transition to ready when DOM is ready |
| `initialState` | `'ready'` | State after autoReady fires |
| `allowResubmit` | `true` | Allow form resubmission after success |
| `resetOnSuccess` | `false` | Clear fields after successful submission |
| `warnOnUnsavedChanges` | `true` | Show browser warning when leaving with unsaved changes |
| `submittingText` | `'Processing...'` | Text shown on submit button during submission |

**Test Page:** Navigate to `/test/libraries/form-manager` to see FormManager in action with various configurations.

### ITM (Internal Tracking Medium)

Internal tracking system modeled after UTM for cross-property user journey tracking.

| Parameter | Purpose | Examples |
|-----------|---------|----------|
| `itm_source` | Platform/origin | `website`, `browser-extension`, `app`, `email` |
| `itm_medium` | Delivery mechanism | `modal`, `prompt`, `banner`, `tooltip` |
| `itm_campaign` | Specific campaign/feature | `exit-popup`, `premium-unlock`, `newsletter-signup` |
| `itm_content` | Specific context | Page path, feature ID, variant |

**Examples:**
```
# Website exit popup
?itm_source=website&itm_medium=modal&itm_campaign=exit-popup&itm_content=/pricing

# Extension premium unlock
?itm_source=browser-extension&itm_medium=prompt&itm_campaign=premium-unlock&itm_content=bulk-export
```

### Icons
* Fontawesome
  * https://fontawesome.com/search
* Flags
  * https://www.freepik.com/icon/england_4720360#fromView=resource_detail&position=1
* More
  * Language
    * https://www.freepik.com/icon/language_484531#fromView=family&page=1&position=0&uuid=651a2f0f-9023-4063-a495-af9a4ef72304

### Webpack Import Aliases

UJM defines two webpack aliases (in `src/gulp/tasks/webpack.js`) for importing assets in JavaScript:

| Alias | Resolves To | Purpose |
|-------|------------|---------|
| `__main_assets__` | `[UJM package]/dist/assets` | UJM's own built-in assets (core modules, libraries, pages) |
| `__project_assets__` | `[consuming project]/src/assets` | The consuming project's custom assets |

**`__main_assets__`** — Import UJM libraries and core modules:
```javascript
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
```

**`__project_assets__`** — Import consuming project's own assets:
```javascript
// Used in src/index.js to load project-specific page modules
import(`__project_assets__/js/pages/${pageModulePath}`)
```

**How they work together:** `src/index.js` loads page modules from both aliases — first from `__main_assets__` (UJM defaults), then from `__project_assets__` (project overrides/extensions). If a project module doesn't exist, it gracefully skips. This enables a layered system where UJM provides defaults and consuming projects can extend or override page behavior.

**When to use which:**
- **`__main_assets__`** — When importing UJM-provided libraries, core modules, or referencing UJM's built-in page scripts
- **`__project_assets__`** — When a consuming project needs to import its own custom assets from within UJM-managed code

## Dev Flags
Add this to any js file to ONLY run in development mode (it will be excluded in production builds):
```
  /* @dev-only:start */
  {
    // Your development-only code goes here
  }
  /* @dev-only:end */
```

## 🧰 Sister projects

- [Electron Manager (EM)](https://github.com/itw-creative-works/electron-manager) — same patterns, but for Electron desktop apps
- [Browser Extension Manager (BXM)](https://github.com/itw-creative-works/browser-extension-manager) — same patterns, but for cross-browser MV3 extensions
- [Backend Manager (BEM)](https://github.com/itw-creative-works/backend-manager) — Firebase Functions backend framework
