caspian-utils 0.1.26 → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: PulsePoint Runtime Guide
3
- description: Use this page when the task mentions PulsePoint, `pp.state`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`.
3
+ description: Use this page when the task mentions PulsePoint, `pp.state`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read the "PulsePoint Is Not JSX" section before writing any template — PulsePoint borrows React's hook API but its markup is plain HTML, and JSX constructs silently corrupt the page.
4
4
  related:
5
5
  title: Related docs
6
6
  description: Read the components, routing, data-fetching, and project-structure docs alongside the PulsePoint runtime contract.
@@ -20,9 +20,9 @@ This file documents the PulsePoint contract for the shipped Caspian browser runt
20
20
 
21
21
  If a task involves `pp.state`, `pp.effect`, `pp.layoutEffect`, `pp-ref`, `pp-style`, `pp-spread`, `pp-for`, context, portals, `pp-reset-scroll`, SPA navigation, or component boundary behavior, read this page first and keep generated code aligned with the current Caspian runtime.
22
22
 
23
- Use `components.md` for authoring Python `@component` files, same-name HTML templates, and HTML-first `x-*` component tags. Use this page for the browser-side PulsePoint contract, the authoring rules that feed it, and the React-style mental model used by the shipped runtime.
23
+ Use `components.md` for authoring Python `@component` files, same-name HTML templates, and HTML-first `x-*` component tags. Use this page for the browser-side PulsePoint contract and the authoring rules that feed it.
24
24
 
25
- PulsePoint is the default reactive frontend layer for Caspian. In the current runtime it follows a React-like component pattern, but it is HTML-first rather than JSX-first.
25
+ PulsePoint is the default reactive frontend layer for Caspian. Its **script API** deliberately mirrors React hooks. Its **markup is plain HTML**, compiled by a template compiler that has no JSX support of any kind. Those two facts are independent, and conflating them is the single most common way generated PulsePoint code fails. Read the next section before writing a template.
26
26
 
27
27
  Do not assume React, Vue, Svelte, JSX, Alpine, HTMX, or older PulsePoint docs unless the task explicitly asks for a different frontend contract.
28
28
 
@@ -38,6 +38,93 @@ Apply these before generating any template, even without reading the rest of thi
38
38
  6. Call the backend with `pp.rpc(...)` backed by Python `@rpc()` actions; do not invent fetch wrappers or `pp.fetchFunction()`.
39
39
  7. `pp-for` goes only on `<template>` with plain `key`; context uses `pp.createContext(...)`, a lowercase `<token.provider>` tag, and `pp.context(token)`.
40
40
  8. If an API is not in `public/js/pp-reactive-v2.js`, it does not exist; do not invent hooks, directives, or globals.
41
+ 9. **Every brace expression in an attribute must be inside quotes**: `class="{expr}"`, never `class={expr}`. Unquoted braces are shredded by the HTML parser.
42
+ 10. **A `{...}` expression produces text, never elements.** There is no JSX. Conditionals use `hidden="{...}"`, lists use `<template pp-for="…">`.
43
+
44
+ ## PulsePoint Is Not JSX
45
+
46
+ This section exists because the React comparison in this doc, in `components.md`, and in `index.md` has repeatedly been over-read as permission to write JSX. It is not.
47
+
48
+ **The React comparison is scoped to exactly two things:**
49
+
50
+ 1. The **hook API inside `<script>`** — `pp.state`, `pp.effect`, `pp.memo`, `pp.ref`, dependency arrays, cleanup functions. These behave like their React counterparts.
51
+ 2. **Component decomposition** — split a page into small single-responsibility components with props, the way you would split a React tree. This is about *file and responsibility shape*, not syntax.
52
+
53
+ **The React comparison does not extend to markup — at all.** A Caspian template is an HTML file. It is parsed by an HTML parser first, then compiled. It is never parsed as JavaScript, so JSX expressions in element position are not "unsupported" — they are not even seen as code.
54
+
55
+ ### The JSX constructs that break PulsePoint
56
+
57
+ Each row is a real failure, not a style preference.
58
+
59
+ | JSX construct | What actually happens | PulsePoint form |
60
+ |---|---|---|
61
+ | `{cond && (<div>…</div>)}` | The `{cond && (` becomes literal text; the `<div>` becomes a real always-visible element; the `)}` becomes literal text. | `<div hidden="{!cond}">…</div>` |
62
+ | `{cond ? <A/> : <B/>}` | Same — both branches render, plus visible stray text. | Two elements with complementary `hidden` bindings. |
63
+ | `{items.map(i => (<li>{i.name}</li>))}` | The `<li>` renders exactly once with the literal text `{items.map(i => (` before it. | `<template pp-for="item in items"><li key="{item.id}">{item.name}</li></template>` |
64
+ | An unquoted template-literal class, `class={` … `}` | **Silent HTML corruption.** The parser splits the unquoted value on spaces, producing junk attributes like `${b}="" 'a'=""`. The component root fails to compile and the page renders blank. | Quote the whole thing: ``class="{`a ${b}`}"`` |
65
+ | `selected={x === 'y'}` | Same unquoted-attribute corruption, because of the spaces around `===`. | `selected="{x === 'y'}"`, or bind `value` on the `<select>`. |
66
+ | `className=` | Not an HTML attribute. Ignored. | `class=` |
67
+ | `onClick={fn}` | Not a DOM event attribute in HTML (attributes are case-insensitive, so this lands as `onclick` with a JSX value). | `onclick="{fn()}"` |
68
+ | `htmlFor=`, `key={x}` in JSX braces | `htmlFor` is not HTML; `key` is a plain attribute here. | `for=`, `key="{x}"` |
69
+ | `<>…</>` fragments | Parsed as an unknown tag. Also breaks the single-root rule. | One real root element. |
70
+ | `style={{color:'red'}}` | Double-brace object literal is not CSS text. | `pp-style="{'color:red'}"` — a **string**, not an object. |
71
+ | `dangerouslySetInnerHTML` | Does not exist. | Server-render trusted HTML, or use `pp-for` over real data. |
72
+
73
+ ### Why the corruption is silent
74
+
75
+ `class={...}` and `selected={...}` fail in the parser, not in PulsePoint. The template compiler never gets valid markup, the component root never mounts, and the runtime's reveal step never clears `<body style="opacity: 0">`. The result is a **blank white page with no console error** — the hardest possible failure to diagnose. Quoting the attribute is not cosmetic.
76
+
77
+ ### The one-line test
78
+
79
+ Before writing a template, ask: *"Would this file be valid HTML if I deleted every `{}` from it?"* If not, it is JSX and it will not work.
80
+
81
+ ## Complete Directive And API Surface
82
+
83
+ **These lists are closed.** PulsePoint has no other directives, template syntax, or globals. If something is not listed here, it does not exist — do not infer it from React, Vue, Alpine, or another PulsePoint version. Verify against `public/js/pp-reactive-v2.js` when in doubt.
84
+
85
+ ### Author-facing template syntax (the whole list)
86
+
87
+ | Syntax | Where it goes | Purpose |
88
+ |---|---|---|
89
+ | `{expression}` | Text nodes and **quoted** attribute values | Interpolate a value as text/attribute content |
90
+ | `on*` (`onclick`, `oninput`, `onchange`, `onsubmit`, any native DOM event attribute) | Any element | Event binding |
91
+ | `pp-for="item in items"` / `"(item, index) in items"` | **`<template>` only** | Keyed list rendering |
92
+ | `key="{expr}"` | Repeated sibling inside a `pp-for` template | Keyed diffing identity |
93
+ | `pp-ref="name"` / `pp-ref="{expr}"` | Native elements and `x-*` component tags | Imperative element access |
94
+ | `pp-style="{cssText}"` | Any element | Dynamic inline style, as a **CSS string** |
95
+ | `pp-spread="{...obj}"` | Any element | Spread an object into attributes |
96
+ | `pp-ref-forward` | *Server-set* on component roots — see note below | Ref forwarding through layout-neutral hosts |
97
+ | `<token.provider value="{v}">` (lowercase) | Anywhere in markup | Context provider |
98
+ | `pp-spa="true"` / `pp-spa="false"` | `<body>` enables SPA navigation; `pp-spa="false"` on an `<a>` opts that link out | SPA navigation interception |
99
+ | `pp-reset-scroll="true"` | A scroll container, or `<body>` | Reset that container's scroll on navigation |
100
+ | `pp-scroll-key="stable-name"` | A scroll container | Stable scroll-restoration identity |
101
+ | `pp-loading-content="true"` | The region swapped during navigation | Marks the navigation content region |
102
+ | `pp-loading-url="/route"` | A loading-state element | Route-specific loading lookup |
103
+ | `pp-loading-transition='{"fadeIn":…,"fadeOut":…}'` | The loading region | Navigation fade timings |
104
+
105
+ There is **no** `pp-if`, `pp-show`, `pp-else`, `pp-model`, `pp-bind`, `pp-class`, `pp-text`, `pp-html`, `pp-on`, or `pp-key`. Conditionals are `hidden="{...}"`; two-way binding is `value="{state}"` plus an `oninput` handler.
106
+
107
+ ### Runtime-managed — never author these
108
+
109
+ The render pipeline or the browser runtime writes these. Handwriting them corrupts instance tracking:
110
+
111
+ `pp-component`, `type="text/pp"`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `pp-context-provider` (the generated tag), `pp-context-token`, `pp-context-value`, `pp-root-layout`, `pp-fragment-root`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-dynamic-script`, `pp-dynamic-meta`, `pp-dynamic-link`.
112
+
113
+ `pp-ref-forward` appears in the author-facing table only because you will see it in rendered HTML and in composition components; the server component compiler sets it on a root whose own root is another `x-*` component. Do not write it yourself.
114
+
115
+ ### Component-script hooks (the whole list)
116
+
117
+ `pp.state`, `pp.effect`, `pp.layoutEffect`, `pp.ref`, `pp.memo`, `pp.callback`, `pp.reducer`, `pp.context`, `pp.portal`, `pp.id`, `pp.errorBoundary`, `pp.syncExternalStore`, `pp.imperativeHandle`, `pp.transition`, `pp.deferredValue`, `pp.optimistic`, plus the `pp.props` bag.
118
+
119
+ ### Runtime utilities (the whole list)
120
+
121
+ `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
122
+
123
+ React hooks with **no** PulsePoint equivalent: `useContext`-as-a-provider-call, `useInsertionEffect`, `useDebugValue`, `useActionState`, `useFormStatus`, `forwardRef`, `memo()` as a component wrapper, `lazy`, `Suspense`, `startTransition` as a free function. Do not generate them.
124
+
125
+ ### Identifiers injected into event handlers
126
+
127
+ Inside an `on*` attribute the runtime provides `event`, plus the aliases `e`, `$event`, `target` (→ `event.target`), `currentTarget` and `el` (both → `event.currentTarget`). An alias is skipped when the component scope already declares that name.
41
128
 
42
129
  ## Source Of Truth
43
130
 
@@ -350,6 +437,10 @@ Important:
350
437
  - Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
351
438
  - The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
352
439
  - `pp.props` contains the current prop bag for the component.
440
+ - `pp.props` is computed from the **rendered root element's attributes**, not from any server-side signature. A prop that never lands on the root is `undefined` in the browser, with no server error and no console warning. In a Python component this means every prop the template references must be forwarded through `get_attributes({...}, props)`; see [components.md](./components.md#every-prop-a-template-reads-must-be-forwarded-to-the-root).
441
+ - Prop value types depend on how the attribute reaches the root. An unevaluated brace expression such as `volume="{vol}"` is evaluated in the parent component's scope and keeps its real type (number, boolean, object, function). A literal attribute such as `volume="0"` arrives as the **string** `"0"`, so `volume === 0` is false. A valueless attribute arrives as boolean `true`. Coerce or compare loosely when the value may be a server-rendered literal.
442
+ - Attribute names round-trip through kebab-case: `isFullscreen` renders as `is-fullscreen` and returns as `pp.props.isFullscreen`.
443
+ - Attribute names that are JavaScript reserved words are dropped from the prop bag, so `pp.props.class` and `pp.props.for` never exist. Use a non-reserved prop name when the script must read the value.
353
444
  - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
354
445
  - Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
355
446
 
@@ -389,7 +480,7 @@ That is the authored form. In browser-inspected runtime HTML, Caspian will alrea
389
480
 
390
481
  ## Hooks and runtime API
391
482
 
392
- PulsePoint uses a React-style mental model inside each component script: stateful render scope, dependency-based effects, refs, reducer-style updates, context consumption, and portals.
483
+ PulsePoint uses a React-style mental model **inside each component script**: stateful render scope, dependency-based effects, refs, reducer-style updates, context consumption, and portals. That similarity stops at the `<script>` boundary — the surrounding markup is plain HTML, never JSX. See [PulsePoint Is Not JSX](#pulsepoint-is-not-jsx).
393
484
 
394
485
  Hooks exposed inside component scripts through `pp`:
395
486
 
@@ -402,6 +493,13 @@ Hooks exposed inside component scripts through `pp`:
402
493
  - `pp.reducer(reducer, initialState)` returns `[state, dispatch]`.
403
494
  - `pp.context(token)` resolves a provided context value from ancestor components.
404
495
  - `pp.portal(ref, target?)` registers a ref-managed element for portal rendering and returns an object that includes `sourceParent`.
496
+ - `pp.id()` returns a stable, DOM-safe unique id for the life of that hook slot.
497
+ - `pp.syncExternalStore(subscribe, getSnapshot)` subscribes to a mutable source outside PulsePoint and rerenders when its snapshot changes.
498
+ - `pp.imperativeHandle(ref, createHandle, deps?)` publishes an imperative API on a parent-owned ref instead of the raw DOM node.
499
+ - `pp.transition()` returns `[isPending, startTransition]`.
500
+ - `pp.deferredValue(value, initialValue?)` returns a copy of `value` that catches up one commit later.
501
+ - `pp.optimistic(passthrough, reducer?)` returns `[optimisticState, addOptimistic]`.
502
+ - `pp.errorBoundary()` returns `[error, reset]` and marks the component as an error boundary.
405
503
  - `pp.props` exposes the current props.
406
504
 
407
505
  Global helpers exposed through the `pp` singleton and also merged into the component runtime:
@@ -425,6 +523,12 @@ Notes:
425
523
  - Older docs may call the RPC helper `pp.fetchFunction()`. In the current bundled runtime the implemented global API is `pp.rpc()`.
426
524
  - Keep template-facing bindings at the top level so the AST-based exporter can see them.
427
525
  - For predictable code generation, prefer passing an explicit dependency array to `pp.effect`, `pp.layoutEffect`, `pp.memo`, and `pp.callback`.
526
+ - Top-level destructuring is exported to template scope for every form: array patterns, object patterns, nested patterns, defaults, rest elements, and holes. `const [isPending, startTransition] = pp.transition()` and `const [error, reset] = pp.errorBoundary()` reach the template the same way `pp.state` does.
527
+ - `pp.id()` is derived from the component id plus the hook slot, so the same slot keeps its id across rerenders and two instances of the same component never collide. Use it for `id`/`for` pairing and `aria-*` references instead of hand-rolled counters.
528
+ - `pp.syncExternalStore` requires a referentially stable `subscribe`. Wrap it in `pp.callback(..., [])`, otherwise the store is unsubscribed and resubscribed on every render. The runtime re-reads the snapshot immediately after subscribing so a change that lands between render and subscription is not lost.
529
+ - `pp.transition` does not deprioritize rendering the way React's concurrent scheduler does, because PulsePoint renders synchronously. It does give a correct `isPending` flag for both synchronous and promise-returning scopes, which is the common use (disabling a submit button while `pp.rpc()` resolves). Overlapping scopes stay pending until the last one settles.
530
+ - `pp.optimistic` drops its pending actions as soon as `passthrough` changes, which is the point at which the server has confirmed or rejected the guess. Without a reducer, each action replaces the value.
531
+ - `pp.deferredValue` lags by one commit. Use it to keep an input responsive while an expensive derived subtree catches up.
428
532
 
429
533
  Effect with cleanup and dependencies:
430
534
 
@@ -513,6 +617,160 @@ Portal pattern for overlays that must escape clipping ancestors:
513
617
 
514
618
  `pp.portal(ref)` moves the ref-managed element to `document.body` by default, or to the element passed as the second argument. Portaled content keeps its logical component ancestry, so context, props, and events keep working.
515
619
 
620
+ Stable ids for accessible field pairing:
621
+
622
+ ```html
623
+ <div>
624
+ <label for="{emailId}">Email</label>
625
+ <input id="{emailId}" type="email" aria-describedby="{hintId}" />
626
+ <p id="{hintId}">We never share this.</p>
627
+
628
+ <script>
629
+ const emailId = pp.id();
630
+ const hintId = pp.id();
631
+ </script>
632
+ </div>
633
+ ```
634
+
635
+ Never derive ids from an index or a module-level counter. `pp.id()` is the only form that stays stable across rerenders and unique across instances of the same component.
636
+
637
+ External store subscription:
638
+
639
+ ```html
640
+ <section>
641
+ <p>{isNarrow ? "Compact layout" : "Wide layout"}</p>
642
+
643
+ <script>
644
+ const subscribe = pp.callback((onStoreChange) => {
645
+ const query = window.matchMedia("(max-width: 640px)");
646
+ query.addEventListener("change", onStoreChange);
647
+ return () => query.removeEventListener("change", onStoreChange);
648
+ }, []);
649
+
650
+ const isNarrow = pp.syncExternalStore(
651
+ subscribe,
652
+ () => window.matchMedia("(max-width: 640px)").matches
653
+ );
654
+ </script>
655
+ </section>
656
+ ```
657
+
658
+ Prefer this over `pp.state` plus `pp.effect` for anything the component does not own. It reads the snapshot during render and closes the gap between render and subscription, which a hand-rolled effect silently drops.
659
+
660
+ Imperative handle for a child-owned API. The parent passes its own ref down as an ordinary prop:
661
+
662
+ ```html
663
+ <!-- parent -->
664
+ <div>
665
+ <button onclick="{dialogControl.current.open()}">Open</button>
666
+ <x-confirm-dialog control-ref="{dialogControl}" />
667
+
668
+ <script>
669
+ const dialogControl = pp.ref(null);
670
+ </script>
671
+ </div>
672
+ ```
673
+
674
+ ```html
675
+ <!-- ConfirmDialog component -->
676
+ <section>
677
+ <script>
678
+ const [open, setOpen] = pp.state(false);
679
+
680
+ pp.imperativeHandle(
681
+ pp.props.controlRef,
682
+ () => ({
683
+ open: () => setOpen(true),
684
+ close: () => setOpen(false),
685
+ }),
686
+ []
687
+ );
688
+ </script>
689
+
690
+ <div hidden="{!open}">
691
+ <button onclick="{setOpen(false)}">Close</button>
692
+ </div>
693
+ </section>
694
+ ```
695
+
696
+ Notes:
697
+
698
+ - A prop whose bound expression evaluates to an object is passed through by reference, so `control-ref="{dialogControl}"` arrives as the real ref object on `pp.props.controlRef`.
699
+ - Do **not** author `pp-ref-forward` to make this work. That attribute is runtime-managed, and `pp-ref` on an `x-*` tag binds the child's root DOM node, which is a different feature. Use a normal prop when the parent needs a child-defined API rather than an element.
700
+ - The handle is published in the layout-effect phase and cleared on unmount, so parents should call into it from event handlers or effects, not during their own render.
701
+
702
+ Optimistic update around an RPC call:
703
+
704
+ ```html
705
+ <section>
706
+ <button onclick="{addLike()}">Like ({likes})</button>
707
+
708
+ <script>
709
+ const [confirmedLikes, setConfirmedLikes] = pp.state(pp.props.likes ?? 0);
710
+ const [likes, addOptimisticLike] = pp.optimistic(
711
+ confirmedLikes,
712
+ (state, delta) => state + delta
713
+ );
714
+
715
+ async function addLike() {
716
+ addOptimisticLike(1);
717
+ const result = await pp.rpc("like_post", { postId: pp.props.postId });
718
+ setConfirmedLikes(result.likes);
719
+ }
720
+ </script>
721
+ </section>
722
+ ```
723
+
724
+ The optimistic `+1` is discarded the moment `confirmedLikes` changes, so the server value never double-counts the guess.
725
+
726
+ Transition for pending async work:
727
+
728
+ ```html
729
+ <section>
730
+ <button onclick="{save()}" disabled="{isSaving}">
731
+ {isSaving ? "Saving..." : "Save"}
732
+ </button>
733
+
734
+ <script>
735
+ const [isSaving, startSaving] = pp.transition();
736
+
737
+ function save() {
738
+ startSaving(() => pp.rpc("save_profile", { name: pp.props.name }));
739
+ }
740
+ </script>
741
+ </section>
742
+ ```
743
+
744
+ ### Error boundaries
745
+
746
+ `pp.errorBoundary()` marks a component as the recovery point for render and effect failures. Without one, a throwing render is logged to the console and leaves the subtree in whatever state it reached.
747
+
748
+ ```html
749
+ <section>
750
+ <script>
751
+ const [error, reset] = pp.errorBoundary();
752
+ </script>
753
+
754
+ <div hidden="{!error}">
755
+ <p>Something went wrong: {error?.message}</p>
756
+ <button onclick="{reset()}">Try again</button>
757
+ </div>
758
+
759
+ <div hidden="{!!error}">
760
+ <x-report-chart />
761
+ </div>
762
+ </section>
763
+ ```
764
+
765
+ Behavior to rely on:
766
+
767
+ - Errors thrown during a child's render or effects bubble up the logical component ancestry to the nearest boundary. If nothing catches, the runtime logs `[PP-ERROR] Render Cycle Failed` as before.
768
+ - Unlike React, a boundary also catches throws from its **own** render and effects, because in Caspian the boundary and its fallback markup live in the same single-root component.
769
+ - Effect callbacks and effect cleanups take the same path as render failures, so a throwing subscription teardown is not silently swallowed.
770
+ - The boundary **latches**: it keeps holding the error until `reset()` is called.
771
+ - A boundary absorbs at most five errors without an intervening `reset()`, then logs and stops catching. This stops a fallback that itself throws from spinning the render loop. `reset()` re-arms it.
772
+ - Event handler errors are not routed to boundaries. Handle those with `try`/`catch` inside the handler, which matches React.
773
+
516
774
  ## Context
517
775
 
518
776
  Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
@@ -567,11 +825,11 @@ Consumer example for a child component that receives the token through props:
567
825
 
568
826
  When a child component needs the same token object, pass it from the provider scope as a prop such as `theme-token="{ThemeContext}"`.
569
827
 
570
- ## Props and nested components
571
-
572
- In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
573
-
574
- - Child component props are derived from DOM attributes.
828
+ ## Props and nested components
829
+
830
+ In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
831
+
832
+ - Child component props are derived from DOM attributes.
575
833
  - Attribute names are converted from kebab-case to camelCase for the prop bag.
576
834
  - Native `on*` attributes and `pp-component` are not included in props.
577
835
  - Empty attributes become boolean `true` props.
@@ -591,6 +849,9 @@ Nested components:
591
849
  ## Template expressions and attributes
592
850
 
593
851
  - Use `{expression}` in text nodes and attribute values.
852
+ - **Attribute brace expressions must be quoted.** Write `class="{expr}"`, `disabled="{isSaving}"`, `selected="{role === 'admin'}"`. The unquoted JSX form `class={expr}` is not a PulsePoint syntax variant — it is invalid HTML. An unquoted value ends at the first space, so everything after it is re-parsed as further attribute names, and the element (and usually the whole component root) is destroyed before the compiler ever runs. This is silent: no console error, just a blank page.
853
+ - **An interpolation evaluates to a value, never to markup.** The compiler coerces the result with `String(...)` and HTML-escapes it. Elements cannot come out of an expression, so there is no JSX-style element-returning branch or `.map()`. Use `hidden="{...}"` for conditionals and `<template pp-for="…">` for lists.
854
+ - Objects, functions, and symbols in text position log `[PP-WARN] Invalid template child` and render nothing — that warning usually means JSX-shaped code was attempted.
594
855
  - Follow HTML-first attribute naming. Native event attributes are lowercase DOM event attributes (`onclick`, `oninput`, `onsubmit`). Every other attribute on a component boundary is a prop: kebab-case names become camel-cased only inside `pp.props` (`selected-value` becomes `selectedValue`, `open-change` becomes `openChange`, and `on-open-change` becomes `onOpenChange`). A function prop does not need an `on-` prefix; that prefix is only an API naming convention. Use `on-click` when a component intentionally exposes an `onClick` prop, because lowercase `onclick` is reserved for the native DOM event on the boundary element.
595
856
  - Pure bindings like `value="{count}"` are evaluated as expressions.
596
857
  - For dynamic inline styles in authored templates, prefer `pp-style="{styleText}"` over `style="{styleText}"` so HTML/CSS tooling does not parse the brace expression as raw CSS.
@@ -606,6 +867,7 @@ Nested components:
606
867
  - Use `pp-spread="{...attrs}"` to spread an object expression into attributes.
607
868
  - `pp-spread` omits nullish values, omits known HTML boolean attributes when their value is `false`, emits them bare when `true`, preserves string-valued `aria-*`/`data-*` booleans, and escapes `&`, `"`, and `<` in emitted attribute values.
608
869
  - Use plain `key` for keyed diffing. `pp-key` is not implemented.
870
+ - Only top-level declarations in the component script reach template scope, but every destructuring shape is supported there: `const [a, b] = anyTuple()`, `const [, only] = pair`, `const [first = 1, ...rest] = list`, `const [{ id }] = rows`, and the object-pattern forms. A binding declared inside a block or function is not exported.
609
871
 
610
872
  Example:
611
873
 
@@ -677,6 +939,57 @@ Component example:
677
939
  </div>
678
940
  ```
679
941
 
942
+ ## Conditional rendering
943
+
944
+ PulsePoint has **no conditional directive**. There is no `pp-if`, no `pp-show`, no `pp-else`, and no JSX-style `{cond && (<div/>)}`. Conditional UI is expressed three ways, in this order of preference.
945
+
946
+ **1. `hidden="{...}"` — the default.** Bind the native boolean attribute. The element stays in the DOM and keeps its state; only its visibility changes.
947
+
948
+ ```html
949
+ <div hidden="{!message}" class="notice">{message}</div>
950
+
951
+ <div hidden="{!showForm}">
952
+ <form onsubmit="{submitForm(event)}"> … </form>
953
+ </div>
954
+ ```
955
+
956
+ Notes:
957
+
958
+ - `hidden` is in the runtime's boolean-attribute set, so a truthy value emits the bare attribute and a falsy value removes it.
959
+ - Under Tailwind's preflight, `[hidden]:where(:not([hidden="until-found"]))` is `display: none !important`, so `hidden` reliably beats a `flex`/`grid`/`block` utility on the same element. **Without Tailwind preflight**, `hidden` is only a UA-stylesheet `display: none` and any `display` rule overrides it — in that case hide with a bound class instead.
960
+ - Because the subtree still exists, guard expressions inside it: use `{deleteTarget?.name}`, not `{deleteTarget.name}`. A hidden element is still rendered, so a throw there still breaks the component.
961
+
962
+ **2. A ternary inside an interpolation — for text, classes, and attributes.**
963
+
964
+ ```html
965
+ <h3>{editingUser ? 'Edit User' : 'Create User'}</h3>
966
+ <button disabled="{isSaving}">{isSaving ? 'Saving…' : 'Save'}</button>
967
+ <span class="{'badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-user')}">{user.role}</span>
968
+ ```
969
+
970
+ Template literals work too, as long as the attribute is quoted — the compiler tracks nested braces and quotes: ``class="{`badge ${isAdmin ? 'badge-admin' : ''}`}"``. String concatenation is easier to read and avoids nesting mistakes.
971
+
972
+ **3. `pp-for` over a 0-or-1 length collection — when the node must truly leave the DOM.** Use this when a hidden subtree would be wrong (an expensive child component, a duplicate form field name, a video that must stop).
973
+
974
+ ```html
975
+ <template pp-for="item in selected ? [selected] : []">
976
+ <x-detail-panel key="{item.id}" item="{item}" />
977
+ </template>
978
+ ```
979
+
980
+ For an empty-state row, bind `hidden` on the placeholder rather than branching:
981
+
982
+ ```html
983
+ <tbody>
984
+ <tr hidden="{users.length !== 0}">
985
+ <td colspan="5">No users found.</td>
986
+ </tr>
987
+ <template pp-for="user in users">
988
+ <tr key="{user.id}">…</tr>
989
+ </template>
990
+ </tbody>
991
+ ```
992
+
680
993
  ## Lists and keyed diffing
681
994
 
682
995
  - Use `pp-for` only on `<template>`.
@@ -829,6 +1142,7 @@ These are runtime details.
829
1142
 
830
1143
  Use these rules when generating or editing PulsePoint runtime code:
831
1144
 
1145
+ - **Write plain HTML, never JSX.** Before finishing any template, verify it would still be valid HTML with every `{}` deleted, that every attribute brace expression is quoted, that conditionals use `hidden="{...}"`, and that lists use `<template pp-for="…">` with `key`. This check catches the highest-frequency generation failure and costs one pass.
832
1146
  - Treat PulsePoint as the default reactive frontend for Caspian app code.
833
1147
  - For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
834
1148
  - For simple forms, bind `onsubmit` in the authored HTML and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; do not generate per-input refs or effect-managed submit listeners just to gather values.
@@ -860,6 +1174,19 @@ Use these rules when generating or editing PulsePoint runtime code:
860
1174
 
861
1175
  Do not generate these unless the current source explicitly adds support:
862
1176
 
1177
+ JSX constructs in markup — these are the highest-frequency generation failure, so check for them explicitly before finishing a template:
1178
+
1179
+ - `{cond && (<element/>)}` or `{cond ? <A/> : <B/>}` element-returning branches — use `hidden="{...}"`
1180
+ - `{list.map(item => (<element/>))}` — use `<template pp-for="item in list">`
1181
+ - unquoted brace attributes: `class={…}`, `selected={…}`, `value={…}`, `disabled={…}` — always quote: `class="{…}"`
1182
+ - `className`, `htmlFor`, `onClick`/`onChange`/`onSubmit` camelCase event props, `defaultValue` as a React prop, `dangerouslySetInnerHTML`
1183
+ - `style={{ color: "red" }}` object literals — `pp-style` takes a CSS **string**
1184
+ - `<>…</>` fragments, or any second top-level node
1185
+ - `key` written as a JSX brace prop outside a quoted attribute
1186
+ - imported/returned components as JavaScript values — components are server-side `x-*` tags
1187
+
1188
+ Also avoid:
1189
+
863
1190
  - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
864
1191
  - standard DOM scripting as the default first-party interaction model, including id/data-attribute driven `querySelector(...)`, `addEventListener(...)`, or manual `innerHTML` rendering for normal buttons, forms, filters, toggles, uploads, and reactive lists
865
1192
  - `pp-context`
@@ -889,6 +1216,12 @@ These are current runtime caveats that matter for authors and AI tools:
889
1216
  - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
890
1217
  - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
891
1218
  - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
1219
+ - Top-level array destructuring is exported to template scope for any initializer, not only `pp.state` and `pp.reducer`. A binding declared inside a block or function is still not exported.
1220
+ - `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
1221
+ - `pp.syncExternalStore(...)` resubscribes whenever `subscribe` changes identity, so pass a `pp.callback(..., [])`-wrapped function.
1222
+ - `pp.optimistic(...)` clears its pending actions on the render where `passthrough` changes, using a render-phase update rather than an effect, so there is no frame showing stale optimistic output.
1223
+ - `pp.errorBoundary()` catches render, effect, and effect-cleanup throws, including the boundary component's own. It does not catch event-handler errors. It latches until `reset()` and stops catching after five captures without a reset.
1224
+ - `pp.imperativeHandle(...)` needs the parent's ref passed down as an ordinary prop. `pp-ref` on an `x-*` tag is a different feature that binds the child's root DOM node, and `pp-ref-forward` is runtime-managed and must not be authored.
892
1225
 
893
1226
  ## Final reminder
894
1227
 
@@ -36,7 +36,7 @@ Start with these rules:
36
36
  - In grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane in `layout.html` when that pane should reset on child-route navigation while the shell sidebar or rail keeps its own scroll.
37
37
  - Use `layout.py` when a layout needs shared props or metadata before rendering. The `layout()` function may be synchronous or async.
38
38
  - Keep visible route and layout markup in `index.html` and `layout.html`. Treat `index.py` and `layout.py` as backend companions, not as places to author visible HTML.
39
- - Treat every authored route and layout template like a React component body: it must have exactly one top-level parent HTML element or one imported `x-*` root, and any owned plain `<script>` must live inside that same root.
39
+ - Treat every authored route and layout template like a React component body **for root counting only**: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` inside that same root. The markup itself is plain HTML, never JSX — see [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX".
40
40
  - For route and layout interactivity, use PulsePoint in the authored HTML first: native `on*` event attributes, `pp.state(...)`, refs, effects, directives, and `pp.rpc()`. Do not create standard JavaScript event systems with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML` for normal first-party UI.
41
41
 
42
42
  ## Hard Template Invariant
@@ -189,7 +189,7 @@ Route templates follow the same authored-vs-runtime contract documented in [puls
189
189
 
190
190
  When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
191
191
 
192
- For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body: return one parent node that contains the entire route markup. This includes any owned script.
192
+ For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body **in root count only**: one parent node containing the entire route markup, including any owned script. Inside that root, write plain HTML with PulsePoint directives — `hidden="{...}"` for conditionals, `<template pp-for="…">` for lists, and quoted brace attributes such as `class="{…}"`. JSX shapes (`{cond && <div/>}`, `{list.map(...)}`, `class={...}`) break the render.
193
193
 
194
194
  Good:
195
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.26",
3
+ "version": "0.2.1",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {