From 57dc22b8cc9b89249d3571ea287f82273bd02b04 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Sun, 10 May 2026 11:54:32 +0200 Subject: [PATCH] feat: integrate 15-point feedback into requirements - Replace hypothetical @for/@if with real hemplate h-for/h-key/h-if - Split authoring attrs (data-slhx-*) from runtime attrs (data-hid/data-sid) - Split ResourceId (compile-time) from ResourceRef (runtime+scope) - Fix state wording: Effect::set is publish, not subscribe - Form errors via control ids, not slot ids - Macro policy: #[slhx::handler] required, optional ergonomic helpers - Add component/003 (namespaced APIs) and surface/008-010 - Add opt-in content_ids for raw/pre-rendered HTML tracking - Runtime delegation on root, not document; 5KB target - Sync/Query effects as external types (SyncEffect/QueryEffect) - derive_app: no singleton, multiple roots/widgets per process - hemplate htmx as raw attrs only; migration tool separate - nav/003: preserve real href links for progressive enhancement - check/002: honest Rust build model for missing handler checks --- REQUIREMENTS.md | 52 +++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index f3c5b13..f22caef 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -91,6 +91,11 @@ slhx competes with React by making frontend frameworks unnecessary for most apps ### req: component/002 002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup. +### req: component/003 +003 Generated APIs are component-namespaced by default: +`ui::slots::todo_row`, `ui::handles::create`, `ui::forms::create`. +Global exports are opt-in only. + ### req: component/004 004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source. @@ -101,7 +106,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps ## codegen ### req: codegen/001 -001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `@for key`, form controls) from the Surface. [north_star] +001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star] ### req: codegen/002 002 Generated module `slots` exposes ergonomic methods: `Slot::render(value)`, `Slot::text(value)`, `KeyedSlot::append(key, value)`, `KeyedSlot::replace(key, value)`, `KeyedSlot::remove(key)`. Methods return `impl IntoEffect`. @@ -233,7 +238,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps 002 Subscription is explicit: `Effect::set(atoms::FOO, 42)` pushes the new value to all consumers. No automatic component re-render graph. ### req: state/003 -003 JS runtime maintains a client-side atom store with identical API to the server: `get(atom)`, `set(atom, value)`, `subscribe(atom, callback)`. Type erased at runtime with `TypeId`. +003 The JS runtime maintains a client-side atom store keyed by `AtomId`. Runtime values are type-erased postcard bytes. Types are compile-time only. A deterministic `TypeHash` may be generated by `slhx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`. ### req: state/004 004 SSR pages carry a `data-slhx-st` base64url-encoded postcard blob on the document root. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip. @@ -252,14 +257,14 @@ slhx competes with React by making frontend frameworks unnecessary for most apps 002 The handle id is carried as `__h` in POST `application/x-www-form-urlencoded`. A JSON body is allowed at the integration boundary (`application/json`) only if the handler accepts it; core uses form encoding. ### req: form/003 -003 handler receives `form: Form`. Validation errors are returned as `Effect::form_error(field, message)`, which the JS runtime maps back to the originating input via `data-sid`. +003 Handler receives `form: Form`. Validation errors target `(FormId, field_name)` or generated control ids. The runtime maps them to originating form controls via control ids derived from Surface `NodeId`, not via slot ids. --- ## list ### req: list/001 -001 Any `data-slhx-slot` or `data-slhx-handle` inside `@for` requires an explicit `key` expression. Syntax: `@for item in items key item.id { ... }`. Without `key`, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(SlotId, KeyValue)`. +001 Any `data-slhx-slot` or `data-slhx-handle` inside a hemplate `h-for` scope requires a stable key. Preferred syntax: ``. Without a key, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(SlotId, KeyValue)`. ### req: list/002 002 Slots inside a keyed loop receive a composite identity: `(SlotId, KeyValue)`, not a flat id. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups. @@ -272,7 +277,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps ## typed_id ### req: typed_id/001 -001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32, key: Option }`. Typed wrappers (`Slot`, `KeyedSlot`, `Atom`, `Handle`, `Form`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star] +001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32 }`. A concrete runtime target is a `ResourceRef { resource: ResourceId, scope: Option }`. Typed wrappers (`Slot`, `KeyedSlot`, `Atom`, `Handle`, `Form`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star] ### req: typed_id/002 002 `ResourceKind` is an internal closed enum (Slot, Atom, Handle, Form, Route). External crates may not add variants. Extensibility comes via `Effect::event`, `Effect::emit`, or custom `IntoEffect` implementations, never via new `ResourceKind` variants in core. @@ -285,21 +290,21 @@ slhx competes with React by making frontend frameworks unnecessary for most apps 001 Async remote data lives in `Resource` / `Query` / `Mutation`. These are optional, not core primitives. They provide loading/error/refresh semantics without client-side data libraries. ### req: async_data/002 -002 `Effect::resource(res).reload()` triggers a re-fetch and re-render. The server sends a new EffectBatch when data is ready. +002 `QueryEffect::reload(res)` triggers a re-fetch and re-render. The server sends a new `EffectBatch` when data is ready. Query/Resource effects live in a separate API surface to keep core small. --- ## scope ### req: scope/001 -001 `Scope` is a first-class primitive. Keyed loops (`@for`), conditional branches (`@if`), component instances, modals, tabs, nested forms — all are scopes. A slhx-addressable node inside any dynamic scope must carry a stable `ScopeKey`. Composite identity is `(ResourceId, ScopeKey)`. [north_star] +001 `Scope` is a first-class primitive. Keyed loops (`h-for`), conditional branches (`h-if`), component instances, modals, tabs, nested forms — all are scopes. A slhx-addressable node inside any dynamic scope must carry a stable `ScopeKey`. Composite identity is `(ResourceId, ScopeKey)`. [north_star] --- ## wire ### req: wire/001 -001 HTML wire format is standard HTML with `data-hid` and `data-sid` attributes only. No custom markup, no hx-* attributes. +001 Authoring HTML uses symbolic `data-slhx-*` attributes. Rendered runtime HTML lowers these to compact numeric metadata: `data-hid`, `data-sid`, optional `data-key`, optional atom/bootstrap ids. The browser never sees handler or slot names. `data-slhx-root` marks a scoped root boundary. ### req: wire/002 002 POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path. @@ -308,7 +313,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps 003 Responses are `text/html` fragments (or `application/slhx` for push streams). Fragments may contain `