caspian-utils 0.0.34 → 0.1.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.
- package/dist/docs/ai-validation-checklist.md +3 -3
- package/dist/docs/auth.md +19 -28
- package/dist/docs/components.md +1 -3
- package/dist/docs/core-runtime-map.md +3 -4
- package/dist/docs/database.md +2 -2
- package/dist/docs/fetch-data.md +49 -17
- package/dist/docs/file-conventions.md +11 -9
- package/dist/docs/metadata.md +10 -10
- package/dist/docs/project-structure.md +2 -2
- package/dist/docs/pulsepoint-runtime-map.md +116 -115
- package/dist/docs/pulsepoint.md +97 -20
- package/dist/docs/routing.md +39 -37
- package/package.json +1 -1
|
@@ -55,14 +55,14 @@ Use prompts like these to check whether AI lands on the correct docs and files.
|
|
|
55
55
|
| Create a protected dashboard section with child routes and a shared shell. | [index.md](./index.md), [routing.md](./routing.md), [auth.md](./auth.md), [project-structure.md](./project-structure.md) | `src/app/**`, `src/lib/auth/auth_config.py`, `main.py`, `.venv/Lib/site-packages/casp/layout.py` | section layout ownership, route privacy mode, and child-route wrapping |
|
|
56
56
|
| Make a grouped shell keep sidebar scroll while resetting page content on child-route navigation. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/layout.html`, `public/js/pp-reactive-v2.js`, `main.py` | `pp-reset-scroll` placement, push-vs-history scroll behavior, and shared-shell ownership |
|
|
57
57
|
| Create a new contact page with interactive form behavior. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `src/app/**/index.html`, `main.py`, `.venv/Lib/site-packages/casp/components_compiler.py`, `.venv/Lib/site-packages/casp/scripts_type.py` | single-root template shape, script-inside-root authoring, and authored-vs-runtime boundaries |
|
|
58
|
-
| Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, and avoiding id-driven `querySelector` or `addEventListener` wiring |
|
|
58
|
+
| Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
|
|
59
59
|
| Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and Prisma-backed storage boundaries |
|
|
60
60
|
| Explain why authored Caspian templates use a plain `<script>` instead of `type="text/pp"`. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [routing.md](./routing.md) | `main.py`, `.venv/Lib/site-packages/casp/scripts_type.py`, `.venv/Lib/site-packages/casp/components_compiler.py` | script rewriting, `pp-component` injection, and authored-vs-runtime boundaries |
|
|
61
61
|
| Debug why `StateManager` does not persist across a full redirect. | [index.md](./index.md), [state.md](./state.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/state_manager.py` | wire vs non-wire reset behavior and `request.state.session` dependency |
|
|
62
62
|
| Trace the auth cookie and CSRF cookie names used in development. | [index.md](./index.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/auth.py` | development cookie scoping and CSRF or session naming ownership |
|
|
63
63
|
| Find how route params and query params reach `page()`. | [index.md](./index.md), [routing.md](./routing.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/layout.py` | path-dict delivery, query coercion, and `request` injection |
|
|
64
64
|
| Decide whether a dashboard child route should use page caching. | [index.md](./index.md), [cache.md](./cache.md), [routing.md](./routing.md), [fetch-data.md](./fetch-data.md) | `main.py`, `.venv/Lib/site-packages/casp/cache_handler.py`, the route's `index.py` | route-level cache ownership, cacheable HTML scope, and invalidation points |
|
|
65
|
-
| Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | `
|
|
65
|
+
| Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `ts/TemplateCompiler.ts`, `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | lowercase HTML-first `*.provider` usage, logical ancestry, single-root template shape |
|
|
66
66
|
| Add upload progress to an existing file manager. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md) | `src/app/**/index.py`, `src/app/**/index.html`, `src/lib/**`, `public/js/pp-reactive-v2.js` | `pp.rpc` upload options, state replacement, persisted metadata, BrowserSync ignore rules |
|
|
67
67
|
| Decide whether MCP files should be created. | [index.md](./index.md), [mcp.md](./mcp.md), [commands.md](./commands.md), [project-structure.md](./project-structure.md) | `caspian.config.json`, `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` when enabled | `mcp` feature gate, update workflow, nested FastMCP config ownership |
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ Treat the table as a prompt pack for spot checks, not as a full validation matri
|
|
|
75
75
|
- AI edits framework internals when the task only requires app-owned route or helper changes.
|
|
76
76
|
- AI treats runtime HTML examples as authored template examples.
|
|
77
77
|
- AI generates a route or component template with a valid-looking root element but leaves a sibling top-level `<script>` or second top-level element after it.
|
|
78
|
-
- AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`,
|
|
78
|
+
- AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, manual `innerHTML`, or per-input `pp-ref` payload collection instead of PulsePoint `on*` attributes, form submit events, state, directives, and `pp.rpc()`.
|
|
79
79
|
- AI puts `pp-reset-scroll="true"` on the whole shell or `body` when only the page-content pane should reset, causing persistent sidebars or rails to lose their scroll position.
|
|
80
80
|
- AI decides behavior from memory without checking the owning implementation details.
|
|
81
81
|
|
package/dist/docs/auth.md
CHANGED
|
@@ -20,14 +20,14 @@ Treat `casp.auth` as the default authentication layer in Caspian app code. Do no
|
|
|
20
20
|
|
|
21
21
|
## Overview
|
|
22
22
|
|
|
23
|
-
Caspian authentication has two main layers:
|
|
24
|
-
|
|
25
|
-
- app-level policy
|
|
26
|
-
- framework runtime behavior
|
|
23
|
+
Caspian authentication has two main layers:
|
|
24
|
+
|
|
25
|
+
- app-level policy, controlled by `src/lib/auth/auth_config.py`
|
|
26
|
+
- framework runtime behavior, implemented by `.venv/Lib/site-packages/casp/auth.py`
|
|
27
27
|
|
|
28
28
|
The main public API includes:
|
|
29
29
|
|
|
30
|
-
- `AuthSettings` for centralized auth
|
|
30
|
+
- `AuthSettings` for centralized app auth policy
|
|
31
31
|
- `configure_auth(...)` and `get_auth_settings()` for app startup and reads
|
|
32
32
|
- the global `auth` object for session lifecycle work
|
|
33
33
|
- `require_auth`, `require_role`, and `guest_only` for page-level protection
|
|
@@ -49,7 +49,7 @@ from casp.auth import (
|
|
|
49
49
|
|
|
50
50
|
## Default Auth Rule
|
|
51
51
|
|
|
52
|
-
- Define app-wide auth behavior in `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
|
|
52
|
+
- Define app-wide auth behavior in `src/lib/auth/auth_config.py` through `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
|
|
53
53
|
- Use `auth.sign_in(...)` and `auth.sign_out(...)` instead of setting or clearing session keys directly.
|
|
54
54
|
- Prefer `pp.rpc(...)` plus `@rpc(require_auth=True)` for signout buttons or menus rendered in pages or components. Use a dedicated signout route only when you need a plain HTML form POST or a no-JavaScript fallback.
|
|
55
55
|
- Use `@require_auth`, `@require_role`, and `@guest_only` for page access rules.
|
|
@@ -61,15 +61,15 @@ from casp.auth import (
|
|
|
61
61
|
|
|
62
62
|
## Framework Internals Note
|
|
63
63
|
|
|
64
|
-
- The centralized app auth
|
|
65
|
-
- The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
|
|
66
|
-
- Treat `auth_config.py` as project code and `casp/auth.py` as framework code.
|
|
64
|
+
- The centralized app auth policy controller is `src/lib/auth/auth_config.py`.
|
|
65
|
+
- The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
|
|
66
|
+
- Treat `auth_config.py` as project code and `casp/auth.py` as framework runtime code. Do not edit `casp/auth.py` to control app route privacy, redirects, or RBAC.
|
|
67
67
|
- If upstream docs and the installed implementation disagree, prefer the installed implementation for local project guidance.
|
|
68
68
|
- Use [core-runtime-map.md](./core-runtime-map.md) when an auth task crosses `main.py` bootstrap behavior such as development cookie scoping or middleware ownership.
|
|
69
69
|
|
|
70
70
|
## Centralized Auth Settings
|
|
71
71
|
|
|
72
|
-
Keep application auth policy in `src/lib/auth/auth_config.py`.
|
|
72
|
+
Keep application auth policy in `src/lib/auth/auth_config.py`. This file is the controller for route privacy, redirects, and RBAC.
|
|
73
73
|
|
|
74
74
|
Example:
|
|
75
75
|
|
|
@@ -80,10 +80,12 @@ from casp.auth import AuthSettings
|
|
|
80
80
|
|
|
81
81
|
def build_auth_settings() -> AuthSettings:
|
|
82
82
|
"""
|
|
83
|
-
Centralized auth
|
|
84
|
-
|
|
85
|
-
Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
|
|
86
|
-
Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
|
|
83
|
+
Centralized app auth policy controller.
|
|
84
|
+
|
|
85
|
+
Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
|
|
86
|
+
Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
|
|
87
|
+
Decide route privacy, redirects, and RBAC here at app setup time instead of
|
|
88
|
+
changing Caspian core runtime files.
|
|
87
89
|
"""
|
|
88
90
|
|
|
89
91
|
return AuthSettings(
|
|
@@ -101,7 +103,7 @@ def build_auth_settings() -> AuthSettings:
|
|
|
101
103
|
is_role_based=False,
|
|
102
104
|
role_identifier="role",
|
|
103
105
|
|
|
104
|
-
#
|
|
106
|
+
# RBAC policy is app-owned here; the runtime expects ROUTE/PATTERN -> [ROLES].
|
|
105
107
|
role_based_routes={},
|
|
106
108
|
|
|
107
109
|
# Redirects / prefixes
|
|
@@ -150,7 +152,7 @@ Make this decision at app setup time in `src/lib/auth/auth_config.py`.
|
|
|
150
152
|
- In the current runtime, `auth_routes=["/signin", "/signup"]` stays public by default, and most apps do not need to change it unless the user explicitly asks for different auth routes.
|
|
151
153
|
- In all-private mode, the default `public_routes=["/"]` keeps the home page public unless you change that list.
|
|
152
154
|
- `token_auto_refresh=True` does not make routes private. It only enables sliding-session refresh when the request lifecycle calls `auth.refresh_session()`.
|
|
153
|
-
-
|
|
155
|
+
- Do not modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
|
|
154
156
|
- If you customize `src/lib/auth/auth_config.py`, add it to `excludeFiles` in `caspian.config.json` so update commands do not overwrite your local auth policy.
|
|
155
157
|
|
|
156
158
|
Example all-private setup with a few public exceptions:
|
|
@@ -765,18 +767,7 @@ Behavior:
|
|
|
765
767
|
|
|
766
768
|
Use this helper when custom form or fetch flows need access to the session CSRF token.
|
|
767
769
|
|
|
768
|
-
##
|
|
769
|
-
|
|
770
|
-
The installed auth file still includes `AuthConfig` as a compatibility alias.
|
|
771
|
-
|
|
772
|
-
It exposes:
|
|
773
|
-
|
|
774
|
-
- property proxies for `PUBLIC_ROUTES`, `PRIVATE_ROUTES`, `AUTH_ROUTES`, `IS_ALL_ROUTES_PRIVATE`, `DEFAULT_SIGNIN_REDIRECT`, and `DEFAULT_SIGNOUT_REDIRECT`
|
|
775
|
-
- `AuthConfig.check_auth_role(...)` as a proxy to `auth.check_role(...)`
|
|
776
|
-
|
|
777
|
-
Prefer `AuthSettings`, `configure_auth(...)`, and `auth.settings` in new code.
|
|
778
|
-
|
|
779
|
-
## Current Implementation Notes
|
|
770
|
+
## Current Implementation Notes
|
|
780
771
|
|
|
781
772
|
- The installed auth runtime is session-backed. It stores the auth payload and CSRF token in `request.session`.
|
|
782
773
|
- Expiration uses timestamps and the current duration parser only accepts `s`, `m`, `h`, and `d` units.
|
package/dist/docs/components.md
CHANGED
|
@@ -39,8 +39,6 @@ When the task is about component internals rather than normal app-owned componen
|
|
|
39
39
|
- `.venv/Lib/site-packages/casp/component_decorator.py` owns `@component`, `Component`, `render_html(...)`, and component loading.
|
|
40
40
|
- `.venv/Lib/site-packages/casp/components_compiler.py` owns `@import` parsing, `x-*` tag resolution, root validation, and `pp-component` injection.
|
|
41
41
|
- `.venv/Lib/site-packages/casp/html_attrs.py` owns `get_attributes(...)` and the Python-side `merge_classes(...)` contract.
|
|
42
|
-
- `.venv/Lib/site-packages/casp/syntax_compiler.py` owns Caspian template syntax transpilation before Jinja render.
|
|
43
|
-
|
|
44
42
|
Use [core-runtime-map.md](./core-runtime-map.md) when you need the broader Python runtime-module map. Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when a component task is specifically about browser-side PulsePoint state, refs, context, portals, events, RPC, or SPA behavior.
|
|
45
43
|
|
|
46
44
|
## Basic Component
|
|
@@ -203,7 +201,7 @@ def Counter(label: str = "Clicks") -> str:
|
|
|
203
201
|
|
|
204
202
|
```html
|
|
205
203
|
<div>
|
|
206
|
-
<h3>
|
|
204
|
+
<h3>{{ label }}</h3>
|
|
207
205
|
<button onclick="setCount(count + 1)">
|
|
208
206
|
{count}
|
|
209
207
|
</button>
|
|
@@ -88,7 +88,7 @@ Interactive CRUD page:
|
|
|
88
88
|
|
|
89
89
|
| Runtime file | Primary responsibility | Read these docs |
|
|
90
90
|
| --- | --- | --- |
|
|
91
|
-
| [.venv/Lib/site-packages/casp/layout.py](../../../../.venv/Lib/site-packages/casp/layout.py) | `render_page(...)`, `render_layout(...)`, nested layout discovery, metadata merge,
|
|
91
|
+
| [.venv/Lib/site-packages/casp/layout.py](../../../../.venv/Lib/site-packages/casp/layout.py) | `render_page(...)`, `render_layout(...)`, nested layout discovery, metadata merge, sync or async `layout()` results, and parser-based `<slot />` replacement | [routing.md](./routing.md), [metadata.md](./metadata.md) |
|
|
92
92
|
| [.venv/Lib/site-packages/casp/auth.py](../../../../.venv/Lib/site-packages/casp/auth.py) | `AuthSettings`, route privacy checks, session payloads, OAuth providers, CSRF helper behavior, and redirect logic | [auth.md](./auth.md) |
|
|
93
93
|
| [.venv/Lib/site-packages/casp/runtime_security.py](../../../../.venv/Lib/site-packages/casp/runtime_security.py) | safe public-file serving, baseline non-CSP response headers, production-safe error messages, and production session-secret enforcement used by `main.py` | [project-structure.md](./project-structure.md), [auth.md](./auth.md) |
|
|
94
94
|
| [.venv/Lib/site-packages/casp/rpc.py](../../../../.venv/Lib/site-packages/casp/rpc.py) | `@rpc()` registration, rate limits, request handling, auth-aware action checks, and streamed RPC responses | [fetch-data.md](./fetch-data.md) |
|
|
@@ -101,9 +101,8 @@ Interactive CRUD page:
|
|
|
101
101
|
| [.venv/Lib/site-packages/casp/scripts_type.py](../../../../.venv/Lib/site-packages/casp/scripts_type.py) | rewriting authored body `<script>` tags to `type="text/pp"` in rendered HTML | [pulsepoint.md](./pulsepoint.md), [routing.md](./routing.md), [components.md](./components.md) |
|
|
102
102
|
| [.venv/Lib/site-packages/casp/html_attrs.py](../../../../.venv/Lib/site-packages/casp/html_attrs.py) | `get_attributes(...)`, alias normalization, and the Python-side `merge_classes(...)` contract | [components.md](./components.md) |
|
|
103
103
|
| [.venv/Lib/site-packages/casp/caspian_config.py](../../../../.venv/Lib/site-packages/casp/caspian_config.py) | typed config loading, feature-flag reads, `settings/files-list.json` parsing, and route rule derivation | [project-structure.md](./project-structure.md), [commands.md](./commands.md), [routing.md](./routing.md) |
|
|
104
|
-
| [.venv/Lib/site-packages/casp/syntax_compiler.py](../../../../.venv/Lib/site-packages/casp/syntax_compiler.py) | transpiling Caspian `<[[ ... ]]>` and `<template casp-*>` syntax before template render | [components.md](./components.md), [routing.md](./routing.md) |
|
|
105
104
|
|
|
106
|
-
Secondary helpers such as `html_native.py`, `loading.py`, and `string_helpers.py` support the modules above. Read
|
|
105
|
+
Secondary helpers such as `html_native.py`, `loading.py`, and `string_helpers.py` support the modules above. `html_native.py` owns BeautifulSoup-backed fragment parsing used by component/root transforms and layout slot replacement. Read these helpers only when a higher-level runtime file is still insufficient to explain the behavior you are tracing.
|
|
107
106
|
|
|
108
107
|
## Verification Focus
|
|
109
108
|
|
|
@@ -116,7 +115,7 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
|
|
|
116
115
|
| `casp.auth` | auth settings, signin and signout flow, provider wiring, and page protection behavior |
|
|
117
116
|
| `casp.rpc` and streamed RPC responses | middleware interception, CSRF and session expectations, registry behavior, and helper-level RPC contracts |
|
|
118
117
|
| `casp.layout` | layout discovery, metadata merge, root handling, and layout rendering rules |
|
|
119
|
-
| `casp.components_compiler`, `casp.component_decorator`, `casp.scripts_type`, and template-root injection | `@import` parsing, `x-*` expansion,
|
|
118
|
+
| `casp.components_compiler`, `casp.component_decorator`, `casp.scripts_type`, and template-root injection | `@import` parsing, `x-*` expansion, deterministic root keys, `pp-component` injection, and authored-script rewriting to `type="text/pp"` |
|
|
120
119
|
| `casp.state_manager` | wire vs non-wire reset behavior, request-state persistence assumptions, and AttributeDict access |
|
|
121
120
|
| `casp.cache_handler` | filename generation, manifest writes, TTL handling, and invalidation behavior |
|
|
122
121
|
| `casp.caspian_config` | config parsing, files index building, and Next.js-style route inventory behavior |
|
package/dist/docs/database.md
CHANGED
|
@@ -178,7 +178,7 @@ Prisma calls fit naturally in:
|
|
|
178
178
|
- `async def page()` for first-render data
|
|
179
179
|
- `@rpc()` actions for browser-triggered reads and writes
|
|
180
180
|
|
|
181
|
-
Keep Prisma I/O
|
|
181
|
+
Keep route-specific Prisma I/O in `page()` or `@rpc()` actions. The installed layout engine supports synchronous and async `layout()` results, but layout work should stay focused on shared subtree props or metadata.
|
|
182
182
|
|
|
183
183
|
See `fetch-data.md` for the recommended route-render versus RPC split.
|
|
184
184
|
|
|
@@ -352,5 +352,5 @@ If an AI agent is working on a Caspian app with Prisma enabled, apply these rule
|
|
|
352
352
|
- Reuse the existing `src/lib/prisma/` package when the Python app needs database access.
|
|
353
353
|
- For file managers and uploads, persist metadata in Prisma and keep blob storage separate. See [file-uploads.md](./file-uploads.md).
|
|
354
354
|
- Put reusable database helpers in `src/lib/`; keep route and RPC orchestration in `src/app/`.
|
|
355
|
-
- Use `async def page()` for first-render reads.
|
|
355
|
+
- Use `async def page()` for route-specific first-render reads. Use `layout()` only for shared subtree props or metadata, and use `@rpc()` plus `pp.rpc()` for browser-triggered reads and writes.
|
|
356
356
|
- Check `fetch-data.md` for route versus RPC guidance and `validation.md` before writing public mutations.
|
package/dist/docs/fetch-data.md
CHANGED
|
@@ -23,7 +23,7 @@ This page explains how data fetching works in Caspian. Use route functions for i
|
|
|
23
23
|
|
|
24
24
|
Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
|
|
25
25
|
|
|
26
|
-
Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`,
|
|
26
|
+
Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
|
|
27
27
|
|
|
28
28
|
MCP is a separate integration surface. Do not place app-owned FastMCP tools in route `index.py` files or treat `@rpc()` actions as a replacement for MCP tools. Use `mcp.md` and `src/lib/mcp/` only when `caspian.config.json` has `mcp: true`. If `mcp` is false, do not assume those files exist.
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ MCP is a separate integration surface. Do not place app-owned FastMCP tools in r
|
|
|
31
31
|
|
|
32
32
|
Caspian has two main data-loading paths:
|
|
33
33
|
|
|
34
|
-
- `page()` for initial-render data, plus `layout()` for
|
|
34
|
+
- `page()` for initial-render data, plus `layout()` for shared props or metadata during the render
|
|
35
35
|
- `@rpc()` plus `pp.rpc()` for interactive fetches after the page is already loaded
|
|
36
36
|
|
|
37
37
|
In practice, most pages use both:
|
|
@@ -46,10 +46,11 @@ When a page belongs to a grouped subtree such as a dashboard, account area, admi
|
|
|
46
46
|
|
|
47
47
|
## Default Data Rule
|
|
48
48
|
|
|
49
|
-
- Use `page()` for
|
|
49
|
+
- Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
|
|
50
50
|
- When a route renders UI and also needs backend work, keep the HTML in the sibling `index.html`; `index.py` should prepare data and call `render_page(__file__, ...)`, not inline the route markup.
|
|
51
51
|
- Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
|
|
52
52
|
- Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
|
|
53
|
+
- For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
|
|
53
54
|
- Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
|
|
54
55
|
|
|
55
56
|
## Initial Data In `index.py`
|
|
@@ -81,7 +82,7 @@ If a route's first-render HTML is public and stable enough to reuse across reque
|
|
|
81
82
|
Notes:
|
|
82
83
|
|
|
83
84
|
- Prefer `async def page()` when your database or API client is async-capable.
|
|
84
|
-
- Put shared section-level props in `layout.py` when multiple child routes need the same
|
|
85
|
+
- Put shared section-level props in `layout.py` when multiple child routes need the same payload. The current layout engine supports synchronous and async `layout()` results, but route-specific data should stay in `page()`.
|
|
85
86
|
- Use a normal parent folder such as `dashboard/` when the section name should appear in the URL. Use a route-group folder such as `(reports)/` only when the shared wrapper should organize child routes without adding a URL segment.
|
|
86
87
|
- Keep reusable database or API clients under `src/lib/`; keep route-specific orchestration in `src/app/`.
|
|
87
88
|
|
|
@@ -107,14 +108,18 @@ from src.lib.prisma import prisma
|
|
|
107
108
|
async def list_todos():
|
|
108
109
|
return await prisma.todo.find_many()
|
|
109
110
|
|
|
110
|
-
@rpc(require_auth=True, limits="20/minute")
|
|
111
|
-
async def create_todo(title: str):
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
@rpc(require_auth=True, limits="20/minute")
|
|
112
|
+
async def create_todo(title: str):
|
|
113
|
+
title = title.strip()
|
|
114
|
+
if not title:
|
|
115
|
+
raise ValueError("Title is required.")
|
|
116
|
+
|
|
117
|
+
return await prisma.todo.create(
|
|
118
|
+
data={"title": title, "completed": False}
|
|
119
|
+
)
|
|
115
120
|
```
|
|
116
121
|
|
|
117
|
-
Call it from the client with `pp.rpc()`:
|
|
122
|
+
Call it from the client with `pp.rpc()`:
|
|
118
123
|
|
|
119
124
|
```html
|
|
120
125
|
<script>
|
|
@@ -127,10 +132,37 @@ Call it from the client with `pp.rpc()`:
|
|
|
127
132
|
const todo = await pp.rpc("create_todo", { title });
|
|
128
133
|
console.log(todo);
|
|
129
134
|
}
|
|
130
|
-
</script>
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
</script>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<form onsubmit="{createTodoFromForm(event)}" novalidate>
|
|
142
|
+
<input name="title" required />
|
|
143
|
+
<button type="submit" disabled="{isSaving}">Add</button>
|
|
144
|
+
|
|
145
|
+
<script>
|
|
146
|
+
const [isSaving, setIsSaving] = pp.state(false);
|
|
147
|
+
|
|
148
|
+
async function createTodoFromForm(event) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
|
|
151
|
+
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
152
|
+
if (isSaving) return;
|
|
153
|
+
|
|
154
|
+
setIsSaving(true);
|
|
155
|
+
try {
|
|
156
|
+
await pp.rpc("create_todo", data, { abortPrevious: true });
|
|
157
|
+
} finally {
|
|
158
|
+
setIsSaving(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
</form>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use RPC for:
|
|
134
166
|
|
|
135
167
|
- CRUD reads and writes after the initial render
|
|
136
168
|
- Button-triggered refreshes
|
|
@@ -294,7 +326,7 @@ For initial route rendering with `render_page(...)`, prefer passing plain templa
|
|
|
294
326
|
|
|
295
327
|
## Recommended Decision Rule
|
|
296
328
|
|
|
297
|
-
Use `page()` when
|
|
329
|
+
Use `page()` when route-specific data is part of the page render. Use `layout()` for shared subtree props or metadata. Use `@rpc()` plus `pp.rpc()` when the browser needs to ask the server for more data after the page is already visible.
|
|
298
330
|
|
|
299
331
|
A common pattern is:
|
|
300
332
|
|
|
@@ -326,8 +358,8 @@ If the first-render HTML is expensive to produce and safe to share between visit
|
|
|
326
358
|
If an AI agent is choosing how to load data in Caspian, apply these rules first.
|
|
327
359
|
|
|
328
360
|
- Put first-render data loading in `src/app/**/index.py`.
|
|
329
|
-
- Put shared section props in `layout.py` only when multiple child routes need the same
|
|
330
|
-
- Keep async I/O in `page()` or `@rpc()`
|
|
361
|
+
- Put shared section props in `layout.py` only when multiple child routes need the same shared data.
|
|
362
|
+
- Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
|
|
331
363
|
- For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md) before deciding where `page()` data and `@rpc()` actions belong.
|
|
332
364
|
- Treat RPC as the default read and write layer between PulsePoint code and Python route logic, especially for CRUD and interactive backend reads.
|
|
333
365
|
- Use `@rpc()` for backend functions that should be callable from the browser.
|
|
@@ -33,7 +33,7 @@ Treat `caspian.config.json` and the actual project tree as the source of truth f
|
|
|
33
33
|
| `index.html` | Authored visible page template for a route | The route renders UI | `src/app/**`, `routing.md`, `pulsepoint.md` |
|
|
34
34
|
| `index.py` | Backend companion for route logic and metadata | The route needs `page()`, metadata, auth checks, redirects, caching, or route-owned `@rpc()` actions | `main.py`, `.venv/Lib/site-packages/casp/layout.py` |
|
|
35
35
|
| `layout.html` | Shared shell for a route subtree | Multiple child routes share wrapper markup | `.venv/Lib/site-packages/casp/layout.py`, `routing.md` |
|
|
36
|
-
| `layout.py` | Shared
|
|
36
|
+
| `layout.py` | Shared props and metadata defaults for a subtree | The shared shell needs Python-provided values or metadata | `.venv/Lib/site-packages/casp/layout.py`, `metadata.md` |
|
|
37
37
|
| `loading.html` | Route-scope loading UI used during SPA navigation | A section or page needs an immediate loading state before the next route finishes rendering | `.venv/Lib/site-packages/casp/loading.py`, `public/js/pp-reactive-v2.js` |
|
|
38
38
|
| `not-found.html` | Global 404 page | The app needs a branded fallback for unmatched URLs | `main.py` |
|
|
39
39
|
| `error.html` | Global 500 page | The app needs a safe fallback for unhandled exceptions | `main.py` |
|
|
@@ -129,9 +129,9 @@ When one page needs to influence a wrapping layout, `page()` can return `(page_h
|
|
|
129
129
|
Use it for:
|
|
130
130
|
|
|
131
131
|
- shared shell markup such as sidebars, headers, docs rails, or dashboard frames
|
|
132
|
-
- the
|
|
133
|
-
- shared layout props consumed as `
|
|
134
|
-
- shared metadata fields consumed as `
|
|
132
|
+
- the `<slot />` insertion point for child routes
|
|
133
|
+
- shared layout props consumed as `{{ layout.* }}`
|
|
134
|
+
- shared metadata fields consumed as `{{ metadata.* }}`
|
|
135
135
|
|
|
136
136
|
Example:
|
|
137
137
|
|
|
@@ -140,12 +140,14 @@ Example:
|
|
|
140
140
|
<aside class="docs-nav">Docs navigation</aside>
|
|
141
141
|
|
|
142
142
|
<main class="docs-content" pp-reset-scroll="true">
|
|
143
|
-
|
|
143
|
+
<slot />
|
|
144
144
|
</main>
|
|
145
145
|
</div>
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
Use nested `layout.html` files for sections like `dashboard/`, `docs/`, `account/`, or route groups such as `(marketing)/`.
|
|
148
|
+
Use nested `layout.html` files for sections like `dashboard/`, `docs/`, `account/`, or route groups such as `(marketing)/`.
|
|
149
|
+
|
|
150
|
+
The child outlet must be a real authored `<slot />` element in `layout.html`. The runtime parses layout HTML and replaces real slot elements during nested rendering; it does not invent an implicit slot from `layout.py` or replace escaped documentation text such as `<slot />`.
|
|
149
151
|
|
|
150
152
|
In grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane that should reset on child-route navigation. Leave persistent shell scrollers such as sidebars unmarked when they should keep their own scroll position.
|
|
151
153
|
|
|
@@ -155,7 +157,7 @@ In grouped shells with separate shell and content scrolling, put `pp-reset-scrol
|
|
|
155
157
|
|
|
156
158
|
Use it for:
|
|
157
159
|
|
|
158
|
-
- shared
|
|
160
|
+
- shared props
|
|
159
161
|
- metadata defaults for everything below that folder
|
|
160
162
|
- small shared layout decisions that belong to the subtree rather than one page
|
|
161
163
|
|
|
@@ -179,7 +181,7 @@ def layout():
|
|
|
179
181
|
}
|
|
180
182
|
```
|
|
181
183
|
|
|
182
|
-
Important runtime detail: `layout()`
|
|
184
|
+
Important runtime detail: `layout()` may be synchronous or async in the installed runtime. Keep async layout work focused on shared subtree props or metadata; put route-specific first-render data in `page()` and browser-triggered work in route-owned `@rpc()` actions.
|
|
183
185
|
|
|
184
186
|
## `loading.html`
|
|
185
187
|
|
|
@@ -199,7 +201,7 @@ Example layout shell:
|
|
|
199
201
|
|
|
200
202
|
```html
|
|
201
203
|
<main class="docs-content" pp-loading-content="true" pp-reset-scroll="true">
|
|
202
|
-
|
|
204
|
+
<slot />
|
|
203
205
|
</main>
|
|
204
206
|
```
|
|
205
207
|
|
package/dist/docs/metadata.md
CHANGED
|
@@ -12,7 +12,7 @@ related:
|
|
|
12
12
|
|
|
13
13
|
This page explains how Caspian handles document metadata, SEO fields, and social sharing tags.
|
|
14
14
|
|
|
15
|
-
At render time, Caspian resolves metadata through the layout engine and exposes the merged result to templates as `
|
|
15
|
+
At render time, Caspian resolves metadata through the layout engine and exposes the merged result to templates as `{{ metadata.* }}`.
|
|
16
16
|
|
|
17
17
|
## Source Of Truth
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ At render time, Caspian resolves metadata through the layout engine and exposes
|
|
|
20
20
|
- Route registration and runtime metadata collection are controlled by `main.py`.
|
|
21
21
|
- Shared metadata defaults belong in `src/app/**/layout.py`.
|
|
22
22
|
- Page-specific static or dynamic metadata belongs in `src/app/**/index.py`.
|
|
23
|
-
- Rendered metadata is consumed from layout templates through `
|
|
23
|
+
- Rendered metadata is consumed from layout templates through `{{ metadata.* }}`.
|
|
24
24
|
|
|
25
25
|
## Overview
|
|
26
26
|
|
|
@@ -168,10 +168,10 @@ Resolved metadata is passed into layout rendering as a `metadata` object.
|
|
|
168
168
|
Example:
|
|
169
169
|
|
|
170
170
|
```html
|
|
171
|
-
<title>
|
|
172
|
-
<meta name="description" content="
|
|
173
|
-
<meta property="og:image" content="
|
|
174
|
-
<meta name="twitter:card" content="
|
|
171
|
+
<title>{{ metadata.title }}</title>
|
|
172
|
+
<meta name="description" content="{{ metadata.description }}" />
|
|
173
|
+
<meta property="og:image" content="{{ metadata['og:image'] }}" />
|
|
174
|
+
<meta name="twitter:card" content="{{ metadata['twitter:card'] }}" />
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
Use bracket access for `extra` keys that contain characters such as `:`.
|
|
@@ -196,9 +196,9 @@ Use stable, publicly reachable image paths for social cards so crawlers can fetc
|
|
|
196
196
|
|
|
197
197
|
Keep visual layout data and SEO metadata separate.
|
|
198
198
|
|
|
199
|
-
- Values returned from `layout()` are exposed as `
|
|
200
|
-
- The second dict returned from `page()` as `(page_html, layout_props_dict)` is also exposed to wrapping layouts as `
|
|
201
|
-
- SEO values are exposed as `
|
|
199
|
+
- Values returned from `layout()` are exposed as `{{ layout.* }}`.
|
|
200
|
+
- The second dict returned from `page()` as `(page_html, layout_props_dict)` is also exposed to wrapping layouts as `{{ layout.* }}`.
|
|
201
|
+
- SEO values are exposed as `{{ metadata.* }}`.
|
|
202
202
|
- Do not return `title` or `description` from `layout()` expecting SEO changes.
|
|
203
203
|
- The layout engine explicitly strips `title` and `description` from layout props to avoid mixing visual props with metadata.
|
|
204
204
|
|
|
@@ -223,5 +223,5 @@ If an AI agent is deciding where to put SEO fields, apply these rules first.
|
|
|
223
223
|
- If a single route only needs to tweak a wrapping layout, return `(render_page(__file__, ...), {"dashboard_body_class": ...})` from `page()` instead of moving that prop into metadata.
|
|
224
224
|
- Use `extra` for Open Graph and Twitter card tags.
|
|
225
225
|
- Access `extra` values in templates with bracket syntax such as `metadata['og:image']`.
|
|
226
|
-
- Keep `layout()` return data in `
|
|
226
|
+
- Keep `layout()` return data in `{{ layout.* }}` and keep SEO fields in `Metadata(...)`.
|
|
227
227
|
- Check [routing.md](./routing.md) when deciding whether metadata belongs in a layout or a specific route folder.
|
|
@@ -106,7 +106,7 @@ This is the main application area. It contains route files, templates, styles, a
|
|
|
106
106
|
|
|
107
107
|
This directory handles file-based routing. Route templates and route-specific backend logic live here.
|
|
108
108
|
|
|
109
|
-
For any route that renders UI, keep that markup in `src/app/**/index.html`. If the route is UI-only, `index.html` alone is enough. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Keep shared wrappers in `layout.html` and use `layout.py` only for shared
|
|
109
|
+
For any route that renders UI, keep that markup in `src/app/**/index.html`. If the route is UI-only, `index.html` alone is enough. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Keep shared wrappers in `layout.html` and use `layout.py` only for shared props or metadata. Use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
|
|
110
110
|
|
|
111
111
|
When a folder represents a section with child routes, such as `dashboard`, `account`, `settings`, or `docs`, create `layout.html` in that folder and let the child routes live beneath it. See [routing.md](./routing.md) for the canonical section layout pattern.
|
|
112
112
|
|
|
@@ -261,7 +261,7 @@ Keep visible wrapper markup in `layout.html`, not in `layout.py`.
|
|
|
261
261
|
|
|
262
262
|
### `src/app/layout.py`
|
|
263
263
|
|
|
264
|
-
The backend companion for a layout. Use this file for shared
|
|
264
|
+
The backend companion for a layout. Use this file for shared props, metadata defaults, and other server-side preparation for the sibling `layout.html`.
|
|
265
265
|
|
|
266
266
|
Do not store layout HTML in `layout.py`. Keep the authored wrapper in `layout.html` and let `layout.py` return props or metadata.
|
|
267
267
|
|
|
@@ -1,132 +1,133 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: PulsePoint Runtime Map
|
|
3
|
-
description: Use this page when AI needs a fast feature-to-runtime lookup for PulsePoint behavior before editing `src/app/**`, component templates, or `public/js/pp-reactive-v2.js`.
|
|
4
|
-
related:
|
|
5
|
-
title: Related docs
|
|
6
|
-
description: Use the PulsePoint guide for authoring rules, the routing and component guides for template placement, and the core runtime map when Python-side transforms are involved.
|
|
7
|
-
links:
|
|
8
|
-
- /docs/pulsepoint
|
|
9
|
-
- /docs/routing
|
|
10
|
-
- /docs/components
|
|
11
|
-
- /docs/fetch-data
|
|
12
|
-
- /docs/core-runtime-map
|
|
13
|
-
- /docs/index
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
This page is the quick AI routing layer for PulsePoint core features.
|
|
17
|
-
|
|
18
|
-
Use [pulsepoint.md](./pulsepoint.md) for the full authoring contract. Use this file when you need to jump from a feature name, directive, or runtime symptom to the files and behavior checkpoints that matter.
|
|
19
|
-
|
|
20
|
-
## Source Of Truth
|
|
21
|
-
|
|
22
|
-
- `public/js/pp-reactive-v2.js` is the shipped browser runtime.
|
|
23
|
-
- `main.py` owns the final render pipeline that calls `transform_components(...)` and `transform_scripts(...)`.
|
|
24
|
-
- `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` after component expansion and validates the single-root contract.
|
|
25
|
-
- `.venv/Lib/site-packages/casp/scripts_type.py` rewrites authored body `<script>` tags to `type="text/pp"`.
|
|
26
|
-
- `.venv/Lib/site-packages/casp/html_attrs.py` owns Python-side class and attribute helpers such as `merge_classes(...)`.
|
|
27
|
-
|
|
28
|
-
If an inspected browser DOM disagrees with authored template source, remember that the runtime DOM includes framework-managed output. Do not copy runtime-only attributes back into authored templates.
|
|
29
|
-
|
|
30
|
-
## Feature Map
|
|
31
|
-
|
|
32
|
-
| PulsePoint feature | Authoring surface | Runtime owner | Verify before changing |
|
|
33
|
-
| --- | --- | --- | --- |
|
|
34
|
-
| Component roots | `src/app/**/index.html`, `layout.html`, component `.html` files | `components_compiler.py`, `pp-reactive-v2.js` | one authored root, final expanded root receives one `pp-component`, no sibling scripts |
|
|
35
|
-
| Component scripts | plain `<script>` inside the authored root | `scripts_type.py`, `pp-reactive-v2.js` | authored scripts are plain, runtime scripts become `type="text/pp"`, one owned script per root |
|
|
36
|
-
| Template expressions | text and attributes with `{...}` | `pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not assumed |
|
|
37
|
-
| State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
|
|
38
|
-
| Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
|
|
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(...)`, `<
|
|
41
|
-
| Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
|
|
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`; avoid id-driven `querySelector`/`addEventListener` for normal UI |
|
|
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 |
|
|
46
|
-
| Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
|
|
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
|
-
| Scroll restoration | `pp-reset-scroll="true"` | `pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
|
|
49
|
-
| Tailwind merge | `{twMerge(...)}`, Python `merge_classes(...)` | `html_attrs.py`, `pp-reactive-v2.js` | Python emits frontend-ready expressions when Tailwind is enabled |
|
|
50
|
-
|
|
51
|
-
## AI Decision Rules
|
|
52
|
-
|
|
53
|
-
- Read `caspian.config.json` first when the task depends on Tailwind, generated files, or optional project features.
|
|
54
|
-
- Read [routing.md](./routing.md) before adding route or layout templates.
|
|
55
|
-
- Read [components.md](./components.md) before adding reusable Python components or `x-*` imports.
|
|
56
|
-
- Read [fetch-data.md](./fetch-data.md) before adding browser-triggered backend work.
|
|
57
|
-
- Use this map when the task names a PulsePoint feature and you need the owning runtime file quickly.
|
|
58
|
-
- Verify implemented behavior in `public/js/pp-reactive-v2.js` before adding new PulsePoint API claims.
|
|
59
|
-
- If an interaction is normal first-party HTML behavior, route it through PulsePoint before considering standard DOM scripting.
|
|
60
|
-
|
|
61
|
-
## Copy-Safe Authoring Rules
|
|
62
|
-
|
|
63
|
-
- Author one root element or one imported `x-*` root per route, layout, or component template.
|
|
64
|
-
- Keep any owned plain `<script>` inside that same root.
|
|
65
|
-
- Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
|
|
1
|
+
---
|
|
2
|
+
title: PulsePoint Runtime Map
|
|
3
|
+
description: Use this page when AI needs a fast feature-to-runtime lookup for PulsePoint behavior before editing `src/app/**`, component templates, or `public/js/pp-reactive-v2.js`.
|
|
4
|
+
related:
|
|
5
|
+
title: Related docs
|
|
6
|
+
description: Use the PulsePoint guide for authoring rules, the routing and component guides for template placement, and the core runtime map when Python-side transforms are involved.
|
|
7
|
+
links:
|
|
8
|
+
- /docs/pulsepoint
|
|
9
|
+
- /docs/routing
|
|
10
|
+
- /docs/components
|
|
11
|
+
- /docs/fetch-data
|
|
12
|
+
- /docs/core-runtime-map
|
|
13
|
+
- /docs/index
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
This page is the quick AI routing layer for PulsePoint core features.
|
|
17
|
+
|
|
18
|
+
Use [pulsepoint.md](./pulsepoint.md) for the full authoring contract. Use this file when you need to jump from a feature name, directive, or runtime symptom to the files and behavior checkpoints that matter.
|
|
19
|
+
|
|
20
|
+
## Source Of Truth
|
|
21
|
+
|
|
22
|
+
- `public/js/pp-reactive-v2.js` is the shipped browser runtime.
|
|
23
|
+
- `main.py` owns the final render pipeline that calls `transform_components(...)` and `transform_scripts(...)`.
|
|
24
|
+
- `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` after component expansion and validates the single-root contract.
|
|
25
|
+
- `.venv/Lib/site-packages/casp/scripts_type.py` rewrites authored body `<script>` tags to `type="text/pp"`.
|
|
26
|
+
- `.venv/Lib/site-packages/casp/html_attrs.py` owns Python-side class and attribute helpers such as `merge_classes(...)`.
|
|
27
|
+
|
|
28
|
+
If an inspected browser DOM disagrees with authored template source, remember that the runtime DOM includes framework-managed output. Do not copy runtime-only attributes back into authored templates.
|
|
29
|
+
|
|
30
|
+
## Feature Map
|
|
31
|
+
|
|
32
|
+
| PulsePoint feature | Authoring surface | Runtime owner | Verify before changing |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| Component roots | `src/app/**/index.html`, `layout.html`, component `.html` files | `components_compiler.py`, `pp-reactive-v2.js` | one authored root, final expanded root receives one `pp-component`, no sibling scripts |
|
|
35
|
+
| Component scripts | plain `<script>` inside the authored root | `scripts_type.py`, `pp-reactive-v2.js` | authored scripts are plain, runtime scripts become `type="text/pp"`, one owned script per root |
|
|
36
|
+
| Template expressions | text and attributes with `{...}` | `pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not assumed |
|
|
37
|
+
| State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
|
|
38
|
+
| Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
|
|
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` |
|
|
41
|
+
| Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
|
|
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 |
|
|
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 |
|
|
46
|
+
| Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
|
|
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
|
+
| Scroll restoration | `pp-reset-scroll="true"` | `pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
|
|
49
|
+
| Tailwind merge | `{twMerge(...)}`, Python `merge_classes(...)` | `html_attrs.py`, `pp-reactive-v2.js` | Python emits frontend-ready expressions when Tailwind is enabled |
|
|
50
|
+
|
|
51
|
+
## AI Decision Rules
|
|
52
|
+
|
|
53
|
+
- Read `caspian.config.json` first when the task depends on Tailwind, generated files, or optional project features.
|
|
54
|
+
- Read [routing.md](./routing.md) before adding route or layout templates.
|
|
55
|
+
- Read [components.md](./components.md) before adding reusable Python components or `x-*` imports.
|
|
56
|
+
- Read [fetch-data.md](./fetch-data.md) before adding browser-triggered backend work.
|
|
57
|
+
- Use this map when the task names a PulsePoint feature and you need the owning runtime file quickly.
|
|
58
|
+
- Verify implemented behavior in `public/js/pp-reactive-v2.js` before adding new PulsePoint API claims.
|
|
59
|
+
- If an interaction is normal first-party HTML behavior, route it through PulsePoint before considering standard DOM scripting.
|
|
60
|
+
|
|
61
|
+
## Copy-Safe Authoring Rules
|
|
62
|
+
|
|
63
|
+
- Author one root element or one imported `x-*` root per route, layout, or component template.
|
|
64
|
+
- Keep any owned plain `<script>` inside that same root.
|
|
65
|
+
- Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
|
|
66
66
|
- Use `pp.rpc(...)` for current browser-to-server calls.
|
|
67
67
|
- Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
|
|
68
|
-
-
|
|
69
|
-
- Use
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
- Use `pp-for` only on `<template>` and plain `key` for keyed lists.
|
|
71
|
+
- Prefer PulsePoint state and directives over manual `innerHTML` repainting.
|
|
72
|
+
- Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
|
|
73
|
+
|
|
74
|
+
## Compact Examples
|
|
75
|
+
|
|
75
76
|
Context provider:
|
|
76
77
|
|
|
77
78
|
```html
|
|
78
79
|
<section>
|
|
79
80
|
<script>
|
|
80
81
|
const ThemeContext = pp.createContext("light");
|
|
81
|
-
const [theme, setTheme] = pp.state("dark");
|
|
82
|
-
</script>
|
|
83
|
-
|
|
84
|
-
<
|
|
82
|
+
const [theme, setTheme] = pp.state("dark");
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<themecontext.provider value="{theme}">
|
|
85
86
|
<button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
|
|
86
87
|
Theme: {theme}
|
|
87
88
|
</button>
|
|
88
|
-
</
|
|
89
|
+
</themecontext.provider>
|
|
89
90
|
</section>
|
|
90
91
|
```
|
|
91
|
-
|
|
92
|
-
Upload progress:
|
|
93
|
-
|
|
94
|
-
```html
|
|
95
|
-
<section>
|
|
96
|
-
<input type="file" onchange="{uploadFile(event.target.files?.[0])}" />
|
|
97
|
-
<p>{progress}%</p>
|
|
98
|
-
|
|
99
|
-
<script>
|
|
100
|
-
const [progress, setProgress] = pp.state(0);
|
|
101
|
-
|
|
102
|
-
async function uploadFile(file) {
|
|
103
|
-
if (!file) return;
|
|
104
|
-
|
|
105
|
-
await pp.rpc("upload_asset", { file }, {
|
|
106
|
-
onUploadProgress: (event) => setProgress(event.percentage ?? 0),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
</section>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Grouped shell scroll reset:
|
|
114
|
-
|
|
115
|
-
```html
|
|
116
|
-
<section class="dashboard-shell">
|
|
92
|
+
|
|
93
|
+
Upload progress:
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<section>
|
|
97
|
+
<input type="file" onchange="{uploadFile(event.target.files?.[0])}" />
|
|
98
|
+
<p>{progress}%</p>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
const [progress, setProgress] = pp.state(0);
|
|
102
|
+
|
|
103
|
+
async function uploadFile(file) {
|
|
104
|
+
if (!file) return;
|
|
105
|
+
|
|
106
|
+
await pp.rpc("upload_asset", { file }, {
|
|
107
|
+
onUploadProgress: (event) => setProgress(event.percentage ?? 0),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
</section>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Grouped shell scroll reset:
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<section class="dashboard-shell">
|
|
117
118
|
<aside class="dashboard-sidebar">...</aside>
|
|
118
119
|
<main class="dashboard-content" pp-reset-scroll="true">
|
|
119
|
-
|
|
120
|
+
<slot />
|
|
120
121
|
</main>
|
|
121
122
|
</section>
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Verification Prompts
|
|
125
|
-
|
|
126
|
-
Use these prompts after docs or runtime changes to confirm AI can route correctly:
|
|
127
|
-
|
|
128
|
-
- "Create an interactive filter in a Caspian route template."
|
|
129
|
-
- "Explain why authored scripts are plain `<script>` but browser output shows `type=\"text/pp\"`."
|
|
130
|
-
- "Debug a dashboard sidebar losing scroll during child-route navigation."
|
|
131
|
-
- "Add an upload widget with progress and a reactive file list."
|
|
132
|
-
- "Use context to share theme state between parent and child components."
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Verification Prompts
|
|
126
|
+
|
|
127
|
+
Use these prompts after docs or runtime changes to confirm AI can route correctly:
|
|
128
|
+
|
|
129
|
+
- "Create an interactive filter in a Caspian route template."
|
|
130
|
+
- "Explain why authored scripts are plain `<script>` but browser output shows `type=\"text/pp\"`."
|
|
131
|
+
- "Debug a dashboard sidebar losing scroll during child-route navigation."
|
|
132
|
+
- "Add an upload widget with progress and a reactive file list."
|
|
133
|
+
- "Use context to share theme state between parent and child components."
|
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -55,6 +55,7 @@ When a Caspian page needs reactive browser behavior, use PulsePoint.
|
|
|
55
55
|
- Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
|
|
56
56
|
- Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
|
|
57
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.
|
|
58
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.
|
|
59
60
|
- Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
|
|
60
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.
|
|
@@ -69,9 +70,10 @@ 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.
|
|
70
71
|
- Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
|
|
71
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.
|
|
72
74
|
- Keep reactive values in `pp.state(...)`.
|
|
73
75
|
- Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
|
|
74
|
-
- Use `pp.ref(...)` and `pp-ref` when a real element reference is needed.
|
|
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.
|
|
75
77
|
- Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
|
|
76
78
|
- Use `pp.rpc(...)` for browser-triggered backend reads and writes.
|
|
77
79
|
|
|
@@ -80,6 +82,7 @@ Avoid building a parallel JavaScript layer for normal UI behavior:
|
|
|
80
82
|
- Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
|
|
81
83
|
- Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
|
|
82
84
|
- Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
|
|
85
|
+
- Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
|
|
83
86
|
- Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
|
|
84
87
|
- Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
|
|
85
88
|
|
|
@@ -134,6 +137,75 @@ Avoid this first-party pattern:
|
|
|
134
137
|
```
|
|
135
138
|
|
|
136
139
|
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.
|
|
140
|
+
|
|
141
|
+
Preferred form submit pattern:
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<section>
|
|
145
|
+
<form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
|
|
146
|
+
<input name="name" value="{{ user_name }}" autocomplete="name" required />
|
|
147
|
+
<input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
|
|
148
|
+
<input name="password" type="password" autocomplete="new-password" />
|
|
149
|
+
|
|
150
|
+
<button type="submit" disabled="{isSubmitting}">
|
|
151
|
+
{isSubmitting ? "Saving..." : "Save changes"}
|
|
152
|
+
</button>
|
|
153
|
+
</form>
|
|
154
|
+
|
|
155
|
+
<script>
|
|
156
|
+
const [isSubmitting, setIsSubmitting] = pp.state(false);
|
|
157
|
+
|
|
158
|
+
async function saveProfile(event) {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
if (isSubmitting) return;
|
|
161
|
+
|
|
162
|
+
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
163
|
+
|
|
164
|
+
setIsSubmitting(true);
|
|
165
|
+
try {
|
|
166
|
+
await pp.rpc("save_profile", data, { abortPrevious: true });
|
|
167
|
+
} finally {
|
|
168
|
+
setIsSubmitting(false);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
</section>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Avoid this for a normal form:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<section>
|
|
179
|
+
<form pp-ref="{formRef}">
|
|
180
|
+
<input pp-ref="{nameRef}" name="name" />
|
|
181
|
+
<input pp-ref="{emailRef}" name="email" />
|
|
182
|
+
</form>
|
|
183
|
+
|
|
184
|
+
<script>
|
|
185
|
+
const formRef = pp.ref(null);
|
|
186
|
+
const nameRef = pp.ref(null);
|
|
187
|
+
const emailRef = pp.ref(null);
|
|
188
|
+
|
|
189
|
+
pp.effect(() => {
|
|
190
|
+
const form = formRef.current;
|
|
191
|
+
if (!form) return;
|
|
192
|
+
|
|
193
|
+
const submit = (event) => {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
return pp.rpc("save_profile", {
|
|
196
|
+
name: nameRef.current?.value ?? "",
|
|
197
|
+
email: emailRef.current?.value ?? "",
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
form.addEventListener("submit", submit);
|
|
202
|
+
return () => form.removeEventListener("submit", submit);
|
|
203
|
+
}, []);
|
|
204
|
+
</script>
|
|
205
|
+
</section>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
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.
|
|
137
209
|
|
|
138
210
|
## Authoring Model
|
|
139
211
|
|
|
@@ -296,12 +368,12 @@ Notes:
|
|
|
296
368
|
|
|
297
369
|
## Context
|
|
298
370
|
|
|
299
|
-
Context is implemented in the current runtime
|
|
371
|
+
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`.
|
|
300
372
|
|
|
301
373
|
How it works:
|
|
302
374
|
|
|
303
375
|
- Create a token with `pp.createContext(defaultValue)`.
|
|
304
|
-
- Provide a value with
|
|
376
|
+
- Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
|
|
305
377
|
- Read it in a descendant component with `pp.context(token)`.
|
|
306
378
|
- Resolution walks the logical component parent chain stored in the registry, not the live DOM.
|
|
307
379
|
- Portaled descendants still resolve providers through component ancestry.
|
|
@@ -315,23 +387,24 @@ Important:
|
|
|
315
387
|
- Context is component-level, not directive-based. There is no `pp-context` attribute.
|
|
316
388
|
- `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
|
|
317
389
|
- If provider and consumer live in different component script scopes, pass the token through props or store it in shared outer or global state.
|
|
318
|
-
- The preferred authoring style is a provider tag in the template. The runtime
|
|
319
|
-
-
|
|
390
|
+
- 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`.
|
|
391
|
+
- 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.
|
|
392
|
+
- Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
|
|
320
393
|
|
|
321
394
|
Provider example:
|
|
322
395
|
|
|
323
396
|
```html
|
|
324
397
|
<section>
|
|
325
|
-
<script>
|
|
326
|
-
const ThemeContext = pp.createContext("light");
|
|
327
|
-
const [theme] = pp.state("dark");
|
|
328
|
-
</script>
|
|
329
|
-
|
|
330
|
-
<
|
|
331
|
-
<p>This subtree receives the provided theme.</p>
|
|
332
|
-
</
|
|
333
|
-
</section>
|
|
334
|
-
```
|
|
398
|
+
<script>
|
|
399
|
+
const ThemeContext = pp.createContext("light");
|
|
400
|
+
const [theme] = pp.state("dark");
|
|
401
|
+
</script>
|
|
402
|
+
|
|
403
|
+
<themecontext.provider value="{theme}">
|
|
404
|
+
<p>This subtree receives the provided theme.</p>
|
|
405
|
+
</themecontext.provider>
|
|
406
|
+
</section>
|
|
407
|
+
```
|
|
335
408
|
|
|
336
409
|
Consumer example for a child component that receives the token through props:
|
|
337
410
|
|
|
@@ -401,9 +474,10 @@ Example:
|
|
|
401
474
|
</div>
|
|
402
475
|
```
|
|
403
476
|
|
|
404
|
-
## Refs
|
|
405
|
-
|
|
406
|
-
-
|
|
477
|
+
## Refs
|
|
478
|
+
|
|
479
|
+
- 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())`.
|
|
480
|
+
- Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
|
|
407
481
|
- Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
|
|
408
482
|
- `pp.ref(null)` is the normal way to create a ref object.
|
|
409
483
|
- Callback refs and `{ current }` refs are both supported.
|
|
@@ -469,6 +543,8 @@ Example:
|
|
|
469
543
|
- Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
|
|
470
544
|
- Event values may be raw code or wrapped in `{...}`.
|
|
471
545
|
- The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
|
|
546
|
+
- For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
|
|
547
|
+
- `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.
|
|
472
548
|
- Do not use hyphenated event attrs like `on-click`.
|
|
473
549
|
- Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
|
|
474
550
|
- Owned template/event-owner internals are runtime-managed. Do not author them directly.
|
|
@@ -528,6 +604,7 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
528
604
|
|
|
529
605
|
- Treat PulsePoint as the default reactive frontend for Caspian app code.
|
|
530
606
|
- For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
|
|
607
|
+
- 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.
|
|
531
608
|
- Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
|
|
532
609
|
- Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
|
|
533
610
|
- Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
|
|
@@ -540,7 +617,7 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
540
617
|
- If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
|
|
541
618
|
- In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
|
|
542
619
|
- Keep template-facing variables at top level.
|
|
543
|
-
- Follow the
|
|
620
|
+
- Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
|
|
544
621
|
- Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
|
|
545
622
|
- Keep `pp-for` on `<template>` and use plain `key`.
|
|
546
623
|
- Use native `on*` attributes, not framework-specific event syntax.
|
|
@@ -581,7 +658,7 @@ These are current runtime caveats that matter for authors and AI tools:
|
|
|
581
658
|
- The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
|
|
582
659
|
- `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
|
|
583
660
|
- `pp.context()` resolves through ancestor components, not the current component's own pending providers.
|
|
584
|
-
- `pp.provideContext` is not part of the current runtime API. Use
|
|
661
|
+
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
585
662
|
- `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
|
|
586
663
|
|
|
587
664
|
## Final reminder
|
package/dist/docs/routing.md
CHANGED
|
@@ -35,8 +35,8 @@ Start with these rules:
|
|
|
35
35
|
- 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.
|
|
36
36
|
- Use `layout.py` when a layout needs shared props or metadata before rendering. The `layout()` function may be synchronous or async.
|
|
37
37
|
- 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.
|
|
38
|
-
- 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
|
-
- 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.
|
|
38
|
+
- 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
|
+
- 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.
|
|
40
40
|
|
|
41
41
|
## Hard Template Invariant
|
|
42
42
|
|
|
@@ -182,11 +182,11 @@ Route templates can import reusable Python components with `<!-- @import ... -->
|
|
|
182
182
|
|
|
183
183
|
Place those import comments at the top of the file, above the authored root element. They are file-level directives, not children of the route root.
|
|
184
184
|
|
|
185
|
-
Route templates follow the same authored-vs-runtime contract documented in [pulsepoint.md](./pulsepoint.md) and the same single-root discipline documented in [components.md](./components.md): keep one authored parent node, keep any `<!-- @import ... -->` directives above that root, use a plain `<script>` inside that root when needed, and do not handwrite `pp-component` or `type="text/pp"`. That root may be a native HTML element or a single imported `x-*` component tag, but after expansion it must resolve to one final HTML root.
|
|
186
|
-
|
|
187
|
-
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(...)`.
|
|
188
|
-
|
|
189
|
-
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.
|
|
185
|
+
Route templates follow the same authored-vs-runtime contract documented in [pulsepoint.md](./pulsepoint.md) and the same single-root discipline documented in [components.md](./components.md): keep one authored parent node, keep any `<!-- @import ... -->` directives above that root, use a plain `<script>` inside that root when needed, and do not handwrite `pp-component` or `type="text/pp"`. That root may be a native HTML element or a single imported `x-*` component tag, but after expansion it must resolve to one final HTML root.
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
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.
|
|
190
190
|
|
|
191
191
|
Good:
|
|
192
192
|
|
|
@@ -260,16 +260,16 @@ Use this pattern when the route needs to fetch data, compute metadata, or do oth
|
|
|
260
260
|
`page()` may also return a 2-item tuple: `(page_html, layout_props_dict)`.
|
|
261
261
|
|
|
262
262
|
- The first item is the rendered page HTML, usually `render_page(__file__, page_context)`.
|
|
263
|
-
- The second item must be a dict. Its keys are merged into the wrapping layout context and become available to parent layouts as `
|
|
263
|
+
- The second item must be a dict. Its keys are merged into the wrapping layout context and become available to parent layouts as `{{ layout.* }}`.
|
|
264
264
|
|
|
265
265
|
Use that tuple form when one route needs to influence a wrapper without turning that value into a section-wide default. A common example is a dashboard page that needs to lock the root body with `overflow-hidden` while the rest of the app keeps normal scrolling.
|
|
266
266
|
|
|
267
267
|
Example root layout:
|
|
268
268
|
|
|
269
269
|
```html
|
|
270
|
-
<body class="
|
|
271
|
-
|
|
272
|
-
</body>
|
|
270
|
+
<body class="{{ layout.dashboard_body_class | default('') }}">
|
|
271
|
+
<slot />
|
|
272
|
+
</body>
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
Example route:
|
|
@@ -284,7 +284,7 @@ async def page():
|
|
|
284
284
|
)
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
The key name is arbitrary, but it must match exactly between the dict returned from `page()` and the `
|
|
287
|
+
The key name is arbitrary, but it must match exactly between the dict returned from `page()` and the `{{ layout.some_key }}` lookup in `layout.html`.
|
|
288
288
|
|
|
289
289
|
Use distinct names for those layout props. In the current router, the second dict is merged into the full layout context after path params and `request`, so a key such as `slug` or `request` can shadow an existing value.
|
|
290
290
|
|
|
@@ -366,15 +366,17 @@ In practice, this means a dashboard is usually a folder-level layout, not a sing
|
|
|
366
366
|
|
|
367
367
|
When a layout imports components, keep each `<!-- @import ... -->` comment above the layout's authored wrapper element, such as the root `<section>` in a nested layout or the root `<html>` in the app layout.
|
|
368
368
|
|
|
369
|
-
Resolved SEO fields are exposed to layouts as `
|
|
369
|
+
Resolved SEO fields are exposed to layouts as `{{ metadata.* }}`, while values returned from `layout.py` are exposed separately as `{{ layout.* }}`.
|
|
370
370
|
|
|
371
371
|
### `layout.html`
|
|
372
372
|
|
|
373
373
|
Use `layout.html` for the shared wrapper markup of a subtree. Keep the visible shell here, not in `layout.py`.
|
|
374
374
|
|
|
375
|
-
Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, plain `<script>` inside the root when needed, and no handwritten `pp-component` or `type="text/pp"`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, plain `<script>` inside the root when needed, and no handwritten `pp-component` or `type="text/pp"`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
|
|
376
|
+
|
|
377
|
+
Place child routes with a plain HTML `<slot />` tag. Caspian replaces that layout slot with the current child route or nested layout while rendering.
|
|
378
|
+
|
|
379
|
+
For example, a page inside `/dashboard/settings` is wrapped by the root layout first and then by the dashboard layout.
|
|
378
380
|
|
|
379
381
|
Example root layout:
|
|
380
382
|
|
|
@@ -382,19 +384,19 @@ Example root layout:
|
|
|
382
384
|
<!DOCTYPE html>
|
|
383
385
|
<html>
|
|
384
386
|
<head>
|
|
385
|
-
<title>
|
|
386
|
-
<meta name="description" content="
|
|
387
|
+
<title>{{ metadata.title }}</title>
|
|
388
|
+
<meta name="description" content="{{ metadata.description }}" />
|
|
387
389
|
</head>
|
|
388
|
-
<body>
|
|
389
|
-
<NavBar />
|
|
390
|
-
|
|
391
|
-
</body>
|
|
392
|
-
</html>
|
|
390
|
+
<body>
|
|
391
|
+
<NavBar />
|
|
392
|
+
<slot />
|
|
393
|
+
</body>
|
|
394
|
+
</html>
|
|
393
395
|
```
|
|
394
396
|
|
|
395
397
|
### `layout.py`
|
|
396
398
|
|
|
397
|
-
If a layout needs shared
|
|
399
|
+
If a layout needs shared props or metadata, add a `layout.py` file next to the HTML layout. Treat it as the backend companion for the layout, not as the place to author visible wrapper markup.
|
|
398
400
|
|
|
399
401
|
When `layout()` calls `render_layout(__file__, ...)`, root-validation errors are attributed to the sibling `layout.html` because that file is the authored template. If `layout()` returns a raw HTML string directly, Caspian treats that value as a runtime fragment instead of an authored template and wraps it in a runtime host root when needed.
|
|
400
402
|
|
|
@@ -415,16 +417,16 @@ def layout(context_data):
|
|
|
415
417
|
|
|
416
418
|
`context_data` includes URL parameters such as dynamic route values.
|
|
417
419
|
|
|
418
|
-
In the common case, return a dict and let the sibling `layout.html` read those values through `
|
|
420
|
+
In the common case, return a dict and let the sibling `layout.html` read those values through `{{ layout.* }}`.
|
|
419
421
|
|
|
420
422
|
`layout()` currently supports these result shapes:
|
|
421
423
|
|
|
422
|
-
- `dict`: load the sibling `layout.html` and expose the dict as `
|
|
424
|
+
- `dict`: load the sibling `layout.html` and expose the dict as `{{ layout.* }}`
|
|
423
425
|
- `str`: use that string as the layout content
|
|
424
|
-
- `(layout_html, props_dict)`: use the first item as the layout content and expose the second dict as `
|
|
426
|
+
- `(layout_html, props_dict)`: use the first item as the layout content and expose the second dict as `{{ layout.* }}`
|
|
425
427
|
- `None`: fall back to the sibling `layout.html` with no extra layout props
|
|
426
428
|
|
|
427
|
-
If you intentionally want to render `layout.html` immediately with direct local variables instead of the `layout.*` namespace, call `render_layout(__file__, {...})` and reference those keys directly in the template.
|
|
429
|
+
If you intentionally want to render `layout.html` immediately with direct local variables instead of the `layout.*` namespace, call `render_layout(__file__, {...})` and reference those keys directly in the template. `render_layout(...)` does not create a hidden child outlet; the layout template must still author the real `<slot />` element where child routes should render.
|
|
428
430
|
|
|
429
431
|
Example:
|
|
430
432
|
|
|
@@ -435,9 +437,9 @@ def layout():
|
|
|
435
437
|
return render_layout(__file__, {"my_class": "size-8"})
|
|
436
438
|
```
|
|
437
439
|
|
|
438
|
-
In that pattern, the matching `layout.html` reads `
|
|
440
|
+
In that pattern, the matching `layout.html` reads `{{ my_class }}`, not `{{ layout.my_class }}`, because the template string was already rendered before the nested layout pipeline continues.
|
|
439
441
|
|
|
440
|
-
If you need both a custom layout string and standard `
|
|
442
|
+
If you need both a custom layout string and standard `{{ layout.* }}` props, return a tuple:
|
|
441
443
|
|
|
442
444
|
```python
|
|
443
445
|
from casp.layout import render_layout
|
|
@@ -449,7 +451,7 @@ def layout():
|
|
|
449
451
|
)
|
|
450
452
|
```
|
|
451
453
|
|
|
452
|
-
`layout()`
|
|
454
|
+
`layout()` may be synchronous or async in the installed runtime. Keep async layout work focused on shared subtree props or metadata; use `page()` or `@rpc()` when the work belongs to one route or a browser-triggered user action.
|
|
453
455
|
|
|
454
456
|
Use [metadata.md](./metadata.md) when a layout also needs SEO defaults. Return dictionaries from `layout()` for visual or template props, and use `Metadata(...)` for title, description, and social tags.
|
|
455
457
|
|
|
@@ -504,15 +506,15 @@ If an AI agent is choosing where to add or update route code, apply these rules
|
|
|
504
506
|
- Use folder names to model URL segments.
|
|
505
507
|
- If a route renders UI, create or update `index.html` for the markup.
|
|
506
508
|
- Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
|
|
507
|
-
- Keep visible page markup in `index.html` and shared subtree shells in `layout.html`; do not place route HTML in `index.py` or layout HTML in `layout.py`.
|
|
508
|
-
- Use PulsePoint as the first-party interaction model for route and layout HTML. Avoid custom DOM wiring for normal events and reactivity.
|
|
509
|
-
- When the user asks for a dashboard, admin area, account section, or any grouped subtree of child routes, create a parent folder with `layout.html` and place the child routes beneath it. Follow the same mental model as the Next.js App Router.
|
|
509
|
+
- Keep visible page markup in `index.html` and shared subtree shells in `layout.html`; do not place route HTML in `index.py` or layout HTML in `layout.py`.
|
|
510
|
+
- Use PulsePoint as the first-party interaction model for route and layout HTML. Avoid custom DOM wiring for normal events and reactivity.
|
|
511
|
+
- When the user asks for a dashboard, admin area, account section, or any grouped subtree of child routes, create a parent folder with `layout.html` and place the child routes beneath it. Follow the same mental model as the Next.js App Router.
|
|
510
512
|
- Use a normal folder such as `dashboard/` when the segment should appear in the URL. Use `(group)/` only when the folder should organize or wrap child routes without adding a path segment.
|
|
511
513
|
- Use [cache.md](./cache.md) when an `index.py` route should opt into page-level HTML caching.
|
|
512
|
-
- Use `layout.html` for shared wrappers and `layout.py` for layout-level
|
|
514
|
+
- Use `layout.html` for shared wrappers and `layout.py` for layout-level props or metadata.
|
|
513
515
|
- For grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane instead of the whole shell when only the page content should reset between child routes.
|
|
514
|
-
- When one route needs to change a parent layout, return `(render_page(__file__, ...), {"dashboard_body_class": ...})` from `page()` and read that value as `
|
|
515
|
-
- Use `layout.py` for layout props that should apply across an entire subtree. Use `render_layout(__file__, {...})` only when the layout should consume direct local variables such as `
|
|
516
|
+
- When one route needs to change a parent layout, return `(render_page(__file__, ...), {"dashboard_body_class": ...})` from `page()` and read that value as `{{ layout.dashboard_body_class }}` in the wrapping `layout.html`.
|
|
517
|
+
- Use `layout.py` for layout props that should apply across an entire subtree. Use `render_layout(__file__, {...})` only when the layout should consume direct local variables such as `{{ my_class }}` instead of the standard `{{ layout.* }}` namespace.
|
|
516
518
|
- Keep `<!-- @import ... -->` directives at the top of `index.html` and `layout.html`, above the single authored parent node.
|
|
517
519
|
- Use [metadata.md](./metadata.md) when a route or layout needs SEO fields.
|
|
518
520
|
- Use `[segment]` for single dynamic parameters.
|