ultimate-jekyll-manager 1.9.4 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CLAUDE.md +6 -0
  3. package/PROGRESS.md +10 -1
  4. package/dist/assets/css/core/_initialize.scss +11 -0
  5. package/dist/assets/js/libs/form-manager.js +4 -0
  6. package/dist/commands/install.js +2 -2
  7. package/dist/commands/setup.js +2 -1
  8. package/dist/defaults/.github/workflows/build.yml +3 -1
  9. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +9 -1
  10. package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +1 -1
  11. package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +2 -2
  12. package/dist/defaults/dist/_layouts/blueprint/admin/studio/index.html +1 -1
  13. package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +2 -2
  14. package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +1 -1
  15. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +13 -13
  16. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/reset.html +2 -2
  17. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +3 -3
  18. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +3 -3
  19. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/index.html +1 -1
  20. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +1 -1
  21. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +1 -1
  22. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +1 -1
  23. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +2 -6
  24. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +1 -1
  25. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/portal/email-preferences.html +2 -3
  26. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/status.html +1 -1
  27. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/index.html +1 -1
  28. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/contact.html +1 -1
  29. package/dist/defaults/dist/pages/test/libraries/form-manager.html +31 -31
  30. package/dist/lib/safe-install.js +13 -0
  31. package/logs/test.log +35 -35
  32. package/package.json +1 -1
  33. package/plans/fix-translation-exclude-404s.md +113 -0
package/CHANGELOG.md CHANGED
@@ -14,6 +14,18 @@ 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.9.5] - 2026-06-22
19
+
20
+ ### Changed
21
+ - **FormManager: CSS-based loading guard replaces per-element `disabled`.** All FM-managed forms now use `data-form-state="initializing"` + a CSS rule (`pointer-events: none; opacity: 0.65`) instead of `disabled` attributes on individual inputs. FM sets the attribute in `_init()` and the CSS unblocks when state transitions to `ready`. Removed loading-guard `disabled` from inputs across all 41 forms (auth, contact, checkout, hero demo, account, admin, etc.) and 7 test forms. Submit buttons retain `disabled` in HTML — FM excludes `type="submit"` from the snapshot by design.
22
+
23
+ ### Added
24
+ - **Supply-chain security via Socket Firewall.** New `safeInstall()` helper (`src/lib/safe-install.js`) wraps npm install with `sfw` (Socket Firewall) when available, scanning the full transitive dependency tree for malware before download. `npx mgr install` and `npx mgr setup` now route through it. CI `build.yml` installs sfw globally and runs `sfw npm install`.
25
+
26
+ ### Fixed
27
+ - **Footer language switcher no longer links to excluded pages.** The language dropdown now checks `translation.exclude` before rendering language links. Pages matching an exclude entry (e.g. blog posts when `"blog"` is excluded) only show the default language, preventing dead `/es/blog/...` links that 404.
28
+
17
29
  ---
18
30
  ## [1.9.4] - 2026-06-20
19
31
 
package/CLAUDE.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  > **Note for contributors and Claude:** This file is the architectural overview — identity, top-level conventions, and a map to deep references. The **meat** (per-subsystem APIs, page customization recipes, theming, behavior tables, defaults lists) lives in `docs/<topic>.md`. When extending or adding content, write it in the matching `docs/*.md` file and cross-link from here — do NOT inline it. If a topic doesn't have a doc yet, create one. Goal: keep this file under 250 lines.
4
4
 
5
+ > **Mirrored structure:** BEM, UJM, BXM, and EM CLAUDE.md files mirror each other — shared sections (Supply-Chain Security, Development Workflow, File Conventions, etc.) appear in the **same order at the same position** across all four. When adding a section that applies to multiple frameworks, insert it in the same spot in all of them.
6
+
5
7
  ## Identity
6
8
 
7
9
  Ultimate Jekyll Manager (UJM) is a comprehensive framework for building modern Jekyll-powered static sites. Sister project to Electron Manager (EM) and Browser Extension Manager (BXM). Provides:
@@ -176,6 +178,10 @@ Note: `-t` short alias belongs to `translation`. The `test` command uses `--test
176
178
  - **🚫 NEVER run `npm start` in a consumer project** — the user runs the dev server; running it again kills theirs. Assume it's already running; if it isn't, **instruct the user to run it** rather than running it yourself. Instead, **check `logs/dev.log`** after editing files to confirm the watcher recompiled successfully (`Reloading Browsers...` = success; `errored` = fix the error) — never tail/attach to the process. If editing multiple files, check the log once after the last edit. A change that breaks the build is not a completed change. Running `npx mgr test` is fine.
177
179
  - **Live-test UI changes via CDP.** After code changes compile, use the `chrome-devtools` MCP tools (screenshots, click, evaluate JS, console logs) to verify the change works in the running browser. This is the primary way to confirm UI changes — type-checking and test suites verify code correctness, not feature correctness. Read the URL from the consumer's `.temp/_config_browsersync.yml`. Prefer `https://localhost:4000`; fall back to the local network IP (e.g. `https://192.168.x.x:4000`) if localhost doesn't connect. See [docs/cdp-debugging.md](docs/cdp-debugging.md) + `~/.claude/mcp-server/servers/chrome-devtools/CLAUDE.md`.
178
180
 
181
+ ## Supply-Chain Security
182
+
183
+ All `npm install` calls in CLI commands (`npx mgr i`, `npx mgr setup`) route through the `safeInstall()` helper (`src/lib/safe-install.js`). It prefixes `sfw` (Socket Firewall) when installed — blocking confirmed malware at the network level before packages reach disk. Falls back to plain npm if sfw isn't available. CI workflows install sfw globally and run `sfw npm install`. Installs will **fail if sfw detects confirmed malware** in any package in the dependency tree; non-critical CVEs and quality warnings pass through.
184
+
179
185
  ## File Conventions
180
186
 
181
187
  - **CommonJS** in build-time / Node files (gulp tasks, commands, lib/). **ESM** in `src/index.js` (frontend Manager — webpack-bundled).
package/PROGRESS.md CHANGED
@@ -6,7 +6,7 @@
6
6
  * **Current Phase:** Phase 1 — implementation + tests complete, docs pending
7
7
  * **Priority:** Medium
8
8
  * **Last Updated:** 2026-06-20 11:03 PM PDT
9
- * **Notes:** FM disabled-state refactor done + comprehensive FM test suite added (110 tests total, up from 80). Blog post ad insertion fix applied (Phase 2). Docs (javascript-libraries.md, CHANGELOG) still need updating before shipping.
9
+ * **Notes:** FM disabled-state refactor done + comprehensive FM test suite added (110 tests total, up from 80). Blog post ad insertion fix applied (Phase 2). Auth form HTML fixed to remove `disabled` from inputs (loading guards that conflicted with FM snapshot). Docs (javascript-libraries.md, CHANGELOG) still need updating before shipping.
10
10
 
11
11
  ## Active Task List
12
12
  * [ ] Phase 1: FormManager disabled-state snapshot-and-restore
@@ -16,10 +16,19 @@
16
16
  * [x] Task 1.4: All 85 tests passing
17
17
  * [x] Task 1.5: Add visual Test 7 to FM test page (form-manager.html + JS) with permanently disabled fields + rapid-cycle demo
18
18
  * [x] Task 1.6: Write comprehensive FM page-layer tests — getData/setData (12 tests), validation/honeypot/file-accept (13 tests). 110 total.
19
+ * [x] Task 1.7a: Full audit + fix: remove `disabled` from all loading-guard inputs across UJM forms (auth signin/signup/reset, email-preferences, hero-demo-form input/select/textarea)
20
+ * [x] Task 1.7b: Add `data-form-state="initializing"` to all 34 FM-managed forms + 7 test forms, CSS guard in `_initialize.scss`, FM sets attribute in `_init()`. Remove loading-guard `disabled` from test form inputs (tests 1-6).
19
21
  * [ ] Task 1.7: Update `docs/javascript-libraries.md` — replace `data-fm-keep-disabled` docs with new snapshot pattern + `onsubmit="return false"` + `data-form-state="initializing"` CSS guard
20
22
  * [ ] Task 1.8: Update CHANGELOG with the change
21
23
  * [ ] Task 1.9: Ship (commit, push, publish)
22
24
 
