chore(repo): save local work
This commit is contained in:
@@ -58,7 +58,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
||||
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
|
||||
- hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata, the versioned canonical hemx `EffectBatch` codec, postcard surface facts, and form-encoded public requests—not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010
|
||||
- Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008
|
||||
- The SaaS production reference must use real links/URLs for navigation and an ongoing server-owned canonical SSE stream; bounded one-event behavior is a test probe, not the public transport contract. req: nav/001 req: nav/002 req: push/003 req: examples/014
|
||||
- The SaaS production reference must use real links/URLs for navigation and an ongoing server-owned canonical SSE stream; bounded one-event behavior is a test probe, not the public transport contract. Browser history restores saved generated page snapshots and scroll position when available, with partial-fetch fallback, and restored revealed bindings must re-arm. req: nav/001 req: nav/002 req: nav/005 req: convention/014 req: push/003 req: examples/014
|
||||
- Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: component/005 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005
|
||||
- Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry canonical versioned hemx `EffectBatch` bytes over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002
|
||||
- `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ client app state framework.
|
||||
|
||||
004 Ordinary page navigation must not require user-authored handlers. Explicit navigation handlers are available only when custom application logic is needed, and they still return generated target/page commands. [north_star]
|
||||
|
||||
005 Browser back/forward is supported. On `popstate`, hemx fetches the URL as a partial request and applies the same page-swap update without pushing a new history entry. [north_star]
|
||||
005 Browser back/forward restores a saved generated page snapshot and scroll position when available, otherwise fetches the URL as a partial request without pushing a new entry. [north_star]
|
||||
|
||||
006 If a page lacks the expected `content` slot, hemx-axum falls back to normal browser navigation in production and emits a diagnostic in development. [north_star]
|
||||
|
||||
@@ -1011,7 +1011,7 @@ async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
|
||||
|
||||
013 Static empty confirmation messages are build errors: they silently disable the guard in browsers. Custom confirm UI belongs to integration crates.
|
||||
|
||||
014 `data-hemx-revealed` dispatches once when the element enters view, with an immediate fallback when `IntersectionObserver` is unavailable.
|
||||
014 `data-hemx-revealed` dispatches once when the element enters view, re-arms after browser page restoration, and has an immediate fallback when `IntersectionObserver` is unavailable.
|
||||
|
||||
015 Duplicate timers/observers per root are avoided.
|
||||
|
||||
|
||||
+47
-2
@@ -343,6 +343,7 @@
|
||||
await navigateUrl(pageRequestUrl(form, source), rootOf(form) || rootOf(el), mode);
|
||||
return;
|
||||
}
|
||||
savePage(roots()[0]);
|
||||
const response = await fetch(requestUrl(form, data, method), {
|
||||
method,
|
||||
body,
|
||||
@@ -382,7 +383,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotPage(root) {
|
||||
return root ? {
|
||||
hemx: true,
|
||||
pageHtml: root.innerHTML,
|
||||
pageTitle: document.title,
|
||||
scrollX: window.scrollX,
|
||||
scrollY: window.scrollY,
|
||||
} : null;
|
||||
}
|
||||
|
||||
function savePage(root) {
|
||||
const snapshot = snapshotPage(root);
|
||||
if (snapshot) history.replaceState(snapshot, "", location.href);
|
||||
}
|
||||
|
||||
function restorePage(state, root) {
|
||||
if (!root || !state || typeof state.pageHtml !== "string") return false;
|
||||
root.innerHTML = state.pageHtml;
|
||||
if (state.pageTitle) document.title = state.pageTitle;
|
||||
bindRoot(root);
|
||||
requestAnimationFrame(() => scrollTo(state.scrollX, state.scrollY));
|
||||
return true;
|
||||
}
|
||||
|
||||
async function navigateUrl(href, root, mode = "replace") {
|
||||
if (mode === "push") savePage(root);
|
||||
const response = await fetch(href, {
|
||||
headers: { "X-HEMX-Partial": "1", "Accept": "text/html" },
|
||||
credentials: "same-origin",
|
||||
@@ -443,6 +469,8 @@
|
||||
return;
|
||||
}
|
||||
for (const op of batch.ops) applyOp(scope, op);
|
||||
bindPolling(scope);
|
||||
bindRevealed(scope);
|
||||
}
|
||||
|
||||
function canApplyOp(scope, op) {
|
||||
@@ -1101,6 +1129,19 @@
|
||||
stopDescendantPolling(root);
|
||||
}
|
||||
|
||||
function restoreRevealed(event) {
|
||||
if (!event.persisted) return;
|
||||
roots().forEach((root) => {
|
||||
const observer = revealObservers.get(root);
|
||||
if (observer) observer.disconnect();
|
||||
revealObservers.delete(root);
|
||||
forEachElement(root, (el) => {
|
||||
if (attrEquals(el, "data-hemx-on", "revealed") || el.hasAttribute("data-hemx-revealed")) revealed.delete(el);
|
||||
});
|
||||
bindRevealed(root);
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
roots().forEach((root) => {
|
||||
root.setAttribute("data-hemx-request-timeout-ms", String(REQUEST_TIMEOUT_MS));
|
||||
@@ -1147,9 +1188,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener("popstate", () => {
|
||||
window.addEventListener("pageshow", restoreRevealed);
|
||||
|
||||
addEventListener("popstate", (event) => {
|
||||
const root = roots()[0];
|
||||
if (root) navigateUrl(location.href, root, "none").catch((error) => emit(root, "hemx:error", String(error)));
|
||||
if (root && !restorePage(event.state, root)) {
|
||||
if (root) navigateUrl(location.href, root, "none").catch((error) => emit(root, "hemx:error", String(error)));
|
||||
}
|
||||
});
|
||||
|
||||
window.hemx = Object.freeze({
|
||||
|
||||
@@ -148,11 +148,16 @@ fn runtime_supports_tiny_delay_and_revealed_scheduling() {
|
||||
assert!(source.contains("data-hemx-revealed"));
|
||||
assert!(source.contains("typeof IntersectionObserver === \"undefined\""));
|
||||
assert!(source.contains("schedule(entry.target, \"revealed\")"));
|
||||
assert!(source.contains("window.addEventListener(\"pageshow\", restoreRevealed)"));
|
||||
assert!(source.contains("revealed.delete(el)")); // req: convention/014 test
|
||||
assert!(source.contains("record.addedNodes.forEach((node) =>"));
|
||||
assert!(source.contains("descendantRoots(node).forEach(bindRoot)"));
|
||||
assert!(source.contains("const owner = rootOf(node.parentElement)"));
|
||||
assert!(source.contains("bindPolling(owner)"));
|
||||
assert!(source.contains("bindRevealed(owner)")); // req: runtime/007 test
|
||||
assert!(source.contains("bindRevealed(owner)"));
|
||||
assert!(source.contains(
|
||||
"for (const op of batch.ops) applyOp(scope, op);\n bindPolling(scope);\n bindRevealed(scope);"
|
||||
)); // req: runtime/007 test
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -312,6 +317,9 @@ fn runtime_exposes_page_swap_hooks() {
|
||||
assert!(source.contains("data-hemx-nav"));
|
||||
assert!(source.contains("data-hemx-boost"));
|
||||
assert!(source.contains("history.pushState"));
|
||||
assert!(source.contains("snapshotPage(root)"));
|
||||
assert!(source.contains("restorePage(event.state, root)"));
|
||||
assert!(source.contains("scrollTo(state.scrollX, state.scrollY)")); // req: page_swap/005 test
|
||||
assert!(source.contains("popstate"));
|
||||
assert!(source.contains("x-hemx-title"));
|
||||
assert!(source.contains("x-hemx-fingerprint"));
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@
|
||||
|
||||
use hemx_core::SafeHtml;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use hemx_core::{Effect, KeyedSlot, Slot};
|
||||
use hemx_core::{KeyedSlot, Slot};
|
||||
|
||||
pub use hemx_core::{
|
||||
navigate, push, redirect, replace, CssClass, CssClasses, Form, FormContract, FormControlKind,
|
||||
navigate, push, redirect, replace, CssClass, CssClasses, Effect, Form, FormContract, FormControlKind,
|
||||
FormError, FormField, FormModel, FormValue, FromForm, IntoEffect,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user