ultimate-jekyll-manager 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +2 -1
- package/PROGRESS.md +24 -0
- package/dist/assets/js/libs/form-manager.js +18 -1
- package/dist/assets/js/pages/test/libraries/form-manager/index.js +53 -0
- package/dist/assets/js/pages/token/index.js +37 -3
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +72 -0
- package/dist/test/suites/page/form-manager-data.test.js +418 -0
- package/dist/test/suites/page/form-manager-disabled.test.js +170 -0
- package/dist/test/suites/page/form-manager-validation.test.js +374 -0
- package/docs/cdp-debugging.md +48 -0
- package/docs/javascript-libraries.md +18 -0
- package/docs/local-development.md +1 -1
- package/docs/themes.md +3 -2
- package/logs/test.log +68 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,37 @@ 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.1] - 2026-06-17
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **FormManager: snapshot-and-restore disabled-state model (replaces `data-fm-keep-disabled`).** `_init()` now snapshots every element that has `disabled` in HTML markup (excluding submit buttons — those are loading guards FM takes over). Snapshotted elements stay disabled through every state transition automatically — no data attributes needed. Recommended form pattern: `<form data-form-state="initializing" onsubmit="return false">` with CSS `form[data-form-state]:not([data-form-state="ready"]) { pointer-events: none; }`. Documented in `docs/javascript-libraries.md`.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Comprehensive FormManager test suite (30 page-layer tests).** Disabled-state snapshot (5 tests), getData/setData/input groups (12 tests), validation/honeypot/file-accept (13 tests). Previously FormManager had zero automated tests.
|
|
27
|
+
- **Visual Test 7 on the FM test page** (`/test/libraries/form-manager`) — permanently disabled elements, submit cycle demo, rapid-cycle button for manual verification.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
## [1.9.0] - 2026-06-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Token page: MCP OAuth flow.** When the `/token` page receives `?mcp=true&redirect_uri=...&state=...`, it completes an OAuth-style handshake — the user signs in via Firebase Auth, the page obtains a fresh ID token, and redirects back to the `redirect_uri` with `code=<idToken>&state=<state>`. This lets Claude (or any MCP client) authenticate users through the existing sign-in UI without a custom OAuth server.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **Dev-URL guidance updated** — docs (local-development.md, themes.md, cdp-debugging.md, CLAUDE.md) now say "prefer `https://localhost:4000`; fall back to the network IP if localhost doesn't connect" instead of hardcoding a specific IP.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
## [1.8.2] - 2026-06-14
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **FormManager: `data-fm-keep-disabled` opt-out for permanently-disabled fields.** `_setDisabled` blanket-toggles `disabled` on every control in the form (loading/submitting ↔ ready), which silently re-enabled fields meant to STAY disabled — e.g. "coming soon" radio options rendered inside a managed form. Controls carrying `data-fm-keep-disabled` are now always forced to `disabled = true` and never re-enabled. Documented in `docs/javascript-libraries.md` (FormManager section).
|
|
46
|
+
- **`docs/cdp-debugging.md` — launching a controllable browser (mirrored across UJM/BEM/BXM/EM).** The canonical Chrome launch for agents and humans: CDP port + REQUIRED dedicated `--user-data-dir` (Chrome 136+ silently ignores the debug port on the default profile — verified on 149), the persistent agent profile (`~/Library/Application Support/chrome-profiles/agent` — log in once, state survives relaunches, verified), the shared-instance model (CDP is multi-client — agents share the one logged-in Chrome on one port, one tab each; a second profile/port only for a second identity), safe quit by profile match, and driving via the `chrome-devtools` MCP (`CHROME_CDP_PORT` set before the session) or any CDP client. UJM flavor: point it at the dev server for the edit → live-reload → screenshot/console/network loop. Indexed in CLAUDE.md.
|
|
47
|
+
|
|
17
48
|
---
|
|
18
49
|
## [1.8.1] - 2026-06-11
|
|
19
50
|
|
package/CLAUDE.md
CHANGED
|
@@ -174,7 +174,7 @@ Note: `-t` short alias belongs to `translation`. The `test` command uses `--test
|
|
|
174
174
|
## Development Workflow
|
|
175
175
|
|
|
176
176
|
- **🚫 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
|
-
- **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. See `~/.claude/mcp-server/servers/chrome-devtools/CLAUDE.md`.
|
|
177
|
+
- **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
178
|
|
|
179
179
|
## File Conventions
|
|
180
180
|
|
|
@@ -218,6 +218,7 @@ Deep references live in `docs/`. Treat docs as a first-class deliverable. **When
|
|
|
218
218
|
- [docs/build-system.md](docs/build-system.md) — gulp pipeline (15 tasks), config flow, build modes, pure helpers
|
|
219
219
|
- [docs/templating.md](docs/templating.md) — node-powertools bracket conventions, Liquid coexistence
|
|
220
220
|
- [docs/local-development.md](docs/local-development.md) — browsersync URL, Firebase emulator connect, PurgeCSS safelist
|
|
221
|
+
- [docs/cdp-debugging.md](docs/cdp-debugging.md) — launching a controllable Chrome (CDP) at the dev server, persistent agent profile (one-time logins), driving via MCP/CDP (screenshots, clicks, network)
|
|
221
222
|
- [docs/logging.md](docs/logging.md) — `dev.log` / `build.log` / `test.log` tee, CI skip
|
|
222
223
|
- [docs/common-mistakes.md](docs/common-mistakes.md) — the canonical "don't do this" list
|
|
223
224
|
- [docs/assets.md](docs/assets.md) — UJM vs consumer file layout, section config (nav/footer/account), frontmatter-driven page customization, webpack aliases, page module pattern
|
package/PROGRESS.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Project Progress Tracker
|
|
2
|
+
> Agents and maintainers should update this file regularly to reflect the current state of the project.
|
|
3
|
+
|
|
4
|
+
## Current Focus
|
|
5
|
+
* **Goal:** FormManager disabled-state refactor (snapshot-and-restore)
|
|
6
|
+
* **Current Phase:** Phase 1 — implementation + tests complete, docs pending
|
|
7
|
+
* **Priority:** Medium
|
|
8
|
+
* **Last Updated:** 2026-06-17 6:05 PM PDT
|
|
9
|
+
* **Notes:** FM disabled-state refactor done + comprehensive FM test suite added (110 tests total, up from 80). Covers getData/setData/input groups/validation/honeypot/file-accept/disabled snapshot. Docs (javascript-libraries.md, CHANGELOG) still need updating before shipping.
|
|
10
|
+
|
|
11
|
+
## Active Task List
|
|
12
|
+
* [ ] Phase 1: FormManager disabled-state snapshot-and-restore
|
|
13
|
+
* [x] Task 1.1: Refactor `_setDisabled` to use snapshot instead of `data-fm-keep-disabled`
|
|
14
|
+
* [x] Task 1.2: Add `_permanentlyDisabled` Set, populated in `_init()` before first disable
|
|
15
|
+
* [x] Task 1.3: Write page-layer tests (5 tests: snapshot capture, full disable, selective re-enable, cycle durability, onsubmit HTML guard)
|
|
16
|
+
* [x] Task 1.4: All 85 tests passing
|
|
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
|
+
* [x] Task 1.6: Write comprehensive FM page-layer tests — getData/setData (12 tests), validation/honeypot/file-accept (13 tests). 110 total.
|
|
19
|
+
* [ ] 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
|
+
* [ ] Task 1.8: Update CHANGELOG with the change
|
|
21
|
+
* [ ] Task 1.9: Ship (commit, push, publish)
|
|
22
|
+
|
|
23
|
+
## Completed Task List
|
|
24
|
+
* [x] Phase 0: v1.9.0 release — MCP OAuth flow + CDP debugging docs + dev-URL updates
|
|
@@ -59,6 +59,7 @@ export class FormManager {
|
|
|
59
59
|
// State
|
|
60
60
|
this.state = 'initializing';
|
|
61
61
|
this._isDirty = false;
|
|
62
|
+
this._permanentlyDisabled = new Set();
|
|
62
63
|
|
|
63
64
|
// Event listeners
|
|
64
65
|
this._listeners = {
|
|
@@ -92,6 +93,17 @@ export class FormManager {
|
|
|
92
93
|
* Initialize the form manager
|
|
93
94
|
*/
|
|
94
95
|
_init() {
|
|
96
|
+
// Snapshot elements that are disabled in HTML markup BEFORE the first
|
|
97
|
+
// blanket disable. These are business-logic disabled (e.g. "coming soon"
|
|
98
|
+
// options) and must stay disabled through every state transition.
|
|
99
|
+
// Submit buttons are excluded — disabled submit buttons in HTML are
|
|
100
|
+
// loading guards that FM takes over.
|
|
101
|
+
this.$form.querySelectorAll('button, input, select, textarea').forEach(($el) => {
|
|
102
|
+
if ($el.disabled && $el.type !== 'submit') {
|
|
103
|
+
this._permanentlyDisabled.add($el);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
95
107
|
// Disable form during initialization
|
|
96
108
|
this._setDisabled(true);
|
|
97
109
|
|
|
@@ -782,7 +794,8 @@ export class FormManager {
|
|
|
782
794
|
}
|
|
783
795
|
|
|
784
796
|
/**
|
|
785
|
-
* Enable/disable form controls
|
|
797
|
+
* Enable/disable form controls. Elements snapshotted as permanently
|
|
798
|
+
* disabled during _init() are never re-enabled.
|
|
786
799
|
*/
|
|
787
800
|
_setDisabled(disabled) {
|
|
788
801
|
/* @dev-only:start */
|
|
@@ -792,6 +805,10 @@ export class FormManager {
|
|
|
792
805
|
/* @dev-only:end */
|
|
793
806
|
|
|
794
807
|
this.$form.querySelectorAll('button, input, select, textarea').forEach(($el) => {
|
|
808
|
+
if (this._permanentlyDisabled.has($el)) {
|
|
809
|
+
$el.disabled = true;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
795
812
|
$el.disabled = disabled;
|
|
796
813
|
});
|
|
797
814
|
}
|
|
@@ -19,6 +19,7 @@ export default () => {
|
|
|
19
19
|
initTestFormManual();
|
|
20
20
|
initTestFormGroups();
|
|
21
21
|
initTestFormFileDrop();
|
|
22
|
+
initTestFormSnapshot();
|
|
22
23
|
|
|
23
24
|
// Resolve after initialization
|
|
24
25
|
return resolve();
|
|
@@ -245,6 +246,58 @@ function initTestFormGroups() {
|
|
|
245
246
|
});
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
// Test 7: Disabled-State Snapshot
|
|
250
|
+
function initTestFormSnapshot() {
|
|
251
|
+
const formManager = new FormManager('#test-form-snapshot');
|
|
252
|
+
const $status = document.getElementById('snapshot-status');
|
|
253
|
+
const $cycleCount = document.getElementById('snapshot-cycle-count');
|
|
254
|
+
const $output = document.getElementById('snapshot-output');
|
|
255
|
+
const $cycleBtn = document.getElementById('snapshot-cycle');
|
|
256
|
+
|
|
257
|
+
let cycles = 0;
|
|
258
|
+
|
|
259
|
+
function logStates() {
|
|
260
|
+
const form = document.getElementById('test-form-snapshot');
|
|
261
|
+
const lines = [];
|
|
262
|
+
form.querySelectorAll('input, select, textarea, button').forEach(($el) => {
|
|
263
|
+
const label = $el.name || $el.type || $el.tagName.toLowerCase();
|
|
264
|
+
lines.push(`${label}: disabled=${$el.disabled}`);
|
|
265
|
+
});
|
|
266
|
+
$output.textContent = lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
formManager.on('statechange', ({ state }) => {
|
|
270
|
+
$status.textContent = `Status: ${state}`;
|
|
271
|
+
logStates();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
formManager.on('submit', async ({ data }) => {
|
|
275
|
+
console.log('[Test 7] Submitting:', data);
|
|
276
|
+
logStates();
|
|
277
|
+
await simulateApi(2000);
|
|
278
|
+
cycles++;
|
|
279
|
+
$cycleCount.textContent = `Cycles: ${cycles}`;
|
|
280
|
+
formManager.showSuccess('Done! Permanently disabled fields should still be disabled.');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Rapid cycle button — triggers 5 fast disable/enable cycles
|
|
284
|
+
$cycleBtn.addEventListener('click', async () => {
|
|
285
|
+
$cycleBtn.disabled = true;
|
|
286
|
+
for (let i = 0; i < 5; i++) {
|
|
287
|
+
formManager._setDisabled(true);
|
|
288
|
+
logStates();
|
|
289
|
+
await simulateApi(300);
|
|
290
|
+
formManager._setDisabled(false);
|
|
291
|
+
logStates();
|
|
292
|
+
await simulateApi(300);
|
|
293
|
+
cycles++;
|
|
294
|
+
}
|
|
295
|
+
$cycleCount.textContent = `Cycles: ${cycles}`;
|
|
296
|
+
$cycleBtn.disabled = false;
|
|
297
|
+
formManager.showSuccess('5 rapid cycles complete. Check that Enterprise/Region/Notes stayed disabled.');
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
248
301
|
// Test 6: File Drop
|
|
249
302
|
function initTestFormFileDrop() {
|
|
250
303
|
const formManager = new FormManager('#test-form-file-drop');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// This file is required by /token page to generate custom auth tokens for extensions/apps
|
|
2
|
+
// Also handles MCP OAuth flow: user signs in → Firebase ID token sent back to Claude as auth code
|
|
2
3
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
3
4
|
import webManager from 'web-manager';
|
|
4
5
|
|
|
@@ -11,19 +12,27 @@ export default function () {
|
|
|
11
12
|
// Get URL params
|
|
12
13
|
const url = new URL(window.location.href);
|
|
13
14
|
const authReturnUrl = url.searchParams.get('authReturnUrl');
|
|
15
|
+
const mcpRedirectUri = url.searchParams.get('redirect_uri');
|
|
16
|
+
const mcpState = url.searchParams.get('state');
|
|
17
|
+
const isMcp = url.searchParams.get('mcp') === 'true';
|
|
14
18
|
|
|
15
19
|
// Handle DOM ready
|
|
16
20
|
webManager.dom().ready()
|
|
17
21
|
.then(async () => {
|
|
18
22
|
// Log
|
|
19
|
-
console.log('[Token] Initialized. authReturnUrl:', authReturnUrl);
|
|
23
|
+
console.log('[Token] Initialized.', isMcp ? 'MCP OAuth flow' : 'Standard flow', 'authReturnUrl:', authReturnUrl);
|
|
20
24
|
|
|
21
|
-
// Validate
|
|
25
|
+
// Validate redirect URLs
|
|
22
26
|
if (authReturnUrl && !webManager.isValidRedirectUrl(authReturnUrl)) {
|
|
23
27
|
showError('Invalid redirect URL');
|
|
24
28
|
return;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
if (isMcp && !mcpRedirectUri) {
|
|
32
|
+
showError('Missing redirect_uri for MCP OAuth flow');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
// Wait for auth to be ready and get user
|
|
28
37
|
webManager.auth().listen({ once: true }, async (state) => {
|
|
29
38
|
const user = state.user;
|
|
@@ -35,7 +44,32 @@ export default function () {
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
try {
|
|
38
|
-
//
|
|
47
|
+
// MCP OAuth flow: return Firebase ID token as the authorization code
|
|
48
|
+
if (isMcp && mcpRedirectUri) {
|
|
49
|
+
updateStatus('Completing MCP authorization...');
|
|
50
|
+
|
|
51
|
+
const idToken = await webManager.auth().getIdToken(true);
|
|
52
|
+
const returnUrl = new URL(mcpRedirectUri);
|
|
53
|
+
returnUrl.searchParams.set('code', idToken);
|
|
54
|
+
|
|
55
|
+
if (mcpState) {
|
|
56
|
+
returnUrl.searchParams.set('state', mcpState);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const redirectUrl = returnUrl.toString();
|
|
60
|
+
console.log('[Token] MCP redirect to:', redirectUrl);
|
|
61
|
+
|
|
62
|
+
updateStatus('Redirecting to Claude...');
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
updateStatus('If you were not redirected, <a href="' + webManager.utilities().escapeHTML(redirectUrl) + '">click here to try again</a>.', true);
|
|
66
|
+
}, 3000);
|
|
67
|
+
|
|
68
|
+
window.location.href = redirectUrl;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Standard flow: generate custom token via BEM API
|
|
39
73
|
updateStatus('Generating secure token...');
|
|
40
74
|
const token = await generateCustomToken();
|
|
41
75
|
|
|
@@ -301,6 +301,78 @@ meta:
|
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
|
|
304
|
+
<!-- Test 7: Disabled-State Snapshot -->
|
|
305
|
+
<div class="col-lg-8">
|
|
306
|
+
<div class="card">
|
|
307
|
+
<div class="card-header">
|
|
308
|
+
<h5 class="mb-0">Test 7: Disabled-State Snapshot</h5>
|
|
309
|
+
<small class="text-muted">Elements disabled in HTML stay disabled through every FM state transition. Submit buttons are loading guards — FM takes them over.</small>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="card-body">
|
|
312
|
+
<form id="test-form-snapshot" data-form-state="initializing" onsubmit="return false">
|
|
313
|
+
<div class="row">
|
|
314
|
+
<div class="col-md-6">
|
|
315
|
+
<h6 class="text-muted mb-3">FM-managed <span class="badge bg-success">enabled on ready</span></h6>
|
|
316
|
+
<div class="mb-3">
|
|
317
|
+
<label for="snapshot-name" class="form-label">Name</label>
|
|
318
|
+
<input type="text" class="form-control" id="snapshot-name" name="name" value="Ian">
|
|
319
|
+
</div>
|
|
320
|
+
<div class="mb-3">
|
|
321
|
+
<label for="snapshot-email" class="form-label">Email</label>
|
|
322
|
+
<input type="email" class="form-control" id="snapshot-email" name="email" value="ian@example.com">
|
|
323
|
+
</div>
|
|
324
|
+
<div class="mb-3">
|
|
325
|
+
<label class="form-label d-block">Plan</label>
|
|
326
|
+
<div class="form-check">
|
|
327
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-free" name="plan" value="free" checked>
|
|
328
|
+
<label class="form-check-label" for="snapshot-plan-free">Free</label>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="form-check">
|
|
331
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-pro" name="plan" value="pro">
|
|
332
|
+
<label class="form-check-label" for="snapshot-plan-pro">Pro</label>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="form-check">
|
|
335
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-enterprise" name="plan" value="enterprise" disabled>
|
|
336
|
+
<label class="form-check-label" for="snapshot-plan-enterprise">Enterprise <span class="badge bg-secondary">Coming soon</span></label>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="col-md-6">
|
|
341
|
+
<h6 class="text-muted mb-3">Permanently disabled <span class="badge bg-danger">stays disabled</span></h6>
|
|
342
|
+
<div class="mb-3">
|
|
343
|
+
<label for="snapshot-region" class="form-label">Region <span class="badge bg-secondary">Coming soon</span></label>
|
|
344
|
+
<select class="form-select" id="snapshot-region" name="region" disabled>
|
|
345
|
+
<option>US (coming soon)</option>
|
|
346
|
+
<option>EU (coming soon)</option>
|
|
347
|
+
</select>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="mb-3">
|
|
350
|
+
<label for="snapshot-notes" class="form-label">Internal notes <span class="badge bg-secondary">Admin only</span></label>
|
|
351
|
+
<textarea class="form-control" id="snapshot-notes" name="notes" rows="2" disabled>Restricted field</textarea>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="alert alert-info small mb-0">
|
|
354
|
+
Enterprise radio, Region select, and Notes textarea have <code>disabled</code> in the HTML. FM snapshots them at init and never re-enables them. The other fields start without <code>disabled</code> — the form-level <code>data-form-state</code> + CSS blocks interaction until FM is ready.
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="d-flex gap-2">
|
|
359
|
+
<button type="submit" class="btn btn-primary" disabled>
|
|
360
|
+
<span class="button-text">Submit (watch disabled states)</span>
|
|
361
|
+
</button>
|
|
362
|
+
<button type="button" id="snapshot-cycle" class="btn btn-outline-warning">Rapid cycle (5x)</button>
|
|
363
|
+
</div>
|
|
364
|
+
</form>
|
|
365
|
+
<div class="mt-3 d-flex gap-3">
|
|
366
|
+
<small class="text-muted" id="snapshot-status">Status: initializing</small>
|
|
367
|
+
<small class="text-muted" id="snapshot-cycle-count">Cycles: 0</small>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="mt-2">
|
|
370
|
+
<pre id="snapshot-output" class="bg-body-secondary p-2 rounded small" style="max-height: 160px; overflow: auto;">Submit to see disabled states toggle. Enterprise radio + Region select should NEVER re-enable.</pre>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
304
376
|
</div>
|
|
305
377
|
</div>
|
|
306
378
|
</section>
|