25
+ * [ ] Phase 3: Fix translation exclude list not respected by footer language switcher
26
+ * [x] Task 3.1: Diagnose root cause — footer renders dead `/es/blog/...` links for excluded pages
27
+ * [x] Task 3.2: Fix footer template (`classy/frontend/sections/footer.html`) to check exclude list
28
+ * [x] Task 3.3: Fix `uj_translation_url` Ruby tag in `jekyll-uj-powertools` gem v1.7.11 (defense-in-depth)
29
+ * [ ] Task 3.4: Validate on a consumer site (rebuild + check footer on blog post)
30
+ * [ ] Task 3.5: Ship both repos (commit, push, publish gem + UJM)
31
+
23
32
  * [x] Phase 2: Fix blog post ad insertion inside blockquotes
24
33
  * [x] Task 2.1: Filter out `<p>` elements inside `<blockquote>`, `<details>`, `<figure>` from ad insertion candidates in `post.js`
25
34
 
@@ -20,6 +20,17 @@ body {
20
20
  padding: 0;
21
21
  }
22
22
 
23
+ // ============================================
24
+ // FormManager Loading Guard
25
+ // ============================================
26
+
27
+ // Forms with data-form-state block interaction until FormManager sets state
28
+ // to "ready". Replaces per-element disabled attributes as the loading guard.
29
+ form[data-form-state]:not([data-form-state="ready"]) {
30
+ pointer-events: none;
31
+ opacity: 0.65;
32
+ }
33
+
23
34
  // ============================================
24
35
  // Font Display Configuration
25
36
  // ============================================
