caspian-utils 0.1.3 → 0.1.4
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/dist/docs/pulsepoint-runtime-map.md +26 -26
- package/dist/docs/pulsepoint.md +396 -284
- package/package.json +1 -1
|
@@ -37,12 +37,12 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
37
37
|
| State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
|
|
38
38
|
| Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
|
|
39
39
|
| Refs | `pp.ref(...)`, `pp-ref` | `pp-reactive-v2.js` | generated ref internals are runtime-managed; do not author `data-pp-ref` |
|
|
40
|
-
| Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
|
|
40
|
+
| Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
|
|
41
41
|
| Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
|
|
42
42
|
| Lists | `<template pp-for="item in items">` | `pp-reactive-v2.js` | `pp-for` belongs on `<template>`, use plain `key`, not `pp-key` |
|
|
43
|
-
| Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
|
|
43
|
+
| Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
|
|
44
44
|
| RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
|
|
45
|
-
| Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; replace state from returned payload |
|
|
45
|
+
| Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; the callback receives `{ loaded, total, percent }` and `percent` can be `null` when length is not computable; replace state from returned payload |
|
|
46
46
|
| Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
|
|
47
47
|
| SPA navigation | `body[pp-spa="true"]`, links | `pp-reactive-v2.js`, `main.py` | same-origin eligible links intercept; root-layout mismatches hard reload |
|
|
48
48
|
| Scroll restoration | `pp-reset-scroll="true"` | `pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
|
|
@@ -63,32 +63,32 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
63
63
|
- Author one root element or one imported `x-*` root per route, layout, or component template.
|
|
64
64
|
- Keep any owned plain `<script>` inside that same root.
|
|
65
65
|
- Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
|
|
66
|
-
- Use `pp.rpc(...)` for current browser-to-server calls.
|
|
67
|
-
- Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
|
|
68
|
-
- For ordinary form submits, bind `onsubmit` on the form and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; reserve `pp-ref` for imperative access rather than routine payload collection.
|
|
69
|
-
- Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
|
|
66
|
+
- Use `pp.rpc(...)` for current browser-to-server calls.
|
|
67
|
+
- Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
|
|
68
|
+
- For ordinary form submits, bind `onsubmit` on the form and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; reserve `pp-ref` for imperative access rather than routine payload collection.
|
|
69
|
+
- Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
|
|
70
70
|
- Use `pp-for` only on `<template>` and plain `key` for keyed lists.
|
|
71
71
|
- Prefer PulsePoint state and directives over manual `innerHTML` repainting.
|
|
72
72
|
- Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
|
|
73
73
|
|
|
74
74
|
## Compact Examples
|
|
75
75
|
|
|
76
|
-
Context provider:
|
|
77
|
-
|
|
78
|
-
```html
|
|
79
|
-
<section>
|
|
80
|
-
<script>
|
|
81
|
-
const ThemeContext = pp.createContext("light");
|
|
76
|
+
Context provider:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<section>
|
|
80
|
+
<script>
|
|
81
|
+
const ThemeContext = pp.createContext("light");
|
|
82
82
|
const [theme, setTheme] = pp.state("dark");
|
|
83
83
|
</script>
|
|
84
84
|
|
|
85
|
-
<themecontext.provider value="{theme}">
|
|
86
|
-
<button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
|
|
87
|
-
Theme: {theme}
|
|
88
|
-
</button>
|
|
89
|
-
</themecontext.provider>
|
|
90
|
-
</section>
|
|
91
|
-
```
|
|
85
|
+
<themecontext.provider value="{theme}">
|
|
86
|
+
<button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
|
|
87
|
+
Theme: {theme}
|
|
88
|
+
</button>
|
|
89
|
+
</themecontext.provider>
|
|
90
|
+
</section>
|
|
91
|
+
```
|
|
92
92
|
|
|
93
93
|
Upload progress:
|
|
94
94
|
|
|
@@ -104,7 +104,7 @@ Upload progress:
|
|
|
104
104
|
if (!file) return;
|
|
105
105
|
|
|
106
106
|
await pp.rpc("upload_asset", { file }, {
|
|
107
|
-
onUploadProgress: (event) => setProgress(event.
|
|
107
|
+
onUploadProgress: (event) => setProgress(Math.round(event.percent ?? 0)),
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
</script>
|
|
@@ -115,11 +115,11 @@ Grouped shell scroll reset:
|
|
|
115
115
|
|
|
116
116
|
```html
|
|
117
117
|
<section class="dashboard-shell">
|
|
118
|
-
<aside class="dashboard-sidebar">...</aside>
|
|
119
|
-
<main class="dashboard-content" pp-reset-scroll="true">
|
|
120
|
-
<slot />
|
|
121
|
-
</main>
|
|
122
|
-
</section>
|
|
118
|
+
<aside class="dashboard-sidebar">...</aside>
|
|
119
|
+
<main class="dashboard-content" pp-reset-scroll="true">
|
|
120
|
+
<slot />
|
|
121
|
+
</main>
|
|
122
|
+
</section>
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
## Verification Prompts
|
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -26,6 +26,19 @@ PulsePoint is the default reactive frontend layer for Caspian. In the current ru
|
|
|
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
|
|
|
29
|
+
## Hard Invariants
|
|
30
|
+
|
|
31
|
+
Apply these before generating any template, even without reading the rest of this page:
|
|
32
|
+
|
|
33
|
+
1. One authored top-level root per route, layout, or component template; the owned plain `<script>` lives inside that root, never as a sibling.
|
|
34
|
+
2. Never handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, or other runtime-managed attributes; the render pipeline injects them.
|
|
35
|
+
3. Bind first-party events with native `on*` attributes in the HTML; never wire normal UI with ids, `querySelector`, `addEventListener`, or manual `innerHTML`.
|
|
36
|
+
4. For ordinary form submits, use `onsubmit="{handler(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())`; refs are for imperative access only.
|
|
37
|
+
5. Keep reactive values in `pp.state(...)`; keep template-facing bindings at the top level of the script.
|
|
38
|
+
6. Call the backend with `pp.rpc(...)` backed by Python `@rpc()` actions; do not invent fetch wrappers or `pp.fetchFunction()`.
|
|
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
|
+
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
|
+
|
|
29
42
|
## Source Of Truth
|
|
30
43
|
|
|
31
44
|
When documenting or generating PulsePoint code, follow this order:
|
|
@@ -48,245 +61,209 @@ Use [core-runtime-map.md](./core-runtime-map.md) when the controlling runtime fi
|
|
|
48
61
|
|
|
49
62
|
Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when the task names a specific PulsePoint feature or directive and you need a quick feature-to-runtime lookup before reading the full guide.
|
|
50
63
|
|
|
51
|
-
## Default Frontend Rule
|
|
52
|
-
|
|
53
|
-
When a Caspian page needs reactive browser behavior, use PulsePoint.
|
|
54
|
-
|
|
55
|
-
- Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
|
|
56
|
-
- Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
|
|
57
|
-
- Bind first-party events in the HTML with PulsePoint-handled native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`.
|
|
58
|
-
- For ordinary forms, bind the submit event in the `<form>` and collect named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys, then validate and normalize those values in Python. Do not use `pp.ref(...)` on every input or an effect-managed listener just to read submitted values.
|
|
59
|
-
- When the browser needs CRUD operations or follow-up reads from the backend, call `pp.rpc()` from PulsePoint code and back it with route or backend `@rpc()` actions.
|
|
60
|
-
- Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
|
|
61
|
-
- For dashboards, admin areas, account sections, docs sections, and other grouped subtrees, keep shared shell markup and shared PulsePoint behavior in the parent folder's `layout.html`, then keep child-route PulsePoint state local to each `index.html`. Follow the same mental model as the Next.js App Router.
|
|
62
|
-
- Only introduce another frontend runtime when the user explicitly asks for it or the project already depends on one.
|
|
63
|
-
|
|
64
|
-
## PulsePoint-First Events And Reactivity
|
|
65
|
-
|
|
66
|
-
For first-party Caspian HTML, PulsePoint is not a later enhancement after ordinary JavaScript. It is the event, state, and reactivity model AI should use from the start.
|
|
67
|
-
|
|
68
|
-
Default to this workflow:
|
|
69
|
-
|
|
70
|
-
- Put the button, form, input, toggle, menu, filter, upload control, or list markup directly in the route, layout, or component HTML template.
|
|
71
|
-
- Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
|
|
72
|
-
- This is the PulsePoint `onClick`/native event-attribute model. Authored examples use lowercase HTML spellings such as `onclick` because browser HTML normalizes attribute names, but the important rule is to bind the event in the template instead of wiring it later with DOM selectors.
|
|
73
|
-
- For simple form submissions, let HTML own the form shape and let the submit event carry the values: use `const data = Object.fromEntries(new FormData(event.currentTarget).entries())` inside the handler, then pass that object directly to `pp.rpc(...)`. In a form-level submit handler, `event.target` is usually the form too, but `event.currentTarget` is the copy-safe default because it always means the element that owns the handler.
|
|
74
|
-
- Keep reactive values in `pp.state(...)`.
|
|
75
|
-
- Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
|
|
76
|
-
- Use `pp.ref(...)` and `pp-ref` when a real imperative element reference is needed, such as focus, measurement, media, canvas, third-party widgets, or resetting a specific file/password input after a successful action.
|
|
77
|
-
- Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
|
|
78
|
-
- Use `pp.rpc(...)` for browser-triggered backend reads and writes.
|
|
79
|
-
|
|
80
|
-
Avoid building a parallel JavaScript layer for normal UI behavior:
|
|
81
|
-
|
|
82
|
-
- Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
|
|
83
|
-
- Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
|
|
84
|
-
- Do not create click-in buttons by placing intent in `data-*` attributes and then scanning for those attributes. Put the action directly on the element with `onclick`, `oninput`, `onchange`, `onsubmit`, or another native `on*` event attribute.
|
|
85
|
-
- Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
|
|
86
|
-
- Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
|
|
87
|
-
- Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
|
|
88
|
-
- Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
|
|
89
|
-
|
|
90
|
-
Use direct DOM APIs only as a narrow escape hatch: third-party widgets, browser APIs that require imperative access, measurements, focus, media, canvas, or behavior the current PulsePoint runtime cannot express declaratively. Even then, keep the imperative code inside the owning PulsePoint component script, usually through `pp.ref(...)` plus `pp.effect(...)`, so PulsePoint still owns the component's state, cleanup, and event flow.
|
|
91
|
-
|
|
92
|
-
Preferred authored pattern:
|
|
93
|
-
|
|
94
|
-
```html
|
|
95
|
-
<section>
|
|
96
|
-
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
97
|
-
<button onclick="clearSearch()" disabled="{query.length === 0}">Clear</button>
|
|
98
|
-
|
|
99
|
-
<ul>
|
|
100
|
-
<template pp-for="item in filteredItems">
|
|
101
|
-
<li key="{item.id}">{item.label}</li>
|
|
102
|
-
</template>
|
|
103
|
-
</ul>
|
|
104
|
-
|
|
105
|
-
<script>
|
|
106
|
-
const [query, setQuery] = pp.state("");
|
|
107
|
-
const items = pp.props.items ?? [];
|
|
108
|
-
const filteredItems = items.filter((item) =>
|
|
109
|
-
item.label.toLowerCase().includes(query.toLowerCase())
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
function clearSearch() {
|
|
113
|
-
setQuery("");
|
|
114
|
-
}
|
|
115
|
-
</script>
|
|
116
|
-
</section>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Avoid this first-party pattern:
|
|
120
|
-
|
|
121
|
-
```html
|
|
122
|
-
<section>
|
|
123
|
-
<input id="search" />
|
|
124
|
-
<button id="clear-search">Clear</button>
|
|
125
|
-
<ul id="results"></ul>
|
|
126
|
-
|
|
127
|
-
<script>
|
|
128
|
-
const input = document.querySelector("#search");
|
|
129
|
-
const button = document.querySelector("#clear-search");
|
|
130
|
-
const results = document.querySelector("#results");
|
|
131
|
-
|
|
132
|
-
button.addEventListener("click", () => {
|
|
133
|
-
input.value = "";
|
|
134
|
-
results.innerHTML = "";
|
|
135
|
-
});
|
|
136
|
-
</script>
|
|
137
|
-
</section>
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
That second shape recreates a separate event and rendering system inside a Caspian component. It is harder to maintain because it bypasses PulsePoint's rerender, event rebinding, refs, cleanup, and backend RPC conventions.
|
|
141
|
-
|
|
142
|
-
Preferred form submit pattern:
|
|
143
|
-
|
|
144
|
-
```html
|
|
145
|
-
<section>
|
|
146
|
-
<form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
|
|
147
|
-
<input name="name" value="{{ user_name }}" autocomplete="name" required />
|
|
148
|
-
<input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
|
|
149
|
-
<input name="password" type="password" autocomplete="new-password" />
|
|
150
|
-
|
|
151
|
-
<button type="submit" disabled="{isSubmitting}">
|
|
152
|
-
{isSubmitting ? "Saving..." : "Save changes"}
|
|
153
|
-
</button>
|
|
154
|
-
</form>
|
|
155
|
-
|
|
156
|
-
<script>
|
|
157
|
-
const [isSubmitting, setIsSubmitting] = pp.state(false);
|
|
158
|
-
|
|
159
|
-
async function saveProfile(event) {
|
|
160
|
-
event.preventDefault();
|
|
161
|
-
if (isSubmitting) return;
|
|
162
|
-
|
|
163
|
-
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
164
|
-
|
|
165
|
-
setIsSubmitting(true);
|
|
166
|
-
try {
|
|
167
|
-
await pp.rpc("save_profile", data, { abortPrevious: true });
|
|
168
|
-
} finally {
|
|
169
|
-
setIsSubmitting(false);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
</script>
|
|
173
|
-
</section>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Avoid this for a normal form:
|
|
177
|
-
|
|
178
|
-
```html
|
|
179
|
-
<section>
|
|
180
|
-
<form pp-ref="{formRef}">
|
|
181
|
-
<input pp-ref="{nameRef}" name="name" />
|
|
182
|
-
<input pp-ref="{emailRef}" name="email" />
|
|
183
|
-
</form>
|
|
184
|
-
|
|
185
|
-
<script>
|
|
186
|
-
const formRef = pp.ref(null);
|
|
187
|
-
const nameRef = pp.ref(null);
|
|
188
|
-
const emailRef = pp.ref(null);
|
|
189
|
-
|
|
190
|
-
pp.effect(() => {
|
|
191
|
-
const form = formRef.current;
|
|
192
|
-
if (!form) return;
|
|
193
|
-
|
|
194
|
-
const submit = (event) => {
|
|
195
|
-
event.preventDefault();
|
|
196
|
-
return pp.rpc("save_profile", {
|
|
197
|
-
name: nameRef.current?.value ?? "",
|
|
198
|
-
email: emailRef.current?.value ?? "",
|
|
199
|
-
});
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
form.addEventListener("submit", submit);
|
|
203
|
-
return () => form.removeEventListener("submit", submit);
|
|
204
|
-
}, []);
|
|
205
|
-
</script>
|
|
206
|
-
</section>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Refs are still useful when the feature actually requires imperative access. They are not the first choice for reading standard form fields that the browser already exposes through the submit event. Keep client code minimal and put reviewable data normalization in the route's Python `@rpc()` action unless the client must transform values for UX before submitting.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await pp.rpc("save_contact", data, { abortPrevious: true });
|
|
255
|
-
} finally {
|
|
256
|
-
setIsSaving(false);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
</script>
|
|
260
|
-
</section>
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Back that form with a route-owned `@rpc()` action in the sibling `index.py`. If Prisma is enabled, the action should use the generated Prisma Python ORM for persistence.
|
|
264
|
-
|
|
265
|
-
```python
|
|
266
|
-
from casp.rpc import rpc
|
|
267
|
-
from casp.validate import Validate
|
|
268
|
-
from src.lib.prisma import prisma
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
@rpc()
|
|
272
|
-
async def save_contact(name: str, email: str):
|
|
273
|
-
name = name.strip()
|
|
274
|
-
email = email.strip().lower()
|
|
275
|
-
|
|
276
|
-
if not name:
|
|
277
|
-
raise ValueError("Name is required.")
|
|
278
|
-
if Validate.with_rules(email, "required|email") is not True:
|
|
279
|
-
raise ValueError("A valid email address is required.")
|
|
280
|
-
|
|
281
|
-
contact = await prisma.contact.create(
|
|
282
|
-
data={
|
|
283
|
-
"name": name,
|
|
284
|
-
"email": email,
|
|
285
|
-
}
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
return contact.to_dict()
|
|
289
|
-
```
|
|
64
|
+
## Default Frontend Rule
|
|
65
|
+
|
|
66
|
+
When a Caspian page needs reactive browser behavior, use PulsePoint.
|
|
67
|
+
|
|
68
|
+
- Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
|
|
69
|
+
- Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
|
|
70
|
+
- Bind first-party events in the HTML with PulsePoint-handled native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`.
|
|
71
|
+
- For ordinary forms, bind the submit event in the `<form>` and collect named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys, then validate and normalize those values in Python. Do not use `pp.ref(...)` on every input or an effect-managed listener just to read submitted values.
|
|
72
|
+
- When the browser needs CRUD operations or follow-up reads from the backend, call `pp.rpc()` from PulsePoint code and back it with route or backend `@rpc()` actions.
|
|
73
|
+
- Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
|
|
74
|
+
- For dashboards, admin areas, account sections, docs sections, and other grouped subtrees, keep shared shell markup and shared PulsePoint behavior in the parent folder's `layout.html`, then keep child-route PulsePoint state local to each `index.html`. Follow the same mental model as the Next.js App Router.
|
|
75
|
+
- Only introduce another frontend runtime when the user explicitly asks for it or the project already depends on one.
|
|
76
|
+
|
|
77
|
+
## PulsePoint-First Events And Reactivity
|
|
78
|
+
|
|
79
|
+
For first-party Caspian HTML, PulsePoint is not a later enhancement after ordinary JavaScript. It is the event, state, and reactivity model AI should use from the start.
|
|
80
|
+
|
|
81
|
+
Default to this workflow:
|
|
82
|
+
|
|
83
|
+
- Put the button, form, input, toggle, menu, filter, upload control, or list markup directly in the route, layout, or component HTML template.
|
|
84
|
+
- Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
|
|
85
|
+
- This is the PulsePoint `onClick`/native event-attribute model. Authored examples use lowercase HTML spellings such as `onclick` because browser HTML normalizes attribute names, but the important rule is to bind the event in the template instead of wiring it later with DOM selectors.
|
|
86
|
+
- For simple form submissions, let HTML own the form shape and let the submit event carry the values: use `const data = Object.fromEntries(new FormData(event.currentTarget).entries())` inside the handler, then pass that object directly to `pp.rpc(...)`. In a form-level submit handler, `event.target` is usually the form too, but `event.currentTarget` is the copy-safe default because it always means the element that owns the handler.
|
|
87
|
+
- Keep reactive values in `pp.state(...)`.
|
|
88
|
+
- Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
|
|
89
|
+
- Use `pp.ref(...)` and `pp-ref` when a real imperative element reference is needed, such as focus, measurement, media, canvas, third-party widgets, or resetting a specific file/password input after a successful action.
|
|
90
|
+
- Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
|
|
91
|
+
- Use `pp.rpc(...)` for browser-triggered backend reads and writes.
|
|
92
|
+
|
|
93
|
+
Avoid building a parallel JavaScript layer for normal UI behavior:
|
|
94
|
+
|
|
95
|
+
- Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
|
|
96
|
+
- Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
|
|
97
|
+
- Do not create click-in buttons by placing intent in `data-*` attributes and then scanning for those attributes. Put the action directly on the element with `onclick`, `oninput`, `onchange`, `onsubmit`, or another native `on*` event attribute.
|
|
98
|
+
- Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
|
|
99
|
+
- Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
|
|
100
|
+
- Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
|
|
101
|
+
- Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
|
|
102
|
+
|
|
103
|
+
Use direct DOM APIs only as a narrow escape hatch: third-party widgets, browser APIs that require imperative access, measurements, focus, media, canvas, or behavior the current PulsePoint runtime cannot express declaratively. Even then, keep the imperative code inside the owning PulsePoint component script, usually through `pp.ref(...)` plus `pp.effect(...)`, so PulsePoint still owns the component's state, cleanup, and event flow.
|
|
104
|
+
|
|
105
|
+
Preferred authored pattern:
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<section>
|
|
109
|
+
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
110
|
+
<button onclick="clearSearch()" disabled="{query.length === 0}">Clear</button>
|
|
111
|
+
|
|
112
|
+
<ul>
|
|
113
|
+
<template pp-for="item in filteredItems">
|
|
114
|
+
<li key="{item.id}">{item.label}</li>
|
|
115
|
+
</template>
|
|
116
|
+
</ul>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
const [query, setQuery] = pp.state("");
|
|
120
|
+
const items = pp.props.items ?? [];
|
|
121
|
+
const filteredItems = items.filter((item) =>
|
|
122
|
+
item.label.toLowerCase().includes(query.toLowerCase())
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
function clearSearch() {
|
|
126
|
+
setQuery("");
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
</section>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Avoid this first-party pattern:
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<section>
|
|
136
|
+
<input id="search" />
|
|
137
|
+
<button id="clear-search">Clear</button>
|
|
138
|
+
<ul id="results"></ul>
|
|
139
|
+
|
|
140
|
+
<script>
|
|
141
|
+
const input = document.querySelector("#search");
|
|
142
|
+
const button = document.querySelector("#clear-search");
|
|
143
|
+
const results = document.querySelector("#results");
|
|
144
|
+
|
|
145
|
+
button.addEventListener("click", () => {
|
|
146
|
+
input.value = "";
|
|
147
|
+
results.innerHTML = "";
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
</section>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That second shape recreates a separate event and rendering system inside a Caspian component. It is harder to maintain because it bypasses PulsePoint's rerender, event rebinding, refs, cleanup, and backend RPC conventions.
|
|
154
|
+
|
|
155
|
+
Preferred form submit pattern:
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<section>
|
|
159
|
+
<form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
|
|
160
|
+
<input name="name" value="{{ user_name }}" autocomplete="name" required />
|
|
161
|
+
<input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
|
|
162
|
+
<input name="password" type="password" autocomplete="new-password" />
|
|
163
|
+
|
|
164
|
+
<button type="submit" disabled="{isSubmitting}">
|
|
165
|
+
{isSubmitting ? "Saving..." : "Save changes"}
|
|
166
|
+
</button>
|
|
167
|
+
</form>
|
|
168
|
+
|
|
169
|
+
<script>
|
|
170
|
+
const [isSubmitting, setIsSubmitting] = pp.state(false);
|
|
171
|
+
|
|
172
|
+
async function saveProfile(event) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
if (isSubmitting) return;
|
|
175
|
+
|
|
176
|
+
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
177
|
+
|
|
178
|
+
setIsSubmitting(true);
|
|
179
|
+
try {
|
|
180
|
+
await pp.rpc("save_profile", data, { abortPrevious: true });
|
|
181
|
+
} finally {
|
|
182
|
+
setIsSubmitting(false);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
</section>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Avoid this for a normal form:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<section>
|
|
193
|
+
<form pp-ref="{formRef}">
|
|
194
|
+
<input pp-ref="{nameRef}" name="name" />
|
|
195
|
+
<input pp-ref="{emailRef}" name="email" />
|
|
196
|
+
</form>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
const formRef = pp.ref(null);
|
|
200
|
+
const nameRef = pp.ref(null);
|
|
201
|
+
const emailRef = pp.ref(null);
|
|
202
|
+
|
|
203
|
+
pp.effect(() => {
|
|
204
|
+
const form = formRef.current;
|
|
205
|
+
if (!form) return;
|
|
206
|
+
|
|
207
|
+
const submit = (event) => {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
return pp.rpc("save_profile", {
|
|
210
|
+
name: nameRef.current?.value ?? "",
|
|
211
|
+
email: emailRef.current?.value ?? "",
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
form.addEventListener("submit", submit);
|
|
216
|
+
return () => form.removeEventListener("submit", submit);
|
|
217
|
+
}, []);
|
|
218
|
+
</script>
|
|
219
|
+
</section>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Refs are still useful when the feature actually requires imperative access. They are not the first choice for reading standard form fields that the browser already exposes through the submit event. Keep client code minimal and put reviewable data normalization in the route's Python `@rpc()` action unless the client must transform values for UX before submitting.
|
|
223
|
+
|
|
224
|
+
Back the form with a route-owned `@rpc()` action in the sibling `index.py`. If Prisma is enabled, the action should use the generated Prisma Python ORM for persistence.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from casp.rpc import rpc
|
|
228
|
+
from casp.validate import Validate
|
|
229
|
+
from src.lib.prisma import prisma
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@rpc()
|
|
233
|
+
async def save_profile(name: str, email: str, password: str = ""):
|
|
234
|
+
name = name.strip()
|
|
235
|
+
email = email.strip().lower()
|
|
236
|
+
|
|
237
|
+
if not name:
|
|
238
|
+
raise ValueError("Name is required.")
|
|
239
|
+
if Validate.with_rules(email, "required|email") is not True:
|
|
240
|
+
raise ValueError("A valid email address is required.")
|
|
241
|
+
|
|
242
|
+
profile = await prisma.user.update(
|
|
243
|
+
where={"email": email},
|
|
244
|
+
data={"name": name},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return profile.to_dict()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Basic two-way state pattern:
|
|
251
|
+
|
|
252
|
+
```html
|
|
253
|
+
<section>
|
|
254
|
+
<label>
|
|
255
|
+
Name
|
|
256
|
+
<input name="name" value="{name}" oninput="setName(event.target.value)" />
|
|
257
|
+
</label>
|
|
258
|
+
|
|
259
|
+
<p>Hello, {name || "friend"}.</p>
|
|
260
|
+
<button onclick="setName('')">Clear</button>
|
|
261
|
+
|
|
262
|
+
<script>
|
|
263
|
+
const [name, setName] = pp.state("");
|
|
264
|
+
</script>
|
|
265
|
+
</section>
|
|
266
|
+
```
|
|
290
267
|
|
|
291
268
|
## Authoring Model
|
|
292
269
|
|
|
@@ -447,14 +424,101 @@ Notes:
|
|
|
447
424
|
- Keep template-facing bindings at the top level so the AST-based exporter can see them.
|
|
448
425
|
- For predictable code generation, prefer passing an explicit dependency array to `pp.effect`, `pp.layoutEffect`, `pp.memo`, and `pp.callback`.
|
|
449
426
|
|
|
427
|
+
Effect with cleanup and dependencies:
|
|
428
|
+
|
|
429
|
+
```html
|
|
430
|
+
<section>
|
|
431
|
+
<p>Elapsed: {seconds}s</p>
|
|
432
|
+
<button onclick="setRunning(!running)">{running ? "Pause" : "Resume"}</button>
|
|
433
|
+
|
|
434
|
+
<script>
|
|
435
|
+
const [seconds, setSeconds] = pp.state(0);
|
|
436
|
+
const [running, setRunning] = pp.state(true);
|
|
437
|
+
|
|
438
|
+
pp.effect(() => {
|
|
439
|
+
if (!running) return;
|
|
440
|
+
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
|
|
441
|
+
return () => clearInterval(id);
|
|
442
|
+
}, [running]);
|
|
443
|
+
</script>
|
|
444
|
+
</section>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Use the same shape for any subscription: attach in the effect body, detach in the returned cleanup. The cleanup also runs on component disposal, which is why route-owned `WebSocket` clients close their socket from a cleanup effect.
|
|
448
|
+
|
|
449
|
+
Memoized derived value:
|
|
450
|
+
|
|
451
|
+
```html
|
|
452
|
+
<section>
|
|
453
|
+
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
454
|
+
<p>{visible.length} of {items.length} items</p>
|
|
455
|
+
|
|
456
|
+
<script>
|
|
457
|
+
const [query, setQuery] = pp.state("");
|
|
458
|
+
const items = pp.props.items ?? [];
|
|
459
|
+
|
|
460
|
+
const visible = pp.memo(
|
|
461
|
+
() => items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())),
|
|
462
|
+
[query]
|
|
463
|
+
);
|
|
464
|
+
</script>
|
|
465
|
+
</section>
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
For cheap derivations, a plain top-level `const` recomputed each render is fine; reach for `pp.memo` when the computation is heavy or the result identity matters for dependencies.
|
|
469
|
+
|
|
470
|
+
Reducer for multi-field update logic:
|
|
471
|
+
|
|
472
|
+
```html
|
|
473
|
+
<section>
|
|
474
|
+
<p>{cart.count} items, total {cart.total}</p>
|
|
475
|
+
<button onclick="dispatch({ type: 'add', price: 10 })">Add</button>
|
|
476
|
+
<button onclick="dispatch({ type: 'clear' })">Clear</button>
|
|
477
|
+
|
|
478
|
+
<script>
|
|
479
|
+
const [cart, dispatch] = pp.reducer((state, action) => {
|
|
480
|
+
if (action.type === "add") {
|
|
481
|
+
return { count: state.count + 1, total: state.total + action.price };
|
|
482
|
+
}
|
|
483
|
+
if (action.type === "clear") {
|
|
484
|
+
return { count: 0, total: 0 };
|
|
485
|
+
}
|
|
486
|
+
return state;
|
|
487
|
+
}, { count: 0, total: 0 });
|
|
488
|
+
</script>
|
|
489
|
+
</section>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Portal pattern for overlays that must escape clipping ancestors:
|
|
493
|
+
|
|
494
|
+
```html
|
|
495
|
+
<div>
|
|
496
|
+
<button onclick="setOpen(true)">Open dialog</button>
|
|
497
|
+
|
|
498
|
+
<div pp-ref="dialogRef" hidden="{!open}" class="modal">
|
|
499
|
+
<p>Rendered under document.body.</p>
|
|
500
|
+
<button onclick="setOpen(false)">Close</button>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<script>
|
|
504
|
+
const [open, setOpen] = pp.state(false);
|
|
505
|
+
const dialogRef = pp.ref(null);
|
|
506
|
+
|
|
507
|
+
pp.portal(dialogRef);
|
|
508
|
+
</script>
|
|
509
|
+
</div>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`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.
|
|
513
|
+
|
|
450
514
|
## Context
|
|
451
515
|
|
|
452
|
-
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`.
|
|
516
|
+
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`.
|
|
453
517
|
|
|
454
518
|
How it works:
|
|
455
519
|
|
|
456
520
|
- Create a token with `pp.createContext(defaultValue)`.
|
|
457
|
-
- Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
|
|
521
|
+
- Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
|
|
458
522
|
- Read it in a descendant component with `pp.context(token)`.
|
|
459
523
|
- Resolution walks the logical component parent chain stored in the registry, not the live DOM.
|
|
460
524
|
- Portaled descendants still resolve providers through component ancestry.
|
|
@@ -468,24 +532,24 @@ Important:
|
|
|
468
532
|
- Context is component-level, not directive-based. There is no `pp-context` attribute.
|
|
469
533
|
- `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
|
|
470
534
|
- If provider and consumer live in different component script scopes, pass the token through props or store it in shared outer or global state.
|
|
471
|
-
- The preferred authoring style is a lowercase provider tag in the template. The TypeScript runtime's `TemplateCompiler.transformContextProviderTags(...)` recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The runtime context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
|
|
472
|
-
- The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but that form provides from the component boundary rather than documenting the HTML subtree shape. Use it only when component-wide provision is intended.
|
|
473
|
-
- Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
|
|
535
|
+
- The preferred authoring style is a lowercase provider tag in the template. The TypeScript runtime's `TemplateCompiler.transformContextProviderTags(...)` recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The runtime context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
|
|
536
|
+
- The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but that form provides from the component boundary rather than documenting the HTML subtree shape. Use it only when component-wide provision is intended.
|
|
537
|
+
- Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
|
|
474
538
|
|
|
475
539
|
Provider example:
|
|
476
540
|
|
|
477
541
|
```html
|
|
478
542
|
<section>
|
|
479
|
-
<script>
|
|
480
|
-
const ThemeContext = pp.createContext("light");
|
|
481
|
-
const [theme] = pp.state("dark");
|
|
482
|
-
</script>
|
|
483
|
-
|
|
484
|
-
<themecontext.provider value="{theme}">
|
|
485
|
-
<p>This subtree receives the provided theme.</p>
|
|
486
|
-
</themecontext.provider>
|
|
487
|
-
</section>
|
|
488
|
-
```
|
|
543
|
+
<script>
|
|
544
|
+
const ThemeContext = pp.createContext("light");
|
|
545
|
+
const [theme] = pp.state("dark");
|
|
546
|
+
</script>
|
|
547
|
+
|
|
548
|
+
<themecontext.provider value="{theme}">
|
|
549
|
+
<p>This subtree receives the provided theme.</p>
|
|
550
|
+
</themecontext.provider>
|
|
551
|
+
</section>
|
|
552
|
+
```
|
|
489
553
|
|
|
490
554
|
Consumer example for a child component that receives the token through props:
|
|
491
555
|
|
|
@@ -555,10 +619,10 @@ Example:
|
|
|
555
619
|
</div>
|
|
556
620
|
```
|
|
557
621
|
|
|
558
|
-
## Refs
|
|
559
|
-
|
|
560
|
-
- Refs are for imperative element access. Do not use `pp-ref` as the default way to read normal form input values on submit; prefer the form's `onsubmit` event plus `Object.fromEntries(new FormData(event.currentTarget).entries())`.
|
|
561
|
-
- Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
|
|
622
|
+
## Refs
|
|
623
|
+
|
|
624
|
+
- Refs are for imperative element access. Do not use `pp-ref` as the default way to read normal form input values on submit; prefer the form's `onsubmit` event plus `Object.fromEntries(new FormData(event.currentTarget).entries())`.
|
|
625
|
+
- Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
|
|
562
626
|
- Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
|
|
563
627
|
- `pp.ref(null)` is the normal way to create a ref object.
|
|
564
628
|
- Callback refs and `{ current }` refs are both supported.
|
|
@@ -618,19 +682,19 @@ Example:
|
|
|
618
682
|
</div>
|
|
619
683
|
```
|
|
620
684
|
|
|
621
|
-
## Events
|
|
622
|
-
|
|
623
|
-
- Use native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit` for first-party events.
|
|
624
|
-
- Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
|
|
625
|
-
- Event values may be raw code or wrapped in `{...}`.
|
|
626
|
-
- The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
|
|
627
|
-
- For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
|
|
628
|
-
- `event.target` can also work for form-level submit handlers, but examples use `event.currentTarget` so nested event origins do not change which element becomes the `FormData` source.
|
|
629
|
-
- Do not use hyphenated event attrs like `on-click`.
|
|
630
|
-
- Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
|
|
631
|
-
- Owned template/event-owner internals are runtime-managed. Do not author them directly.
|
|
632
|
-
- Do not replace normal PulsePoint event attributes with id-driven `querySelector(...)` plus `addEventListener(...)` wiring. If an imperative listener is unavoidable for an integration, attach and clean it up from `pp.effect(...)`.
|
|
633
|
-
- Do not replace a normal `onclick` with a `data-action`, `data-target`, or similar attribute plus a script that scans the DOM. PulsePoint event attributes are the event contract for first-party Caspian UI.
|
|
685
|
+
## Events
|
|
686
|
+
|
|
687
|
+
- Use native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit` for first-party events.
|
|
688
|
+
- Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
|
|
689
|
+
- Event values may be raw code or wrapped in `{...}`.
|
|
690
|
+
- The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
|
|
691
|
+
- For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
|
|
692
|
+
- `event.target` can also work for form-level submit handlers, but examples use `event.currentTarget` so nested event origins do not change which element becomes the `FormData` source.
|
|
693
|
+
- Do not use hyphenated event attrs like `on-click`.
|
|
694
|
+
- Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
|
|
695
|
+
- Owned template/event-owner internals are runtime-managed. Do not author them directly.
|
|
696
|
+
- Do not replace normal PulsePoint event attributes with id-driven `querySelector(...)` plus `addEventListener(...)` wiring. If an imperative listener is unavoidable for an integration, attach and clean it up from `pp.effect(...)`.
|
|
697
|
+
- Do not replace a normal `onclick` with a `data-action`, `data-target`, or similar attribute plus a script that scans the DOM. PulsePoint event attributes are the event contract for first-party Caspian UI.
|
|
634
698
|
|
|
635
699
|
## SPA, loading, and navigation helpers
|
|
636
700
|
|
|
@@ -654,8 +718,9 @@ RPC notes:
|
|
|
654
718
|
- Passing `true` as the third argument means `abortPrevious: true`.
|
|
655
719
|
- The options object supports `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, and `onUploadComplete`.
|
|
656
720
|
- File uploads switch to the XHR path when upload progress callbacks are needed.
|
|
721
|
+
- The `onUploadProgress` callback receives `{ loaded, total, percent }`. `percent` is a 0-100 number, and both `total` and `percent` are `null` when the browser cannot compute the upload length. Do not document or generate `event.percentage`.
|
|
657
722
|
- For file managers, use upload callbacks for progress UI but replace the asset list from returned RPC state with `pp.state(...)` and `pp-for` instead of manual DOM repainting. See [file-uploads.md](./file-uploads.md).
|
|
658
|
-
- Streamed `text/event-stream` responses are supported when a stream handler is provided.
|
|
723
|
+
- Streamed `text/event-stream` responses are supported when a stream handler is provided. Each `onStream` chunk is JSON-parsed when the event data looks like JSON; otherwise the raw string is passed through. If the response streams but no `onStream` handler was given, the runtime warns and discards the stream.
|
|
659
724
|
- Redirect headers are honored through `pp.redirect()`.
|
|
660
725
|
|
|
661
726
|
Notes:
|
|
@@ -664,6 +729,53 @@ Notes:
|
|
|
664
729
|
- Root-layout mismatches during SPA navigation trigger a hard reload.
|
|
665
730
|
- `pp.mount()` bootstraps every `[pp-component]` it finds, so generated code should call it only through the global runtime if you are manually mounting at all.
|
|
666
731
|
|
|
732
|
+
Upload with progress pattern:
|
|
733
|
+
|
|
734
|
+
```html
|
|
735
|
+
<section>
|
|
736
|
+
<input type="file" onchange="{uploadFile(event.target.files?.[0])}" />
|
|
737
|
+
<progress max="100" value="{percent ?? 0}"></progress>
|
|
738
|
+
|
|
739
|
+
<script>
|
|
740
|
+
const [percent, setPercent] = pp.state(null);
|
|
741
|
+
|
|
742
|
+
async function uploadFile(file) {
|
|
743
|
+
if (!file) return;
|
|
744
|
+
|
|
745
|
+
await pp.rpc("upload_asset", { file }, {
|
|
746
|
+
onUploadProgress: ({ percent }) => setPercent(percent),
|
|
747
|
+
onUploadComplete: () => setPercent(100),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
</script>
|
|
751
|
+
</section>
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Streaming pattern for server generators that return `text/event-stream`:
|
|
755
|
+
|
|
756
|
+
```html
|
|
757
|
+
<section>
|
|
758
|
+
<button onclick="ask()" disabled="{isStreaming}">Ask</button>
|
|
759
|
+
<pre>{answer}</pre>
|
|
760
|
+
|
|
761
|
+
<script>
|
|
762
|
+
const [answer, setAnswer] = pp.state("");
|
|
763
|
+
const [isStreaming, setIsStreaming] = pp.state(false);
|
|
764
|
+
|
|
765
|
+
function ask() {
|
|
766
|
+
setAnswer("");
|
|
767
|
+
setIsStreaming(true);
|
|
768
|
+
|
|
769
|
+
pp.rpc("ask_question", { topic: "caspian" }, {
|
|
770
|
+
onStream: (chunk) => setAnswer((current) => current + chunk),
|
|
771
|
+
onStreamError: () => setIsStreaming(false),
|
|
772
|
+
onStreamComplete: () => setIsStreaming(false),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
</script>
|
|
776
|
+
</section>
|
|
777
|
+
```
|
|
778
|
+
|
|
667
779
|
## Runtime Output And Debugging
|
|
668
780
|
|
|
669
781
|
When you inspect rendered HTML in the browser, you should expect to see runtime-managed attributes and elements that are not part of authored source.
|
|
@@ -684,23 +796,23 @@ These are runtime details.
|
|
|
684
796
|
|
|
685
797
|
Use these rules when generating or editing PulsePoint runtime code:
|
|
686
798
|
|
|
687
|
-
- Treat PulsePoint as the default reactive frontend for Caspian app code.
|
|
688
|
-
- For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
|
|
689
|
-
- 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.
|
|
690
|
-
- Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
|
|
799
|
+
- Treat PulsePoint as the default reactive frontend for Caspian app code.
|
|
800
|
+
- For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
|
|
801
|
+
- 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.
|
|
802
|
+
- Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
|
|
691
803
|
- Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
|
|
692
804
|
- Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
|
|
693
805
|
- If a development-only source tree exists behind the shipped runtime, treat it as optional implementation detail rather than something generated apps are guaranteed to contain.
|
|
694
806
|
- In authored Caspian templates, do not handwrite `pp-component` or `type="text/pp"`; let the render pipeline inject them.
|
|
695
807
|
- For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md), keep the shared interactive shell in the parent folder's `layout.html`, and keep route-specific PulsePoint code in each child `index.html`.
|
|
696
808
|
- For grouped shells with independent shell and content scrolling, put `pp-reset-scroll="true"` on the content pane rather than the whole shell when only the page content should reset between child-route navigations.
|
|
697
|
-
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
698
|
-
- Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
|
|
699
|
-
- Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
|
|
809
|
+
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
810
|
+
- Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
|
|
811
|
+
- Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
|
|
700
812
|
- If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
|
|
701
813
|
- In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
|
|
702
814
|
- Keep template-facing variables at top level.
|
|
703
|
-
- Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
|
|
815
|
+
- Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
|
|
704
816
|
- Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
|
|
705
817
|
- Keep `pp-for` on `<template>` and use plain `key`.
|
|
706
818
|
- Use native `on*` attributes, not framework-specific event syntax.
|
|
@@ -715,9 +827,9 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
715
827
|
|
|
716
828
|
Do not generate these unless the current source explicitly adds support:
|
|
717
829
|
|
|
718
|
-
- React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
|
|
719
|
-
- 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
|
|
720
|
-
- `pp-context`
|
|
830
|
+
- React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
|
|
831
|
+
- 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
|
|
832
|
+
- `pp-context`
|
|
721
833
|
- `pp-key`
|
|
722
834
|
- `data-pp-ref`
|
|
723
835
|
- `pp-context-provider`
|
|
@@ -741,7 +853,7 @@ These are current runtime caveats that matter for authors and AI tools:
|
|
|
741
853
|
- The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
|
|
742
854
|
- `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
|
|
743
855
|
- `pp.context()` resolves through ancestor components, not the current component's own pending providers.
|
|
744
|
-
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
856
|
+
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
745
857
|
- `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
|
|
746
858
|
|
|
747
859
|
## Final reminder
|