diff --git a/CHANGELOG.md b/CHANGELOG.md
index d25ab956b..7adb6bc8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,25 @@ Current truth lives in:
Historical changelog details remain available through git history and release
evidence.
+## 0.42.0-alpha.3
+
+- Revalidation continuity (ADR-0120): enhanced forms (`data-open-enhance`)
+ now morph the returned document into place instead of reloading —
+ submission returns the same HTML the no-JS path renders (303/422), the
+ client morphs it, and `history.pushState` follows the PRG target.
+- Island survival: a hydrated island whose light-DOM surface is unchanged
+ in the incoming document keeps its shadow state (the DSD template child
+ is excluded from the comparison). Islands whose surface changed are
+ replaced; `data-open-preserve` exempts any subtree; the island client
+ script is never re-executed by a morph.
+- Named regions: a `data-open-region` container limits the morph to the
+ matching region when present.
+- The ActionResult JSON path remains for custom clients; the built-in
+ enhancement uses HTML morphing for continuity.
+- Fixture proof: counter island at 3 survives a 422 morph and a PRG
+ morph; static/request-time mixed navigation — Chromium, Firefox and
+ WebKit, 36/36.
+
## 0.42.0-alpha.2
- The form/action loop (ADR-0120): plain HTML forms work without
diff --git a/README.md b/README.md
index 1286a1e25..323c371f5 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Elements are the durable application contract; JSX and Basic Element are the
authoring layer; Declarative Shadow DOM is the default server representation;
interactive regions upgrade selectively.
-Published package line: `0.42.0-alpha.2` (`v0.42.0-alpha.2`) — the stable five-package
+Published package line: `0.42.0-alpha.3` (`v0.42.0-alpha.3`) — the stable five-package
release under ADR-0119's scoped interface freeze; the abandoned beta naming
is not an active line.
diff --git a/README.zh.md b/README.zh.md
index 4f5c955f7..38cc3267a 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -6,7 +6,7 @@
Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层;
Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。
-已发布包线为 `0.42.0-alpha.2`(`v0.42.0-alpha.2`)——ADR-0119 范围化接口冻结下的
+已发布包线为 `0.42.0-alpha.3`(`v0.42.0-alpha.3`)——ADR-0119 范围化接口冻结下的
stable 五包版本;已放弃的 beta 命名不再是当前版本线。
## 当前产品
diff --git a/docs/current/VERSION_PLAN.md b/docs/current/VERSION_PLAN.md
index 14d59682b..eaeab4e6a 100644
--- a/docs/current/VERSION_PLAN.md
+++ b/docs/current/VERSION_PLAN.md
@@ -1,7 +1,7 @@
# v0.42.0 — WC Application Loop release plan
-> Current source package line: `v0.42.0-alpha.2`\
-> Current npm registry line: `v0.42.0-alpha.2`\
+> Current source package line: `v0.42.0-alpha.3`\
+> Current npm registry line: `v0.42.0-alpha.3`\
> Active release target: `v0.41.1`\
> Planning release target: `v0.42.0` (WC Application Loop)\
> Next release line: `v0.43.0` (Universal WC SSR)\
@@ -152,10 +152,15 @@ coexistence.
3. Static/request-time mixed sites: navigation and hydration between the
two route kinds do not interfere; i18n canonical-route gates extended
to request-time routes.
- 4. www dogfood: one real loop scenario (a site form) ships on the
- request-time path.
+ 4. Mixed-mode dogfood: the request-time fixture ships a page combining a
+ dynamic form route, a static route and a hydrated island. (Amended
+ 2026-07-27 from "www dogfood": www deploys to Cloudflare Pages as a
+ pure-static site and doubles as the byte-identical regression vehicle;
+ adding a dynamic route there would destroy both. The i18n gate
+ extension of step 3 defers to the first site that combines i18n with
+ request-time routes — none exists yet.)
- 准出: the island-survival matrix passes in three engines; the mixed-mode
- site e2e is green; the www dogfood scenario is live and linked in the
+ site e2e is green; the dogfood fixture is green in CI and linked in the
alpha.3 release note; alpha.3 published.
### TP-5 — `0.42.0-alpha.4` hardening, recipes and starter
diff --git a/docs/governance/PROJECT_WORKFLOW.md b/docs/governance/PROJECT_WORKFLOW.md
index f6c29a9ac..b12f635c3 100644
--- a/docs/governance/PROJECT_WORKFLOW.md
+++ b/docs/governance/PROJECT_WORKFLOW.md
@@ -11,8 +11,8 @@ complete because an issue, chat message, or SOP says it is complete. It is
complete only when the repository contains the decision, the execution package,
the implementation, and the gates that prove the claim.
-Current execution anchor: published package line `v0.42.0-alpha.2`, completed
-implementation anchor `v0.42.0-alpha.2`, and `0.42.0` WC Application Loop planning
+Current execution anchor: published package line `v0.42.0-alpha.3`, completed
+implementation anchor `v0.42.0-alpha.3`, and `0.42.0` WC Application Loop planning
under ADR-0120 and `docs/current/VERSION_PLAN.md`.
OpenElement is one Web Components-native,
static-first application framework: Basic Element is an authoring mode, not a
diff --git a/docs/release/v0.42.0-alpha.2.md b/docs/release/v0.42.0-alpha.2.md
index df69a67d4..e180cca38 100644
--- a/docs/release/v0.42.0-alpha.2.md
+++ b/docs/release/v0.42.0-alpha.2.md
@@ -1,5 +1,48 @@
# v0.42.0-alpha.2
+## The form/action loop
+
+The second alpha of the 0.42 line (ADR-0120): plain HTML forms work
+without JavaScript on `rendering: 'dynamic'` routes — the WC Application
+Loop's action half is complete.
+
+- **Protocol**: actions run before loaders (revalidation invariant);
+ `fail(4xx, data)` returns take the 422 re-render channel with submitted
+ values echoed; successful mutations answer **303 (PRG)** — never a 200
+ render — with 302s coerced up; POST to a route without an action is a
+ defined 404.
+- **Named actions** dispatch via `formaction='?/name'`
+ (`export const actions = { name(ctx) { ... } }`); unknown names are a
+ defined 404.
+- **JavaScript path**: fetch callers (`x-openelement-action` header)
+ receive the `ActionResult` discriminated union, and the island client
+ entry enhances `data-open-enhance` forms with the same protocol and no
+ DOM surgery — unhydrated islands are never touched; failure falls back
+ to the native 422 render unless the page handles `open:action-failure`.
+- **Contract**: an action must be safe to re-run after a failed
+ validation (validate first, mutate after).
+
+## Verification
+
+- Full form loop with `javaScriptEnabled: false` (422 echo, PRG success)
+ plus the enhanced fetch path, named and unknown actions, ActionResult
+ shapes — Chromium, Firefox and WebKit, 33/33.
+- Full release-tier gates green; npm `alpha`/`latest` at `0.42.0-alpha.2`;
+ post-publish consumer and third-party WC smoke.
+- Interface snapshot re-baselined for the new unfrozen surface (`fail`,
+ `isActionFailure`, `ActionResult`, `ACTION_FETCH_HEADER`); no frozen
+ 0.41 export changed.
+
+## Migrating from alpha.1
+
+Pure-static sites: nothing. Request-time routes: forms now follow the
+303/422 rules — an action that previously returned data for a 200 render
+should `return fail(422, data)` for validation errors and `redirect()`
+(or nothing, for the default PRG) on success. The loader now runs after
+the action on POST.
+
+---
+
AutoFlow3 patch release evidence: `publish-existing-v0.42.0-alpha.2-2026-07-27T07-43-59-379Z`.
- Previous package line: `0.42.0-alpha.1`
diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md
index ca6fc52cf..3d6841dfe 100644
--- a/docs/roadmap/ROADMAP.md
+++ b/docs/roadmap/ROADMAP.md
@@ -4,8 +4,8 @@ Execution and release state follow the
[`Project Workflow`](../governance/PROJECT_WORKFLOW.md).
> Source of truth for forward product planning.\
-> Published package line: `v0.42.0-alpha.2`.\
-> Active execution target: `v0.42.0-alpha.2`.\
+> Published package line: `v0.42.0-alpha.3`.\
+> Active execution target: `v0.42.0-alpha.3`.\
> Current implementation state: five-package convergence is published;
> alpha.17 closed the first audit remediation, alpha.18 completed the
> second audit sweep (ADR-0117), and alpha.19 completed the third (ADR-0118).\
@@ -116,7 +116,7 @@ making the standard Custom Element contract span both layers. See the official
## Current release state
-`0.42.0-alpha.2` is the published package line. npm beta.1 through beta.3 are
+`0.42.0-alpha.3` is the published package line. npm beta.1 through beta.3 are
immutable partial artifacts and remain withdrawn from the active release story.
The planned beta name was cancelled so the version label honestly reflects
that breaking architecture and interface changes are still allowed.
diff --git a/docs/status/STATUS.md b/docs/status/STATUS.md
index 20b24c447..073774557 100644
--- a/docs/status/STATUS.md
+++ b/docs/status/STATUS.md
@@ -1,9 +1,9 @@
# OpenElement Status
> Updated: 2026-07-27\
-> Repository package line: `v0.42.0-alpha.2`\
-> npm registry line: `v0.42.0-alpha.2`\
-> Active release target: `v0.42.0-alpha.2`\
+> Repository package line: `v0.42.0-alpha.3`\
+> npm registry line: `v0.42.0-alpha.3`\
+> Active release target: `v0.42.0-alpha.3`\
> Next release line: `v0.42.0`\
> Product graph: five packages\
> Current maturity stage: stable (0.41.x)
diff --git a/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx b/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx
index 9740257ff..6274b2551 100644
--- a/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx
+++ b/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx
@@ -11,6 +11,7 @@ import {
redirect,
useActionData,
} from '@openelement/app';
+import '../islands/live-counter.tsx';
export const tagName = 'page-form';
@@ -55,6 +56,7 @@ const FormPage = definePage({
{actionData?.error ?
{actionData.error}
: null}
echo={echoed ?? ''}
+
);
},
diff --git a/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts b/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts
index e454a5dac..0344ed149 100644
--- a/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts
+++ b/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts
@@ -136,3 +136,44 @@ test.describe('action protocol (ADR-0120, 0.42.0-alpha.2)', () => {
await expect(page.locator('#echo')).toHaveText('echo=enhanced-path');
});
});
+
+test.describe('revalidation continuity (0.42.0-alpha.3)', () => {
+ test('422 morph keeps a hydrated island alive and shows the failure echo', async ({ page }) => {
+ await page.goto('/form');
+ const button = page.locator('live-counter #increment');
+ const count = page.locator('live-counter #count');
+ await button.click();
+ await button.click();
+ await button.click();
+ await expect(count).toHaveText('3');
+
+ await page.click('#submit');
+ await expect(page.locator('#error')).toHaveText('message is required');
+ // The island survived the morph: its shadow state was not reset.
+ await expect(count).toHaveText('3');
+ });
+
+ test('PRG morph preserves island state and updates the URL', async ({ page }) => {
+ await page.goto('/form');
+ const button = page.locator('live-counter #increment');
+ const count = page.locator('live-counter #count');
+ await button.click();
+ await button.click();
+ await expect(count).toHaveText('2');
+
+ await page.fill('#message', 'morph-keeps-islands');
+ await page.click('#submit');
+ await page.waitForURL('**/form?echoed=morph-keeps-islands');
+ await expect(page.locator('#echo')).toHaveText('echo=morph-keeps-islands');
+ await expect(count).toHaveText('2');
+ });
+
+ test('mixed static/request-time navigation keeps both modes working', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('#home-marker')).toHaveText('request-time fixture home');
+ await page.goto('/live?x=mixed');
+ await expect(page.locator('#x-value')).toHaveText('x=mixed');
+ await page.goto('/');
+ await expect(page.locator('#home-marker')).toHaveText('request-time fixture home');
+ });
+});
diff --git a/packages/adapter-vite/__tests__/entry-generators.test.ts b/packages/adapter-vite/__tests__/entry-generators.test.ts
index b9f543b4e..c4b2e15e7 100644
--- a/packages/adapter-vite/__tests__/entry-generators.test.ts
+++ b/packages/adapter-vite/__tests__/entry-generators.test.ts
@@ -245,8 +245,11 @@ Deno.test('client entry includes the ADR-0120 form enhancement layer', () => {
]);
assert(code.includes("document.addEventListener('submit'"));
assert(code.includes('data-open-enhance'));
- assert(code.includes("'x-openelement-action': 'true'"));
- assert(code.includes("result.type === 'redirect'"));
- assert(code.includes("result.type === 'failure'"));
- assert(code.includes('open:action-failure'));
+ assert(code.includes("'x-openelement-enhance': 'true'"));
+ // Morph continuity: preserve escape hatch, intact-island survival and the
+ // no-re-execute rule for the client entry script.
+ assert(code.includes('data-open-preserve'));
+ assert(code.includes('__islandIntact'));
+ assert(code.includes("oldEl.tagName === 'SCRIPT'"));
+ assert(code.includes('history.pushState'));
});
diff --git a/packages/adapter-vite/deno.json b/packages/adapter-vite/deno.json
index 4385e51d1..c81b41829 100644
--- a/packages/adapter-vite/deno.json
+++ b/packages/adapter-vite/deno.json
@@ -1,6 +1,6 @@
{
"name": "@openelement/adapter-vite",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"exports": {
".": "./src/index.ts",
"./nitro-mount": "./src/nitro-mount.ts",
diff --git a/packages/adapter-vite/src/internal/ssg/entry-generators.ts b/packages/adapter-vite/src/internal/ssg/entry-generators.ts
index 5ddaba053..76976de60 100644
--- a/packages/adapter-vite/src/internal/ssg/entry-generators.ts
+++ b/packages/adapter-vite/src/internal/ssg/entry-generators.ts
@@ -239,11 +239,103 @@ __schedule(__deferred);`
: '// No client:idle islands'
}
-// Form enhancement (ADR-0120, 0.42.0-alpha.2): forms marked
-// data-open-enhance submit through the ActionResult protocol. Without
+// Form enhancement (ADR-0120, 0.42.0-alpha.2/alpha.3): forms marked
+// data-open-enhance submit via fetch and the returned document is morphed
+// into place — no full reload, and untouched subtrees (including hydrated
+// islands whose light DOM did not change) keep their state. Without
// JavaScript the same form is a native POST (303/422 HTML), so behavior
-// degrades to the browser by construction. No DOM surgery happens here, so
-// islands that have not hydrated yet are never touched.
+// degrades to the browser by construction.
+function __syncAttrs(oldEl, newEl) {
+ for (var i = oldEl.attributes.length - 1; i >= 0; i--) {
+ var name = oldEl.attributes[i].name;
+ if (!newEl.hasAttribute(name)) oldEl.removeAttribute(name);
+ }
+ for (var j = 0; j < newEl.attributes.length; j++) {
+ var attr = newEl.attributes[j];
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
+ oldEl.setAttribute(attr.name, attr.value);
+ }
+ }
+}
+
+function __islandIntact(oldEl, newEl) {
+ // A hydrated island (live shadow root) survives when its light-DOM
+ // surface serializes identically in the incoming document. The incoming
+ // child is skipped in the comparison: the
+ // browser already consumed it into the live shadow root. Replacing an
+ // intact island would reset its state.
+ if (!oldEl.shadowRoot) return false;
+ if (oldEl.attributes.length !== newEl.attributes.length) return false;
+ for (var i = 0; i < newEl.attributes.length; i++) {
+ var attr = newEl.attributes[i];
+ if (oldEl.getAttribute(attr.name) !== attr.value) return false;
+ }
+ var newKids = [];
+ for (var k = 0; k < newEl.childNodes.length; k++) {
+ var n = newEl.childNodes[k];
+ if (n.nodeType === 1 && n.tagName === 'TEMPLATE' && n.hasAttribute('shadowrootmode')) continue;
+ newKids.push(n);
+ }
+ if (oldEl.childNodes.length !== newKids.length) return false;
+ for (var m = 0; m < newKids.length; m++) {
+ var o = oldEl.childNodes[m];
+ var nn = newKids[m];
+ if (o.nodeType !== nn.nodeType) return false;
+ if (o.nodeType === 3 && o.data !== nn.data) return false;
+ if (o.nodeType === 1 && o.outerHTML !== nn.outerHTML) return false;
+ }
+ return true;
+}
+
+function __morphNode(oldEl, newEl) {
+ if (oldEl.nodeType !== newEl.nodeType || oldEl.tagName !== newEl.tagName) {
+ oldEl.replaceWith(newEl);
+ return;
+ }
+ if (oldEl.nodeType === 3) {
+ if (oldEl.data !== newEl.data) oldEl.data = newEl.data;
+ return;
+ }
+ if (oldEl.nodeType !== 1) return;
+ if (oldEl.tagName === 'SCRIPT') {
+ // Keep the live script node: replacing it would re-execute the island
+ // client entry and double every listener.
+ return;
+ }
+ if (oldEl.hasAttribute('data-open-preserve')) return;
+ if (__islandIntact(oldEl, newEl)) return;
+ if (oldEl.shadowRoot) {
+ // Hydrated island whose surface changed: replace (state resets by design).
+ oldEl.replaceWith(newEl);
+ return;
+ }
+ __syncAttrs(oldEl, newEl);
+ var oldChildren = Array.from(oldEl.childNodes);
+ var newChildren = Array.from(newEl.childNodes);
+ var shared = Math.max(oldChildren.length, newChildren.length);
+ for (var i = 0; i < shared; i++) {
+ var o = oldChildren[i];
+ var n = newChildren[i];
+ if (o && n) __morphNode(o, n);
+ else if (n) oldEl.appendChild(n);
+ else if (o) o.remove();
+ }
+}
+
+function __morphDocument(html) {
+ var doc = new DOMParser().parseFromString(html, 'text/html');
+ document.title = doc.title;
+ var region = document.querySelector('[data-open-region]');
+ if (region) {
+ var next = doc.querySelector('[data-open-region="' + region.getAttribute('data-open-region') + '"]');
+ if (next) {
+ __morphNode(region, next);
+ return;
+ }
+ }
+ __morphNode(document.body, doc.body);
+}
+
document.addEventListener('submit', function (event) {
var form = event.target;
if (!(form instanceof HTMLFormElement)) return;
@@ -256,31 +348,16 @@ document.addEventListener('submit', function (event) {
fetch(actionUrl, {
method: method,
body: new FormData(form),
- headers: { 'x-openelement-action': 'true' },
+ headers: { 'x-openelement-enhance': 'true' },
}).then(function (response) {
- return response.json();
+ return response.text().then(function (html) {
+ return { html: html, url: response.url };
+ });
}).then(function (result) {
- if (result && result.type === 'redirect' && result.location) {
- window.location.assign(result.location);
- return;
+ __morphDocument(result.html);
+ if (result.url && result.url !== window.location.href) {
+ history.pushState({}, '', result.url);
}
- if (result && result.type === 'failure') {
- // Page code (islands) may handle the failure inline; without a
- // listener the native path takes over and the browser renders the
- // 422 page with the echo. A failed action must be safe to re-run:
- // validate first, mutate after validation passes.
- var failureEvent = new CustomEvent('open:action-failure', {
- cancelable: true,
- detail: { status: result.status, data: result.data, form: form },
- });
- if (!form.dispatchEvent(failureEvent)) {
- form.removeAttribute('data-open-enhance');
- form.requestSubmit(submitter instanceof HTMLElement ? submitter : undefined);
- }
- return;
- }
- // Unexpected shape: fall back to a full reload of the current state.
- window.location.reload();
}).catch(function () {
window.location.reload();
});
diff --git a/packages/app/deno.json b/packages/app/deno.json
index f981314d2..8ae5702a6 100644
--- a/packages/app/deno.json
+++ b/packages/app/deno.json
@@ -1,6 +1,6 @@
{
"name": "@openelement/app",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"exports": {
".": "./src/index.ts",
"./hono": "./src/hono.ts",
diff --git a/packages/create/deno.json b/packages/create/deno.json
index 29b345ca9..32c49186f 100644
--- a/packages/create/deno.json
+++ b/packages/create/deno.json
@@ -1,6 +1,6 @@
{
"name": "@openelement/create",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"exports": "./src/cli.ts",
"imports": {},
"tasks": {
diff --git a/packages/create/src/version.ts b/packages/create/src/version.ts
index ff9e55753..873cf86de 100644
--- a/packages/create/src/version.ts
+++ b/packages/create/src/version.ts
@@ -1,2 +1,2 @@
/** The published CLI version, embedded so packed npm installs are self-contained. */
-export const CREATE_VERSION = '0.42.0-alpha.2';
+export const CREATE_VERSION = '0.42.0-alpha.3';
diff --git a/packages/element/deno.json b/packages/element/deno.json
index 70e67b919..fd44b95f1 100644
--- a/packages/element/deno.json
+++ b/packages/element/deno.json
@@ -1,6 +1,6 @@
{
"name": "@openelement/element",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"exports": {
".": "./src/index.ts",
"./jsx-runtime": "./src/jsx-runtime.ts",
diff --git a/packages/ui/deno.json b/packages/ui/deno.json
index bb0f51118..acea661a0 100644
--- a/packages/ui/deno.json
+++ b/packages/ui/deno.json
@@ -1,6 +1,6 @@
{
"name": "@openelement/ui",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"exports": {
".": "./src/index.ts",
"./open-button": "./src/open-button.tsx",
diff --git a/packages/ui/src/generated-manifest.json b/packages/ui/src/generated-manifest.json
index ce605b763..9c430c113 100644
--- a/packages/ui/src/generated-manifest.json
+++ b/packages/ui/src/generated-manifest.json
@@ -1,7 +1,7 @@
{
"schemaVersion": "1.0.0",
"packageName": "@openelement/ui",
- "version": "0.42.0-alpha.2",
+ "version": "0.42.0-alpha.3",
"description": "Open Props Web Component library for openElement",
"author": "openElement",
"license": "MIT",
diff --git a/tools/project-constants.ts b/tools/project-constants.ts
index 881768913..f61d8c6bb 100644
--- a/tools/project-constants.ts
+++ b/tools/project-constants.ts
@@ -1,6 +1,6 @@
-export const PACKAGE_VERSION = '0.42.0-alpha.2';
+export const PACKAGE_VERSION = '0.42.0-alpha.3';
export const PACKAGE_VERSION_TAG = `v${PACKAGE_VERSION}`;
-export const ACTIVE_EXECUTION_VERSION = 'v0.42.0-alpha.2';
+export const ACTIVE_EXECUTION_VERSION = 'v0.42.0-alpha.3';
export const RETAINED_PACKAGE_NAMES = Object.freeze([
'@openelement/adapter-vite',
'@openelement/app',
@@ -49,7 +49,7 @@ export const NITRO_COMPATIBILITY_DATE = '2026-06-12';
// single source of truth for the "from" side of version-anchor replacements
// (see buildVersionAnchorReplacements in tools/autoflow/release.ts). It is
// kept in sync automatically by updateProjectConstants() during a bump.
-export const PREVIOUS_PACKAGE_VERSION = '0.42.0-alpha.1';
+export const PREVIOUS_PACKAGE_VERSION = '0.42.0-alpha.2';
export const PREVIOUS_PACKAGE_VERSION_TAG = `v${PREVIOUS_PACKAGE_VERSION}`;
// The theme the www roadmap current-line timeline entry carried immediately
@@ -60,7 +60,7 @@ export const PREVIOUS_PACKAGE_VERSION_TAG = `v${PREVIOUS_PACKAGE_VERSION}`;
// sweep' describing alpha.19 under the v0.41.1 entry). The bump side
// re-records this constant from the pre-bump entry; bootstrap value
// documents the incident.
-export const PREVIOUS_RELEASE_THEME = 'request-time rendering foundation';
+export const PREVIOUS_RELEASE_THEME = 'form/action loop';
/**
* Version strings that must never reappear in the head anchor zone of the
diff --git a/www/app/data/version.ts b/www/app/data/version.ts
index d243afe39..505064be0 100644
--- a/www/app/data/version.ts
+++ b/www/app/data/version.ts
@@ -1,2 +1,2 @@
// Current published package line. Release tooling owns the next bump.
-export const OPENELEMENT_VERSION = 'v0.42.0-alpha.2';
+export const OPENELEMENT_VERSION = 'v0.42.0-alpha.3';
diff --git a/www/app/routes/guide/routing-and-data.tsx b/www/app/routes/guide/routing-and-data.tsx
index 52ee75fc9..b060f58f3 100644
--- a/www/app/routes/guide/routing-and-data.tsx
+++ b/www/app/routes/guide/routing-and-data.tsx
@@ -56,7 +56,7 @@ const content: Record<'en' | 'zh', GuideContent> = {
id: 'form-actions',
title: 'Form actions',
body:
- "A dynamic route may export an action ({ formData }) — plain HTML forms work without JavaScript: validation failures return fail(4xx, data) and re-render with the echo (HTTP 422), successes answer 303 (PRG). Named actions dispatch via formaction='?/name'. Forms marked data-open-enhance use the same protocol through fetch (ActionResult JSON); an action must be safe to re-run after a failed validation (0.42 line, unfrozen).",
+ "A dynamic route may export an action ({ formData }) — plain HTML forms work without JavaScript: validation failures return fail(4xx, data) and re-render with the echo (HTTP 422), successes answer 303 (PRG). Named actions dispatch via formaction='?/name'. Forms marked data-open-enhance submit via fetch and morph the returned document into place: hydrated islands whose light DOM did not change keep their state, data-open-preserve exempts a subtree, and the URL follows the PRG target. An action must be safe to re-run after a failed validation (0.42 line, unfrozen).",
},
],
},
@@ -99,7 +99,7 @@ const content: Record<'en' | 'zh', GuideContent> = {
id: 'form-actions',
title: '表单 action',
body:
- "dynamic 路由可导出 action({ formData })——纯 HTML 表单无需 JavaScript 即可工作:校验失败返回 fail(4xx, data),以 422 重渲染并回显;成功则以 303 应答(PRG)。命名 action 通过 formaction='?/name' 分派。标记 data-open-enhance 的表单走同一协议的 fetch 路径(ActionResult JSON);action 在校验失败后必须可安全重跑(0.42 版本线,未冻结)。",
+ "dynamic 路由可导出 action({ formData })——纯 HTML 表单无需 JavaScript 即可工作:校验失败返回 fail(4xx, data),以 422 重渲染并回显;成功则以 303 应答(PRG)。命名 action 通过 formaction='?/name' 分派。标记 data-open-enhance 的表单经 fetch 提交并把返回的文档 morph 就位:light DOM 未变化的已水合 island 状态保留,data-open-preserve 豁免子树,URL 跟随 PRG 目标。action 在校验失败后必须可安全重跑(0.42 版本线,未冻结)。",
},
],
},
diff --git a/www/app/routes/roadmap.tsx b/www/app/routes/roadmap.tsx
index 118844027..60f6d6753 100644
--- a/www/app/routes/roadmap.tsx
+++ b/www/app/routes/roadmap.tsx
@@ -332,10 +332,10 @@ type TimelineEntry = {
const entries: TimelineEntry[] = [
{
- version: 'v0.42.0-alpha.2',
- theme: 'form/action loop',
+ version: 'v0.42.0-alpha.3',
+ theme: 'revalidation continuity',
copy:
- 'Plain HTML forms work without JavaScript: fail() validation echoes at 422, successes answer 303 (PRG), named actions dispatch via formaction, and data-open-enhance forms use the same ActionResult protocol.',
+ 'Enhanced forms morph the returned document: intact hydrated islands keep their state, data-open-preserve exempts subtrees, and the URL follows the PRG target.',
state: 'stable',
stamp: 'CURRENT',
},
@@ -420,7 +420,7 @@ export class RoadmapPage extends OpenElement {
{entries.map((phase) => {
// The current-line stamp follows the bump-maintained anchor so a
// release bump re-marks the timeline without manual edits.
- const stamp = phase.version === 'v0.42.0-alpha.2' ? 'CURRENT' : phase.stamp;
+ const stamp = phase.version === 'v0.42.0-alpha.3' ? 'CURRENT' : phase.stamp;
return (