@@ -93,6 +93,10 @@ export class FormManager {
93
93
  * Initialize the form manager
94
94
  */
95
95
  _init() {
96
+ // Set the form state attribute immediately so the CSS loading guard
97
+ // kicks in even if the HTML didn't include data-form-state.
98
+ this.$form.setAttribute('data-form-state', this.state);
99
+
96
100
  // Snapshot elements that are disabled in HTML markup BEFORE the first
97
101
  // blanket disable. These are business-logic disabled (e.g. "coming soon"
98
102
  // options) and must stay disabled through every state transition.
@@ -1,7 +1,7 @@
1
1
  // Libraries
2
2
  const Manager = new (require('../build.js'));
3
3
  const logger = Manager.logger('install');
4
- const { execute } = require('node-powertools');
4
+ const { safeInstall } = require('../lib/safe-install');
5
5
  const os = require('os');
6
6
 
7
7
  // Load package
@@ -48,5 +48,5 @@ module.exports = async function (options) {
48
48
  };
49
49
 
50
50
  function install(command) {
51
- return execute(command, { log: true });
51
+ return safeInstall(command);
52
52
  }
@@ -6,6 +6,7 @@ const jetpack = require('fs-jetpack');
6
6
  const version = require('wonderful-version');
7
7
  const fetch = require('wonderful-fetch');
8
8
  const { execute } = require('node-powertools');
9
+ const { safeInstall } = require('../lib/safe-install');
9
10
  const NPM = require('npm-api');
10
11
  const glob = require('glob').globSync;
11
12
  const detectGitHubRepository = require('../gulp/tasks/utils/detect-github-repo');
@@ -652,7 +653,7 @@ function npmInstall(pkg, ver, location) {
652
653
 
653
654
  logger.log('Installing:', command);
654
655
 
655
- return execute(command, { log: true })
656
+ return safeInstall(command)
656
657
  .then(() => {
657
658
  const projectUpdated = jetpack.read(path.join(process.cwd(), 'package.json'), 'json');
658
659
 
@@ -60,8 +60,10 @@ jobs:
60
60
  uses: actions/setup-node@v4
61
61
  with:
62
62
  node-version: ${{ env.NODE_VERSION }}
63
+ - name: Install Socket Firewall
64
+ run: npm install -g sfw
63
65
  - name: Run node install
64
- run: npm install
66
+ run: sfw npm install
65
67
  - name: Log dependency versions
66
68
  run: |
67
69
  echo "🖥️ Unix Versions: "
@@ -86,7 +86,15 @@
86
86
  </button>
87
87
  <ul class="dropdown-menu">
88
88
  {% assign default_language = site.translation.default | default: "en" %}
89
- {% if site.translation.enabled and site.translation.languages.size > 0 %}
89
+ {% assign page_excluded_from_translation = false %}
90
+ {% for exclude in site.translation.exclude %}
91
+ {% assign exclude_folder = "/" | append: exclude | append: "/" %}
92
+ {% assign exclude_page = "/" | append: exclude %}
93
+ {% if page.url == exclude_page or page.url contains exclude_folder %}
94
+ {% assign page_excluded_from_translation = true %}
95
+ {% endif %}
96
+ {% endfor %}
97
+ {% if site.translation.enabled and site.translation.languages.size > 0 and page_excluded_from_translation != true %}
90
98
  {% assign all_languages = site.translation.languages | push: default_language %}
91
99
  {% else %}
92
100
  {% assign all_languages = "" | split: "" | push: default_language %}
@@ -85,7 +85,7 @@ prerender_icons:
85
85
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
86
86
  </div>
87
87
  <div class="modal-body">
88
- <form id="campaign-editor-form" novalidate onsubmit="return false">
88
+ <form id="campaign-editor-form" novalidate data-form-state="initializing" onsubmit="return false">
89
89
 
90
90
  <!-- Campaign Type -->
91
91
  <h6 class="mb-3">
@@ -87,7 +87,7 @@ prerender_icons:
87
87
  <hr class="my-2">
88
88
 
89
89
  <!-- Custom Collection Input -->
90
- <form id="collection-form" onsubmit="return false">
90
+ <form id="collection-form" data-form-state="initializing" onsubmit="return false">
91
91
  <div class="input-group input-group-sm">
92
92
  <input type="text" class="form-control" name="collection.path" placeholder="Collection path..." aria-label="Collection path" required>
93
93
  <button type="submit" class="btn btn-outline-adaptive">
@@ -104,7 +104,7 @@ prerender_icons:
104
104
  <h6 class="mb-0">{% uj_icon "magnifying-glass", "fa-sm me-2" %} Query</h6>
105
105
  </div>
106
106
  <div class="card-body">
107
- <form id="query-form" onsubmit="return false">
107
+ <form id="query-form" data-form-state="initializing" onsubmit="return false">
108
108
  <div class="mb-2">
109
109
  <label for="query-field" class="form-label small mb-1">Field</label>
110
110
  <input type="text" class="form-control form-control-sm" name="query.field" id="query-field" placeholder="e.g. auth.email" required>
@@ -36,7 +36,7 @@ web_manager:
36
36
  <div class="studio-clips" id="studio-clips">
37
37
  <!-- Populated by JS from registered clips -->
38
38
  </div>
39
- <form id="studio-controls-form" class="studio-controls">
39
+ <form id="studio-controls-form" class="studio-controls" data-form-state="initializing" onsubmit="return false">
40
40
  <fieldset id="studio-fieldset">
41
41
  <!-- Aspect ratio toggle -->
42
42
  <label class="studio-label">
@@ -101,7 +101,7 @@ prerender_icons:
101
101
  </div>
102
102
 
103
103
  <!-- Search Form -->
104
- <form id="user-search-form" onsubmit="return false">
104
+ <form id="user-search-form" data-form-state="initializing" onsubmit="return false">
105
105
  <div class="row mb-3">
106
106
  <div class="col">
107
107
  <div class="input-group">
@@ -252,7 +252,7 @@ prerender_icons:
252
252
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
253
253
  </div>
254
254
  <div class="modal-body">
255
- <form id="user-edit-form" onsubmit="return false">
255
+ <form id="user-edit-form" data-form-state="initializing" onsubmit="return false">
256
256
  <!-- UID (read-only) -->
257
257
  <div class="mb-3">
258
258
  <label class="form-label small text-muted">UID</label>
@@ -39,7 +39,7 @@ meta:
39
39
  <h5 class="mb-0">Create new user</h5>
40
40
  </div>
41
41
  <div class="card-body">
42
- <form id="create-user-form" novalidate onsubmit="return false">
42
+ <form id="create-user-form" novalidate data-form-state="initializing" onsubmit="return false">
43
43
  <div class="mb-3">
44
44
  <label for="email" class="form-label">Email address <span class="text-danger">*</span></label>
45
45
  <input type="email" class="form-control" id="email" name="user.email" required autofocus>
@@ -239,7 +239,7 @@ badges:
239
239
  <section id="profile-section" class="account-section d-none">
240
240
  <h2 class="h3 mb-4" >My profile</h2>
241
241
 
242
- <form id="profile-form" novalidate onsubmit="return false">
242
+ <form id="profile-form" novalidate data-form-state="initializing" onsubmit="return false">
243
243
  <!-- Avatar and Edit Button -->
244
244
  <div class="mb-4">
245
245
  <div class="d-flex align-items-center">
@@ -799,7 +799,7 @@ badges:
799
799
  </div>
800
800
  </div>
801
801
  <div class="flex-shrink-0">
802
- <form id="signin-method-{{ method.id }}-form" class="d-grid d-sm-inline-block" novalidate onsubmit="return false">
802
+ <form id="signin-method-{{ method.id }}-form" class="d-grid d-sm-inline-block" novalidate data-form-state="initializing" onsubmit="return false">
803
803
  <input type="hidden" name="method" value="{{ method.id }}">
804
804
  {% if method.id == "password" %}
805
805
  <button type="submit" class="btn btn-primary btn-sm" data-action="change">
@@ -841,7 +841,7 @@ badges:
841
841
  <div class="card-body">
842
842
  <div class="d-flex justify-content-between align-items-center mb-3">
843
843
  <h5 class="card-title mb-0">Active sessions</h5>
844
- <form id="signout-all-sessions-form" class="d-inline" novalidate onsubmit="return false">
844
+ <form id="signout-all-sessions-form" class="d-inline" novalidate data-form-state="initializing" onsubmit="return false">
845
845
  <input type="hidden" name="action" value="signout-all">
846
846
  <button type="submit" class="btn btn-outline-danger btn-sm">
847
847
  {% uj_icon "right-from-bracket", "me-1" %}
@@ -967,7 +967,7 @@ badges:
967
967
  </div>
968
968
  <div class="text-start text-sm-end flex-shrink-0">
969
969
  <div>
970
- <form id="connection-form-{{ connection.id }}" class="d-grid d-sm-inline-block" novalidate onsubmit="return false">
970
+ <form id="connection-form-{{ connection.id }}" class="d-grid d-sm-inline-block" novalidate data-form-state="initializing" onsubmit="return false">
971
971
  <input type="hidden" name="provider" value="{{ connection.id }}">
972
972
  <!-- Connect button -->
973
973
  <button type="submit" class="btn btn-sm btn-primary" data-action="connect">
@@ -1109,7 +1109,7 @@ badges:
1109
1109
  If you cancel, your subscription will remain active until the end of your current billing period. After that, you'll be downgraded to the free plan and will lose access to premium features. You can resubscribe at any time.
1110
1110
  </p>
1111
1111
 
1112
- <form id="cancel-subscription-form" novalidate onsubmit="return false">
1112
+ <form id="cancel-subscription-form" novalidate data-form-state="initializing" onsubmit="return false">
1113
1113
  <!-- Cancellation Reasons (randomized by JS) -->
1114
1114
  <div class="mb-3">
1115
1115
  <label class="form-label">Why are you cancelling?</label>
@@ -1261,7 +1261,7 @@ badges:
1261
1261
  <div class="card-body">
1262
1262
  <h5 class="card-title">Email preferences</h5>
1263
1263
 
1264
- <form id="marketing-emails-form" novalidate>
1264
+ <form id="marketing-emails-form" novalidate data-form-state="initializing" onsubmit="return false">
1265
1265
  <div class="form-check form-switch mb-3">
1266
1266
  <input class="form-check-input" type="checkbox" id="marketing-emails" name="enabled">
1267
1267
  <label class="form-check-label" for="marketing-emails">
@@ -1285,7 +1285,7 @@ badges:
1285
1285
  </div>
1286
1286
  <p class="text-muted small mb-3">Receive push notifications in your browser for important updates.</p>
1287
1287
 
1288
- <form id="push-subscribe-form" novalidate onsubmit="return false" style="display:none">
1288
+ <form id="push-subscribe-form" novalidate data-form-state="initializing" onsubmit="return false" style="display:none">
1289
1289
  <button type="submit" class="btn btn-adaptive btn-sm" id="push-subscribe-btn">
1290
1290
  {% uj_icon "bell", "me-1" %}
1291
1291
  <span class="button-text">Enable push notifications</span>
@@ -1318,7 +1318,7 @@ badges:
1318
1318
  <div class="card-body">
1319
1319
  <div class="d-flex justify-content-between align-items-center mb-3">
1320
1320
  <h5 class="card-title mb-0">Your API key</h5>
1321
- <form id="reset-api-key-form" class="d-inline" novalidate onsubmit="return false">
1321
+ <form id="reset-api-key-form" class="d-inline" novalidate data-form-state="initializing" onsubmit="return false">
1322
1322
  <button type="submit" class="btn btn-outline-danger btn-sm" id="reset-api-key-btn">
1323
1323
  {% uj_icon "rotate", "me-1" %}
1324
1324
  <span class="button-text">
@@ -1619,7 +1619,7 @@ badges:
1619
1619
  </h5>
1620
1620
  <p id="data-request-status-message" class="card-text text-muted"></p>
1621
1621
  <div id="data-request-download" class="d-none">
1622
- <form id="data-request-download-form" onsubmit="return false">
1622
+ <form id="data-request-download-form" data-form-state="initializing" onsubmit="return false">
1623
1623
  <button type="submit" class="btn btn-primary btn-sm" id="data-request-download-btn">
1624
1624
  {% uj_icon "download", "fa-sm me-2" %}
1625
1625
  <span class="button-text">Download your data</span>
@@ -1627,7 +1627,7 @@ badges:
1627
1627
  </form>
1628
1628
  </div>
1629
1629
  <div id="data-request-cancel" class="d-none">
1630
- <form id="data-request-cancel-form" onsubmit="return false">
1630
+ <form id="data-request-cancel-form" data-form-state="initializing" onsubmit="return false">
1631
1631
  <button type="submit" class="btn btn-primary btn-sm" id="data-request-cancel-btn">
1632
1632
  {% uj_icon "rotate-left", "fa-sm me-2" %}
1633
1633
  <span class="button-text">Request withdrawal</span>
@@ -1648,7 +1648,7 @@ badges:
1648
1648
  Confirm data request
1649
1649
  </h5>
1650
1650
 
1651
- <form id="data-request-form" novalidate onsubmit="return false">
1651
+ <form id="data-request-form" novalidate data-form-state="initializing" onsubmit="return false">
1652
1652
  <div class="mb-3">
1653
1653
  <div class="form-check">
1654
1654
  <input class="form-check-input" type="checkbox" id="data-request-confirm-checkbox" name="confirm_processing_time" required>
@@ -1751,7 +1751,7 @@ badges:
1751
1751
  Confirm account deletion
1752
1752
  </h5>
1753
1753
 
1754
- <form id="delete-account-form" novalidate onsubmit="return false">
1754
+ <form id="delete-account-form" novalidate data-form-state="initializing" onsubmit="return false">
1755
1755
  <div class="mb-3">
1756
1756
  <label for="delete-reason" class="form-label">Reason for leaving</label>
1757
1757
  <textarea class="form-control" id="delete-reason" name="reason" rows="3" placeholder="Help us improve by sharing why you're leaving..."></textarea>
@@ -1879,7 +1879,7 @@ badges:
1879
1879
  Confirm refund request
1880
1880
  </h5>
1881
1881
 
1882
- <form id="refund-form" novalidate onsubmit="return false">
1882
+ <form id="refund-form" novalidate data-form-state="initializing" onsubmit="return false">
1883
1883
  <!-- Refund reasons (randomized by JS) -->
1884
1884
  <div class="mb-3">
1885
1885
  <label class="form-label">Why are you requesting a refund? <span class="text-danger">*</span></label>
@@ -25,12 +25,12 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
25
25
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
26
26
 
27
27
  <!-- Reset Form -->
28
- <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
28
+ <form id="auth-form" autocomplete="on" novalidate data-form-state="initializing" onsubmit="return false">
29
29
  <div class="mb-4 text-start">
30
30
  <label for="email" class="form-label fw-semibold">
31
31
  Email <span class="text-danger">*</span>
32
32
  </label>
33
- <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus required disabled>
33
+ <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus required>
34
34
  </div>
35
35
 
36
36
  <button type="submit" class="btn btn-success _btn-lg w-100 mb-4" data-provider="email" disabled>
@@ -40,7 +40,7 @@ social_signin:
40
40
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
41
41
 
42
42
  <!-- Sign in Form -->
43
- <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
43
+ <form id="auth-form" autocomplete="on" novalidate data-form-state="initializing" onsubmit="return false">
44
44
  <!-- Hidden default submit button for Enter key -->
45
45
  <button type="submit" class="d-none" data-provider="email" aria-hidden="true" tabindex="-1"></button>
46
46
 
@@ -67,7 +67,7 @@ social_signin:
67
67
  <label for="email" class="form-label fw-semibold">
68
68
  Email <span class="text-danger">*</span>
69
69
  </label>
70
- <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus disabled>
70
+ <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus>
71
71
  </div>
72
72
 
73
73
  <div class="mb-3 text-start">
@@ -75,7 +75,7 @@ social_signin:
75
75
  Password <span class="text-danger">*</span>
76
76
  </label>
77
77
  <div class="input-group">
78
- <input type="password" class="form-control form-control-md" id="password" name="password" placeholder="Enter your password" autocomplete="current-password" disabled>
78
+ <input type="password" class="form-control form-control-md" id="password" name="password" placeholder="Enter your password" autocomplete="current-password">
79
79
  <button class="btn border uj-password-toggle px-3" type="button" aria-label="Toggle password visibility">
80
80
  <span class="uj-password-show">{% uj_icon "eye", "fa-fw" %}</span>
81
81
  <span class="uj-password-hide d-none">{% uj_icon "eye-slash", "fa-fw" %}</span>
@@ -40,7 +40,7 @@ social_signup:
40
40
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
41
41
 
42
42
  <!-- Sign up Form -->
43
- <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
43
+ <form id="auth-form" autocomplete="on" novalidate data-form-state="initializing" onsubmit="return false">
44
44
  <!-- Hidden default submit button for Enter key -->
45
45
  <button type="submit" class="d-none" data-provider="email" aria-hidden="true" tabindex="-1"></button>
46
46
 
@@ -77,7 +77,7 @@ social_signup:
77
77
  <label for="email" class="form-label fw-semibold">
78
78
  Email <span class="text-danger">*</span>
79
79
  </label>
80
- <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus disabled>
80
+ <input type="email" class="form-control form-control-md" id="email" name="email" placeholder="Enter your email" autocomplete="email" autofocus>
81
81
  </div>
82
82
 
83
83
  <div class="mb-3 text-start">
@@ -85,7 +85,7 @@ social_signup:
85
85
  Password <span class="text-danger">*</span>
86
86
  </label>
87
87
  <div class="input-group">
88
- <input type="password" class="form-control form-control-md" id="password" name="password" placeholder="Create a strong password" autocomplete="new-password" disabled>
88
+ <input type="password" class="form-control form-control-md" id="password" name="password" placeholder="Create a strong password" autocomplete="new-password">
89
89
  <button class="btn border uj-password-toggle px-3" type="button" aria-label="Toggle password visibility">
90
90
  <span class="uj-password-show">{% uj_icon "eye", "fa-fw" %}</span>
91
91
  <span class="uj-password-hide d-none">{% uj_icon "eye-slash", "fa-fw" %}</span>
@@ -340,7 +340,7 @@ newsletter_cta:
340
340
  {% endiftruthy %}
341
341
 
342
342
  <!-- Newsletter Form -->
343
- <form id="newsletter-form" class="newsletter-form" novalidate onsubmit="return false">
343
+ <form id="newsletter-form" class="newsletter-form" novalidate data-form-state="initializing" onsubmit="return false">
344
344
  <div class="row g-3 justify-content-center">
345
345
  <div class="col-md-7">
346
346
  <input
@@ -178,7 +178,7 @@ faqs:
178
178
  {% endiftruthy %}
179
179
  </div>
180
180
 
181
- <form id="contact-form" autocomplete="on" onsubmit="return false">
181
+ <form id="contact-form" autocomplete="on" data-form-state="initializing" onsubmit="return false">
182
182
  <div class="row g-3 mb-4">
183
183
  <div class="col-md-6">
184
184
  <label for="first_name" class="form-label fw-semibold">First name <span class="text-danger">*</span></label>
@@ -351,7 +351,7 @@ cta:
351
351
  {% else %}
352
352
  {% if platform.id == "android" or platform.id == "ios" %}
353
353
  <div class="mb-4 text-start">
354
- <form id="mobile-email-form-{{ platform.id }}" class="mobile-email-form" data-platform="{{ platform.id }}" onsubmit="return false">
354
+ <form id="mobile-email-form-{{ platform.id }}" class="mobile-email-form" data-platform="{{ platform.id }}" data-form-state="initializing" onsubmit="return false">
355
355
  <label for="email-{{ platform.id }}" class="form-label">Email address</label>
356
356
  <div class="row g-3">
357
357
  <div class="col-12 col-md-8">
@@ -84,7 +84,7 @@ prerender_icons:
84
84
  {% endiftruthy %}
85
85
  </div>
86
86
 
87
- <form id="feedback-form" autocomplete="off" onsubmit="return false">
87
+ <form id="feedback-form" autocomplete="off" data-form-state="initializing" onsubmit="return false">
88
88
  <!-- Rating Selection -->
89
89
  <div class="feedback-rating-group mb-4">
90
90
  <input type="hidden" name="rating" id="feedback-rating-input" required>
@@ -226,7 +226,7 @@ cta:
226
226
  <!-- Input Demo: Single input with button -->
227
227
  <div class="card bg-glassy border-0 {% unless is_side_layout %}mx-auto{% endunless %} {{ demo.options.class }}">
228
228
  <div class="card-body">
229
- <form id="hero-demo-form" class="d-flex flex-column {% unless is_side_layout %}flex-sm-row justify-content-center{% endunless %} gap-3" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} onsubmit="return false">
229
+ <form id="hero-demo-form" class="d-flex flex-column {% unless is_side_layout %}flex-sm-row justify-content-center{% endunless %} gap-3" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} data-form-state="initializing" onsubmit="return false">
230
230
  <input
231
231
  type="{{ demo.options.input_type | default: 'text' }}"
232
232
  name="{{ demo.options.name | default: 'input' }}"
@@ -234,7 +234,6 @@ cta:
234
234
  placeholder="{{ demo.options.placeholder }}"
235
235
  {% iftruthy demo.options.required %}required{% endiftruthy %}
236
236
  autofocus
237
- disabled
238
237
  >
239
238
  {% assign demo_btn_class = demo.options.button.class | default: "btn-adaptive-inverse" %}
240
239
  <button type="submit" class="btn {{ demo_btn_class }} btn-lg text-nowrap flex-shrink-0" disabled>
@@ -250,7 +249,7 @@ cta:
250
249
  {% elsif demo.type == "form" %}
251
250
  <!-- Form Demo: Multiple fields with card styling -->
252
251
  <div class="{% unless is_side_layout %}mx-auto{% endunless %} {{ demo.options.class }}">
253
- <form id="hero-demo-form" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} onsubmit="return false">
252
+ <form id="hero-demo-form" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} data-form-state="initializing" onsubmit="return false">
254
253
  <div class="row g-3 {% unless is_side_layout %}justify-content-center{% endunless %}">
255
254
  {% assign autofocus_set = false %}
256
255
  {% for field in demo.options.fields %}
@@ -273,7 +272,6 @@ cta:
273
272
  name="{{ field.name }}"
274
273
  class="form-select form-select-lg border-0"
275
274
  {% iftruthy field.required %}required{% endiftruthy %}
276
- disabled
277
275
  >
278
276
  <option value="" disabled selected>{{ field.placeholder }}</option>
279
277
  {% for option in field.options %}
@@ -289,7 +287,6 @@ cta:
289
287
  rows="{{ field.rows | default: 3 }}"
290
288
  {% iftruthy field.required %}required{% endiftruthy %}
291
289
  {% unless autofocus_set %}autofocus{% assign autofocus_set = true %}{% endunless %}
292
- disabled
293
290
  ></textarea>
294
291
  {% else %}
295
292
  <input
@@ -300,7 +297,6 @@ cta:
300
297
  placeholder="{{ field.placeholder }}"
301
298
  {% iftruthy field.required %}required{% endiftruthy %}
302
299
  {% unless autofocus_set %}autofocus{% assign autofocus_set = true %}{% endunless %}
303
- disabled
304
300
  >
305
301
  {% endif %}
306
302
  </div>
@@ -164,7 +164,7 @@ web_manager:
164
164
  </div> -->
165
165
 
166
166
  <!-- Main Checkout Form -->
167
- <form id="checkout-form" autocomplete="on" novalidate onsubmit="return false">
167
+ <form id="checkout-form" autocomplete="on" novalidate data-form-state="initializing" onsubmit="return false">
168
168
  <!-- Content Row -->
169
169
  <div class="row d-lg-flex">
170
170
  <!-- Section 1: Billing Cycle & Customer Info -->
@@ -45,7 +45,7 @@ prerender_icons:
45
45
  </div>
46
46
 
47
47
  <!-- Email preferences Form -->
48
- <form id="email-preferences-form" autocomplete="off" onsubmit="return false">
48
+ <form id="email-preferences-form" autocomplete="off" data-form-state="initializing" onsubmit="return false">
49
49
  <!-- Action selector -->
50
50
  <div class="mb-4">
51
51
  <div class="d-flex gap-2">
@@ -78,8 +78,7 @@ prerender_icons:
78
78
  placeholder="Type your email to confirm"
79
79
  autocomplete="email"
80
80
  autofocus
81
- required
82
- disabled>
81
+ required>
83
82
  <div class="invalid-feedback"></div>
84
83
  </div>
85
84
 
@@ -255,7 +255,7 @@ build_info:
255
255
  {% endiftruthy %}
256
256
  </div>
257
257
  <div class="col-lg-5">
258
- <form id="status-subscribe-form" class="d-flex flex-column flex-sm-row gap-2" novalidate onsubmit="return false">
258
+ <form id="status-subscribe-form" class="d-flex flex-column flex-sm-row gap-2" novalidate data-form-state="initializing" onsubmit="return false">
259
259
  <input
260
260
  type="email"
261
261
  class="form-control flex-grow-1"
@@ -332,7 +332,7 @@ newsletter_cta:
332
332
  {% endiftruthy %}
333
333
 
334
334
  <!-- Newsletter Form -->
335
- <form id="newsletter-form" class="newsletter-form" novalidate onsubmit="return false">
335
+ <form id="newsletter-form" class="newsletter-form" novalidate data-form-state="initializing" onsubmit="return false">
336
336
  <div class="row g-3 justify-content-center">
337
337
  <div class="col-md-7">
338
338
  <input
@@ -183,7 +183,7 @@ faqs:
183
183
  {% endiftruthy %}
184
184
  </div>
185
185
 
186
- <form id="contact-form" autocomplete="on" onsubmit="return false">
186
+ <form id="contact-form" autocomplete="on" data-form-state="initializing" onsubmit="return false">
187
187
  <div class="row g-3 mb-4">
188
188
  <div class="col-md-6">
189
189
  <label for="first_name" class="form-label fw-semibold">First name <span class="text-danger">*</span></label>
@@ -28,26 +28,26 @@ meta:
28
28
  <small class="text-muted">Success/failure toggle, nested dot notation, change events (check console)</small>
29
29
  </div>
30
30
  <div class="card-body">
31
- <form id="test-form-main">
31
+ <form id="test-form-main" data-form-state="initializing" onsubmit="return false">
32
32
  <div class="row">
33
33
  <div class="col-md-6">
34
34
  <div class="mb-3">
35
35
  <label for="main-name" class="form-label">user.name</label>
36
- <input type="text" class="form-control" id="main-name" name="user.name" value="Ian" disabled autofocus>
36
+ <input type="text" class="form-control" id="main-name" name="user.name" value="Ian" autofocus>
37
37
  </div>
38
38
  <div class="mb-3">
39
39
  <label for="main-email" class="form-label">user.email</label>
40
- <input type="email" class="form-control" id="main-email" name="user.email" value="ian@example.com" disabled>
40
+ <input type="email" class="form-control" id="main-email" name="user.email" value="ian@example.com">
41
41
  </div>
42
42
  <div class="mb-3">
43
43
  <label for="main-city" class="form-label">user.address.city</label>
44
- <input type="text" class="form-control" id="main-city" name="user.address.city" value="NYC" disabled>
44
+ <input type="text" class="form-control" id="main-city" name="user.address.city" value="NYC">
45
45
  </div>
46
46
  </div>
47
47
  <div class="col-md-6">
48
48
  <div class="mb-3">
49
49
  <label for="main-outcome" class="form-label">Simulate outcome</label>
50
- <select class="form-select" id="main-outcome" name="settings.outcome" disabled>
50
+ <select class="form-select" id="main-outcome" name="settings.outcome">
51
51
  <option value="success">Success</option>
52
52
  <option value="error">Error (throw Error)</option>
53
53
  </select>
@@ -55,41 +55,41 @@ meta:
55
55
  <div class="mb-3">
56
56
  <label class="form-label d-block">preferences.notifications (radio group)</label>
57
57
  <div class="form-check form-check-inline">
58
- <input type="radio" class="form-check-input" id="notif-all" name="preferences.notifications" value="all" checked disabled>
58
+ <input type="radio" class="form-check-input" id="notif-all" name="preferences.notifications" value="all" checked>
59
59
  <label class="form-check-label" for="notif-all">All</label>
60
60
  </div>
61
61
  <div class="form-check form-check-inline">
62
- <input type="radio" class="form-check-input" id="notif-important" name="preferences.notifications" value="important" disabled>
62
+ <input type="radio" class="form-check-input" id="notif-important" name="preferences.notifications" value="important">
63
63
  <label class="form-check-label" for="notif-important">Important</label>
64
64
  </div>
65
65
  <div class="form-check form-check-inline">
66
- <input type="radio" class="form-check-input" id="notif-none" name="preferences.notifications" value="none" disabled>
66
+ <input type="radio" class="form-check-input" id="notif-none" name="preferences.notifications" value="none">
67
67
  <label class="form-check-label" for="notif-none">None</label>
68
68
  </div>
69
69
  </div>
70
70
  <div class="mb-3">
71
71
  <label class="form-label d-block">preferences.features (checkbox group)</label>
72
72
  <div class="form-check">
73
- <input type="checkbox" class="form-check-input" id="feat-darkmode" name="preferences.features" value="darkmode" disabled>
73
+ <input type="checkbox" class="form-check-input" id="feat-darkmode" name="preferences.features" value="darkmode">
74
74
  <label class="form-check-label" for="feat-darkmode">Dark Mode</label>
75
75
  </div>
76
76
  <div class="form-check">
77
- <input type="checkbox" class="form-check-input" id="feat-analytics" name="preferences.features" value="analytics" disabled>
77
+ <input type="checkbox" class="form-check-input" id="feat-analytics" name="preferences.features" value="analytics">
78
78
  <label class="form-check-label" for="feat-analytics">Analytics</label>
79
79
  </div>
80
80
  <div class="form-check">
81
- <input type="checkbox" class="form-check-input" id="feat-beta" name="preferences.features" value="beta" disabled>
81
+ <input type="checkbox" class="form-check-input" id="feat-beta" name="preferences.features" value="beta">
82
82
  <label class="form-check-label" for="feat-beta">Beta features</label>
83
83
  </div>
84
84
  </div>
85
85
  <div class="mb-3 form-check">
86
- <input type="checkbox" class="form-check-input" id="main-subscribe" name="settings.subscribe" disabled>
86
+ <input type="checkbox" class="form-check-input" id="main-subscribe" name="settings.subscribe">
87
87
  <label class="form-check-label" for="main-subscribe">settings.subscribe (single checkbox)</label>
88
88
  </div>
89
89
  <!-- Honeypot field for bot detection (fill to trigger rejection) -->
90
90
  <div class="mb-3">
91
91
  <label for="main-honey" class="form-label">honey <span class="badge bg-warning text-dark">Honeypot</span></label>
92
- <input type="text" class="form-control" id="main-honey" name="honey" data-honey placeholder="Fill to trigger bot rejection" autocomplete="off" tabindex="-1" disabled>
92
+ <input type="text" class="form-control" id="main-honey" name="honey" data-honey placeholder="Fill to trigger bot rejection" autocomplete="off" tabindex="-1">
93
93
  <small class="text-muted">Hidden in production; fill here to test rejection</small>
94
94
  </div>
95
95
  </div>
@@ -97,7 +97,7 @@ meta:
97
97
  <div class="d-flex gap-2">
98
98
  <button type="submit" data-action="save" class="btn btn-primary" disabled>Save</button>
99
99
  <button type="submit" data-action="draft" class="btn btn-outline-secondary" disabled>Save as Draft</button>
100
- <button type="button" id="main-set-data" class="btn btn-outline-info" disabled>Set data</button>
100
+ <button type="button" id="main-set-data" class="btn btn-outline-info">Set data</button>
101
101
  </div>
102
102
  </form>
103
103
  <div class="mt-3 d-flex gap-3">
@@ -122,21 +122,21 @@ meta:
122
122
  <button type="button" id="validation-set-correct" class="btn btn-sm btn-outline-success">Set correct</button>
123
123
  </div>
124
124
  <div class="card-body">
125
- <form id="test-form-validation">
125
+ <form id="test-form-validation" data-form-state="initializing" onsubmit="return false">
126
126
  <div class="mb-3">
127
127
  <label for="validation-name" class="form-label">Name (required)</label>
128
- <input type="text" class="form-control" id="validation-name" name="name" required disabled>
128
+ <input type="text" class="form-control" id="validation-name" name="name" required>
129
129
  </div>
130
130
  <div class="mb-3">
131
131
  <label for="validation-email" class="form-label">Email (required, valid format)</label>
132
- <input type="email" class="form-control" id="validation-email" name="email" required disabled>
132
+ <input type="email" class="form-control" id="validation-email" name="email" required>
133
133
  </div>
134
134
  <div class="mb-3">
135
135
  <label for="validation-age" class="form-label">Age (required, must be 18+)</label>
136
- <input type="number" class="form-control" id="validation-age" name="age" required min="1" disabled>
136
+ <input type="number" class="form-control" id="validation-age" name="age" required min="1">
137
137
  </div>
138
138
  <div class="mb-3 form-check">
139
- <input type="checkbox" class="form-check-input" id="validation-terms" name="terms" required disabled>
139
+ <input type="checkbox" class="form-check-input" id="validation-terms" name="terms" required>
140
140
  <label class="form-check-label" for="validation-terms">I agree to the terms (required)</label>
141
141
  </div>
142
142
  <button type="submit" class="btn btn-primary" disabled>Submit</button>
@@ -156,10 +156,10 @@ meta:
156
156
  <small class="text-muted">allowResubmit: false, resetOnSuccess: true</small>
157
157
  </div>
158
158
  <div class="card-body">
159
- <form id="test-form-contact">
159
+ <form id="test-form-contact" data-form-state="initializing" onsubmit="return false">
160
160
  <div class="mb-3">
161
161
  <label for="contact-message" class="form-label">Message</label>
162
- <textarea class="form-control" id="contact-message" name="message" rows="2" disabled>Hello!</textarea>
162
+ <textarea class="form-control" id="contact-message" name="message" rows="2">Hello!</textarea>
163
163
  </div>
164
164
  <button type="submit" class="btn btn-primary" disabled>Send message</button>
165
165
  </form>
@@ -178,10 +178,10 @@ meta:
178
178
  <small class="text-muted">autoReady: false, ready after 2 seconds</small>
179
179
  </div>
180
180
  <div class="card-body">
181
- <form id="test-form-manual">
181
+ <form id="test-form-manual" data-form-state="initializing" onsubmit="return false">
182
182
  <div class="mb-3">
183
183
  <label for="manual-data" class="form-label">Data</label>
184
- <input type="text" class="form-control" id="manual-data" name="data" disabled>
184
+ <input type="text" class="form-control" id="manual-data" name="data">
185
185
  </div>
186
186
  <button type="submit" class="btn btn-primary" disabled>Submit</button>
187
187
  </form>
@@ -200,18 +200,18 @@ meta:
200
200
  <small class="text-muted">setInputGroup() filters getData() - fields without data-input-group are global</small>
201
201
  </div>
202
202
  <div class="card-body">
203
- <form id="test-form-groups">
203
+ <form id="test-form-groups" data-form-state="initializing" onsubmit="return false">
204
204
  <div class="row">
205
205
  <!-- Global settings (no data-input-group = always included) -->
206
206
  <div class="col-md-4">
207
207
  <h6 class="text-muted mb-3">Global settings <span class="badge bg-secondary">Always included</span></h6>
208
208
  <div class="mb-3">
209
209
  <label for="groups-name" class="form-label">settings.name</label>
210
- <input type="text" class="form-control" id="groups-name" name="settings.name" value="My project" disabled>
210
+ <input type="text" class="form-control" id="groups-name" name="settings.name" value="My project">
211
211
  </div>
212
212
  <div class="mb-3">
213
213
  <label for="groups-theme" class="form-label">settings.theme</label>
214
- <select class="form-select" id="groups-theme" name="settings.theme" disabled>
214
+ <select class="form-select" id="groups-theme" name="settings.theme">
215
215
  <option value="light">Light</option>
216
216
  <option value="dark" selected>Dark</option>
217
217
  </select>
@@ -223,11 +223,11 @@ meta:
223
223
  <h6 class="text-primary mb-3">Group A <span class="badge bg-primary">data-input-group="a"</span></h6>
224
224
  <div class="mb-3">
225
225
  <label for="groups-a-url" class="form-label">options.url</label>
226
- <input type="url" class="form-control" id="groups-a-url" name="options.url" data-input-group="a" value="https://example.com" disabled>
226
+ <input type="url" class="form-control" id="groups-a-url" name="options.url" data-input-group="a" value="https://example.com">
227
227
  </div>
228
228
  <div class="mb-3">
229
229
  <label for="groups-a-title" class="form-label">options.title</label>
230
- <input type="text" class="form-control" id="groups-a-title" name="options.title" data-input-group="a" value="My link" disabled>
230
+ <input type="text" class="form-control" id="groups-a-title" name="options.title" data-input-group="a" value="My link">
231
231
  </div>
232
232
  </div>
233
233
 
@@ -236,11 +236,11 @@ meta:
236
236
  <h6 class="text-success mb-3">Group B <span class="badge bg-success">data-input-group="b"</span></h6>
237
237
  <div class="mb-3">
238
238
  <label for="groups-b-ssid" class="form-label">options.ssid</label>
239
- <input type="text" class="form-control" id="groups-b-ssid" name="options.ssid" data-input-group="b" value="MyWiFi" disabled>
239
+ <input type="text" class="form-control" id="groups-b-ssid" name="options.ssid" data-input-group="b" value="MyWiFi">
240
240
  </div>
241
241
  <div class="mb-3">
242
242
  <label for="groups-b-password" class="form-label">options.password</label>
243
- <input type="text" class="form-control" id="groups-b-password" name="options.password" data-input-group="b" value="secret123" disabled>
243
+ <input type="text" class="form-control" id="groups-b-password" name="options.password" data-input-group="b" value="secret123">
244
244
  </div>
245
245
  </div>
246
246
  </div>
@@ -273,7 +273,7 @@ meta:
273
273
  <small class="text-muted">data-file-drop (local) and data-file-drop="page"</small>
274
274
  </div>
275
275
  <div class="card-body">
276
- <form id="test-form-file-drop">
276
+ <form id="test-form-file-drop" data-form-state="initializing" onsubmit="return false">
277
277
  <h6 class="text-muted mb-2">Local mode <span class="badge bg-secondary">default</span></h6>
278
278
  <div class="p-4 text-center mb-3" data-file-drop>
279
279
  <p class="mb-2">Drag & drop or click to browse</p>
@@ -0,0 +1,13 @@
1
+ const { execute } = require('node-powertools');
2
+
3
+ let _hasSfw;
4
+ async function safeInstall(command, options) {
5
+ if (_hasSfw === undefined) {
6
+ _hasSfw = await execute('sfw --version', { log: false }).then(() => true).catch(() => false);
7
+ }
8
+ const isInstall = /^npm\s+(install|i)\b/.test(command);
9
+ const prefix = (_hasSfw && isInstall) ? 'sfw ' : '';
10
+ return execute(`${prefix}${command}`, options || { log: true });
11
+ }
12
+
13
+ module.exports = { safeInstall };
package/logs/test.log CHANGED
@@ -1,29 +1,29 @@
1
- # ujm log — 2026-06-18T20:43:18.296Z — pid=99488
2
- [13:43:18] 'test': Running tests (layer=all)
3
- [13:43:18] 'test': Test mode: normal (external APIs skipped)
1
+ # ujm log — 2026-06-22T21:31:38.629Z — pid=3348
2
+ [14:31:38] 'test': Running tests (layer=all)
3
+ [14:31:38] 'test': Test mode: normal (external APIs skipped)
4
4
 
5
5
  Ultimate Jekyll Manager Tests
6
6
 
7
7
  Framework Tests
8
8
  ⤷ attach-log-file — tee stdout/stderr to a file
9
- ✓ exports the expected surface (1ms)
9
+ ✓ exports the expected surface (0ms)
10
10
  ✓ stripAnsi removes color escape codes (0ms)
11
11
  hello world
12
12
  colored line
13
13
  ✓ attach + stdout.write + detach: file contains the writes (0ms)
14
- ✓ idempotent: attaching twice with same name returns same fd (0ms)
15
- ✓ attach with falsy name returns null and does nothing (1ms)
14
+ ✓ idempotent: attaching twice with same name returns same fd (1ms)
15
+ ✓ attach with falsy name returns null and does nothing (0ms)
16
16
  ⤷ CLI alias resolution
17
17
  ✓ cli.js exports a Main class (0ms)
18
18
  ✓ all expected commands exist on disk (0ms)
19
- ✓ each command module exports an async function (3ms)
19
+ ✓ each command module exports an async function (4ms)
20
20
  ⤷ collectTextNodes (utils/collectTextNodes.js)
21
- ✓ extracts page title (98ms)
21
+ ✓ extracts page title (101ms)
22
22
  ✓ skips <script> and <style> (0ms)
23
23
  ✓ spellcheck dictionary (utils/dictionary.js) (0ms)
24
24
  ⤷ expect() matcher set
25
25
  ✓ toBe + toEqual basics (0ms)
26
- ✓ .not negates (1ms)
26
+ ✓ .not negates (0ms)
27
27
  ✓ toContain works on arrays and strings (0ms)
28
28
  ✓ toThrow catches sync + async throws (0ms)
29
29
  ✓ toBeGreaterThan / toBeLessThan (0ms)
@@ -50,9 +50,9 @@ colored line
50
50
  ⤷ mergeJekyllConfigs (utils/merge-jekyll-configs.js)
51
51
  ✓ merges collections from both configs (project additions win) (3ms)
52
52
  ✓ dedups defaults by scope key (project wins) (1ms)
53
- ✓ returns null when there is nothing to merge (1ms)
53
+ ✓ returns null when there is nothing to merge (0ms)
54
54
  ⤷ mode-helpers (isTesting / isDevelopment / isProduction / getVersion)
55
- ✓ helpers attach to Manager statically AND on prototype (0ms)
55
+ ✓ helpers attach to Manager statically AND on prototype (1ms)
56
56
  ✓ isTesting reflects UJ_TEST_MODE env (0ms)
57
57
  ✓ isDevelopment false / isProduction true when UJ_BUILD_MODE=true (and not testing) (0ms)
58
58
  ✓ environments are mutually exclusive — testing wins under UJ_TEST_MODE (0ms)
@@ -60,7 +60,7 @@ colored line
60
60
  ✓ getVersion returns a non-empty string when run from a package (0ms)
61
61
  ⤷ createTemplateTransform (utils/template-transform.js)
62
62
  ✓ replaces [site.theme.id] with config value in .html files (1ms)
63
- ✓ leaves non-matching extensions untouched (e.g. .css) (1ms)
63
+ ✓ leaves non-matching extensions untouched (e.g. .css) (0ms)
64
64
  ✓ passes directories through untouched (0ms)
65
65
  ⤷ node-powertools templating brackets ({} and [])
66
66
  ✓ default { } brackets resolve nested keys (0ms)
@@ -69,57 +69,57 @@ colored line
69
69
  ⤷ theme contract (structure, swappability, cross-theme JS contracts)
70
70
  ✓ _template: entry files + config contract (1ms)
71
71
  ✓ classy: entry files + config contract (0ms)
72
- ✓ neobrutalism: entry files + config contract (0ms)
72
+ ✓ neobrutalism: entry files + config contract (1ms)
73
73
  ✓ newsflash: entry files + config contract (0ms)
74
74
  ✓ _template: layouts swappable, markup clean (1ms)
75
- ✓ classy: layouts swappable, markup clean (14ms)
76
- ✓ neobrutalism: layouts swappable, markup clean (2ms)
77
- ✓ newsflash: layouts swappable, markup clean (3ms)
75
+ ✓ classy: layouts swappable, markup clean (15ms)
76
+ ✓ neobrutalism: layouts swappable, markup clean (1ms)
77
+ ✓ newsflash: layouts swappable, markup clean (4ms)
78
78
  ✓ _template: cross-theme JS contracts (0ms)
79
79
  ✓ classy: cross-theme JS contracts (0ms)
80
80
  ✓ neobrutalism: cross-theme JS contracts (1ms)
81
81
  ✓ newsflash: cross-theme JS contracts (0ms)
82
- ✓ page asset files match a declared asset_path shape (4ms)
82
+ ✓ page asset files match a declared asset_path shape (3ms)
83
83
  ⤷ validateYAMLFrontMatter (utils/_validate-yaml.js)
84
84
  ✓ returns { valid: true } for a file with valid frontmatter (0ms)
85
85
  ✓ returns { valid: true } when no frontmatter present (1ms)
86
86
  ✓ flags malformed YAML frontmatter as invalid with error message (0ms)
87
87
  ⤷ page-layer baseline (DOM + fetch + storage)
88
88
  ✓ document is interactive or complete (1ms)
89
- ✓ fetch() works against the local harness server (8ms)
89
+ ✓ fetch() works against the local harness server (7ms)
90
90
  ✓ localStorage is available (1ms)
91
91
  ⤷ FormManager getData / setData / input groups
92
- ✓ _setNested builds nested objects from dot paths (0ms)
92
+ ✓ _setNested builds nested objects from dot paths (1ms)
93
93
  ✓ _setNested accumulates duplicate keys into arrays (0ms)
94
94
  ✓ _getNested reads nested values and returns undefined for missing paths (0ms)
95
95
  ✓ _flattenObject converts nested objects to dot paths (1ms)
96
96
  ✓ getData collects text, select, radio, and textarea values with dot notation (5ms)
97
97
  ✓ getData handles single checkbox (boolean) and checkbox groups (object) (0ms)
98
98
  ✓ getData excludes honeypot fields (0ms)
99
- ✓ input group filter includes matching + global fields, excludes others (1ms)
99
+ ✓ input group filter includes matching + global fields, excludes others (0ms)
100
100
  ✓ input group filter with multiple groups includes all matching (0ms)
101
- ✓ setData populates text, select, textarea fields (0ms)
101
+ ✓ setData populates text, select, textarea fields (1ms)
102
102
  ✓ setData sets radio group to matching value (0ms)
103
103
  ✓ setData sets single checkbox boolean and checkbox group values (0ms)
104
104
  ⤷ FormManager disabled-state snapshot
105
105
  ✓ snapshot captures disabled non-submit elements, ignores submit buttons (1ms)
106
106
  ✓ setDisabled(true) disables everything (0ms)
107
107
  ✓ setDisabled(false) re-enables managed elements but keeps snapshotted ones disabled (0ms)
108
- ✓ survives multiple disable/enable cycles (0ms)
108
+ ✓ survives multiple disable/enable cycles (1ms)
109
109
  ✓ onsubmit="return false" blocks native submission before FM loads (10ms)
110
110
  ⤷ FormManager validation + honeypot + file-accept
111
111
  ✓ required text field fails when empty, passes when filled (0ms)
112
- ✓ required checkbox fails when unchecked (1ms)
112
+ ✓ required checkbox fails when unchecked (0ms)
113
113
  ✓ required radio group fails when none checked (0ms)
114
- ✓ email validation rejects invalid formats and accepts valid ones (0ms)
115
- ✓ number min/max validation catches out-of-range values (1ms)
114
+ ✓ email validation rejects invalid formats and accepts valid ones (1ms)
115
+ ✓ number min/max validation catches out-of-range values (2ms)
116
116
  ✓ minlength and maxlength validation (0ms)
117
117
  ✓ pattern attribute validation (0ms)
118
118
  ✓ honeypot detection catches [data-honey] and [name="honey"] fields (0ms)
119
119
  ✓ honeypot detects data-honey without name="honey" (0ms)
120
120
  ✓ file-accept matching: extension, wildcard MIME, exact MIME (0ms)
121
- ✓ field error display adds is-invalid class and feedback element (0ms)
122
- ✓ query param population skips UTM/tracking params (1ms)
121
+ ✓ field error display adds is-invalid class and feedback element (1ms)
122
+ ✓ query param population skips UTM/tracking params (0ms)
123
123
  ✓ data-form-state attribute reflects state transitions (0ms)
124
124
  ⤷ harness globals (window.Configuration + dataset)
125
125
  ✓ window.Configuration has brand + theme + web_manager (0ms)
@@ -129,16 +129,16 @@ colored line
129
129
  ✓ template#prerendered-icons exists and has the test icon (1ms)
130
130
  ✓ looking up a missing icon returns null (0ms)
131
131
  ⤷ boot tests (consumer _site/)
132
- ✓ /service-worker.js served with javascript content type (130ms)
133
- ✓ index.html registers SW and reaches activated state (182ms)
134
- ✓ SW responds to get-cache-name message with brand-id pattern (86ms)
135
- ✓ home page renders with title + body content (187ms)
136
- ✓ /about resolves via Jekyll-style .html fallback (73ms)
137
- ✓ build.json is served with brand metadata (76ms)
138
- ✓ CSS bundle served with text/css content type (87ms)
132
+ ✓ /service-worker.js served with javascript content type (74ms)
133
+ ✓ index.html registers SW and reaches activated state (169ms)
134
+ ✓ SW responds to get-cache-name message with brand-id pattern (75ms)
135
+ ✓ home page renders with title + body content (176ms)
136
+ ✓ /about resolves via Jekyll-style .html fallback (67ms)
137
+ ✓ build.json is served with brand metadata (69ms)
138
+ ✓ CSS bundle served with text/css content type (62ms)
139
139
 
140
140
  Results
141
141
  110 passing
142
142
 
143
- Total: 110 tests in 3274ms
143
+ Total: 110 tests in 2945ms
144
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -0,0 +1,113 @@
1
+ # Fix: Translation exclude list not respected by footer language switcher
2
+
3
+ ## Problem
4
+
5
+ Every UJM site with translation enabled generates dead language links on excluded pages (e.g. blog posts), causing widespread 404s indexed by Google.
6
+
7
+ **Root cause:** The footer language switcher and the `uj_translation_url` Ruby tag both generate language-prefixed URLs (`/es/blog/my-post`, `/ko/blog/my-post`) without checking the `translation.exclude` list. Meanwhile, the translation gulp task correctly skips excluded pages — so the translated HTML files are never generated, but the links to them are.
8
+
9
+ **Impact:** Every blog post on every site with translation enabled has footer links to non-existent translated pages. Google discovers these via crawling, indexes them as 404s, and inflates "Page not found" stats in GA4 (often 40-50% of all pageviews). Also harms crawl budget.
10
+
11
+ **Example (operst):**
12
+ - Config: `translation.enabled: true`, `exclude: ["blog"]`, languages: `es`, `ko`
13
+ - Footer on `/blog/my-post` links to `/es/blog/my-post` and `/ko/blog/my-post`
14
+ - Neither of those pages exist — both return 404
15
+ - GA4 shows "Oops! This page doesn't exist" as the #1 page at 45% of traffic
16
+
17
+ ## Fix 1: Footer template (DONE)
18
+
19
+ **File:** `src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html`
20
+
21
+ **Change:** Before rendering the language dropdown, check the current page against the exclude list. If matched, only show the default language (same behavior as when translation is disabled).
22
+
23
+ ```liquid
24
+ {% assign page_excluded_from_translation = false %}
25
+ {% for exclude in site.translation.exclude %}
26
+ {% assign exclude_folder = "/" | append: exclude | append: "/" %}
27
+ {% assign exclude_page = "/" | append: exclude %}
28
+ {% if page.url == exclude_page or page.url contains exclude_folder %}
29
+ {% assign page_excluded_from_translation = true %}
30
+ {% endif %}
31
+ {% endfor %}
32
+ {% if site.translation.enabled and site.translation.languages.size > 0 and page_excluded_from_translation != true %}
33
+ {% assign all_languages = site.translation.languages | push: default_language %}
34
+ {% else %}
35
+ {% assign all_languages = "" | split: "" | push: default_language %}
36
+ {% endif %}
37
+ ```
38
+
39
+ **Matching logic:** Uses the same pattern as `translation.js` — each exclude entry matches as both a folder prefix (`/blog/...`) and an exact page path (`/blog`).
40
+
41
+ **Status:** Applied to `src/defaults/dist/_includes/themes/classy/frontend/sections/footer.html`. Needs `npm run prepare` to copy to `dist/`.
42
+
43
+ ## Fix 2: `uj_translation_url` Ruby tag (DONE)
44
+
45
+ **File:** `jekyll-uj-powertools` gem, `lib/tags/translation_url.rb`
46
+
47
+ **Current code (v1.7.9):** The `render` method generates language-prefixed URLs without checking the exclude list at all. When called with `{% uj_translation_url "es", page.url %}` on a blog post, it returns `/es/blog/my-post` regardless of whether blog is excluded.
48
+
49
+ **Proposed change:** Add an exclude check in the `render` method. If the URL path matches an exclude entry, return the un-prefixed (default language) URL instead of the language-prefixed one. This is defense-in-depth — even if a template forgets to check, the tag itself won't generate dead links.
50
+
51
+ ```ruby
52
+ def render(context)
53
+ # ... existing argument parsing ...
54
+
55
+ # Get site and translation config from context
56
+ site = context.registers[:site]
57
+ return '/' unless site
58
+
59
+ translation_config = site.config['translation'] || {}
60
+ default_language = translation_config['default'] || 'en'
61
+ available_languages = translation_config['languages'] || [default_language]
62
+
63
+ # NEW: Check if the URL path is excluded from translation
64
+ excludes = translation_config['exclude'] || []
65
+ normalized_path = normalize_path(url_path)
66
+ if page_excluded?(normalized_path, excludes)
67
+ # Return un-prefixed URL for excluded pages
68
+ return normalized_path.empty? ? '/' : "/#{normalized_path}"
69
+ end
70
+
71
+ # ... rest of existing logic (validate language, generate URL) ...
72
+ end
73
+
74
+ private
75
+
76
+ # NEW: Check if a page path matches any exclude entry
77
+ def page_excluded?(normalized_path, excludes)
78
+ return false if excludes.empty? || normalized_path.empty?
79
+
80
+ excludes.any? do |exclude|
81
+ # Match as exact page path or as folder prefix
82
+ normalized_path == exclude || normalized_path.start_with?("#{exclude}/")
83
+ end
84
+ end
85
+ ```
86
+
87
+ **Applied:** `jekyll-uj-powertools` v1.7.11. Version bumped in gemspec + CHANGELOG updated.
88
+
89
+ ## Fix 3: Consider — strip language dropdown entirely on excluded pages
90
+
91
+ The current Fix 1 still renders a language dropdown with a single "English" option on excluded pages. An alternative is to hide the dropdown entirely when the page is excluded. This is a UX call — showing a single-language dropdown is harmless but arguably useless.
92
+
93
+ To hide it entirely, wrap the `<div class="dropup uj-language-dropdown">` block in:
94
+ ```liquid
95
+ {% unless page_excluded_from_translation and site.translation.enabled %}
96
+ <!-- language dropdown markup -->
97
+ {% endunless %}
98
+ ```
99
+
100
+ ## Verification
101
+
102
+ After applying Fix 1 and rebuilding a site:
103
+
104
+ 1. Visit a blog post → footer should show only "English" (or no language dropdown)
105
+ 2. Visit a non-excluded page (e.g. `/pricing`) → footer should show all languages
106
+ 3. View page source on a blog post → no `href="/es/blog/..."` links in footer
107
+ 4. After reindexing, GA4 "Page not found" percentage should drop significantly
108
+
109
+ ## Scope
110
+
111
+ - Fix 1 covers ALL UJM sites (classy is the base footer for all themes)
112
+ - Fix 2 covers any future template that uses `uj_translation_url` on excluded pages
113
+ - The spam bot traffic (`/ar/`, `/fr/`, etc. from disabled languages) is separate — those are fabricated URLs from crawlers, not caused by UJM. A Cloudflare WAF rule can block those if desired.