caspian-utils 0.0.34 → 0.1.0

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/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 in `src/lib/auth/auth_config.py`
26
- - framework runtime behavior in `.venv/Lib/site-packages/casp/auth.py`
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 configuration
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 config belongs in `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 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 configuration.
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
- # IMPORTANT: current casp.auth expects PATH -> [ROLES]
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
- - You do not need to modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
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
- ## Backwards Compatibility Alias
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.
@@ -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>[[ label ]]</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, and the synchronous `layout()` contract | [routing.md](./routing.md), [metadata.md](./metadata.md) |
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 them only when a higher-level runtime file is still insufficient to explain the behavior you are tracing.
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, syntax transpilation, deterministic root keys, `pp-component` injection, and authored-script rewriting to `type="text/pp"` |
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 |
@@ -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 out of `layout.py` in the current runtime because `casp.layout` calls `layout()` synchronously.
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. Keep `layout()` synchronous in the current runtime, and use `@rpc()` plus `pp.rpc()` for browser-triggered reads and writes.
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.
@@ -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 synchronous shared props or metadata during the render
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,7 +46,7 @@ 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 async or route-level data required before HTML renders, and use `layout()` only for synchronous shared props or metadata.
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.
@@ -81,7 +81,7 @@ If a route's first-render HTML is public and stable enough to reuse across reque
81
81
  Notes:
82
82
 
83
83
  - 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 synchronous payload. The current layout engine does not await `layout()`.
84
+ - 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
85
  - 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
86
  - Keep reusable database or API clients under `src/lib/`; keep route-specific orchestration in `src/app/`.
87
87
 
@@ -294,7 +294,7 @@ For initial route rendering with `render_page(...)`, prefer passing plain templa
294
294
 
295
295
  ## Recommended Decision Rule
296
296
 
297
- Use `page()` when async or route-specific data is part of the page render. Use `layout()` for synchronous shared 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.
297
+ 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
298
 
299
299
  A common pattern is:
300
300
 
@@ -326,8 +326,8 @@ If the first-render HTML is expensive to produce and safe to share between visit
326
326
  If an AI agent is choosing how to load data in Caspian, apply these rules first.
327
327
 
328
328
  - 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 synchronous data.
330
- - Keep async I/O in `page()` or `@rpc()` because the current layout engine does not await `layout()`.
329
+ - Put shared section props in `layout.py` only when multiple child routes need the same shared data.
330
+ - Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
331
331
  - For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md) before deciding where `page()` data and `@rpc()` actions belong.
332
332
  - Treat RPC as the default read and write layer between PulsePoint code and Python route logic, especially for CRUD and interactive backend reads.
333
333
  - 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 synchronous 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` |
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 `[[children|safe]]` insertion point for child routes
133
- - shared layout props consumed as `[[ layout.* ]]`
134
- - shared metadata fields consumed as `[[ metadata.* ]]`
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
- [[children|safe]]
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 `&lt;slot /&gt;`.
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 synchronous props
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()` is synchronous in the installed runtime. Put async I/O in route `page()` functions or route-owned `@rpc()` actions instead of awaiting inside `layout.py`.
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
- [[children|safe]]
204
+ <slot />
203
205
  </main>
204
206
  ```
205
207
 
@@ -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 `[[ metadata.* ]]`.
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 `[[ metadata.* ]]`.
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>[[ 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'] ]]" />
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 `[[ 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.* ]]`.
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 `[[ layout.* ]]` and keep SEO fields in `Metadata(...)`.
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 synchronous props or metadata. Use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
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 synchronous props, metadata defaults, and other server-side preparation for the sibling `layout.html`.
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,132 @@
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(...)`, `<Context.Provider>`, `pp.context(token)` | `pp-reactive-v2.js` | 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`; 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
- - 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
- - Use `Context.Provider` and `pp.context(...)` for context.
69
- - Use `pp-for` only on `<template>` and plain `key` for keyed lists.
70
- - Prefer PulsePoint state and directives over manual `innerHTML` repainting.
71
- - Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
72
-
73
- ## Compact Examples
74
-
75
- Context provider:
76
-
77
- ```html
78
- <section>
79
- <script>
80
- const ThemeContext = pp.createContext("light");
81
- const [theme, setTheme] = pp.state("dark");
82
- </script>
83
-
84
- <ThemeContext.Provider value="{theme}">
85
- <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
86
- Theme: {theme}
87
- </button>
88
- </ThemeContext.Provider>
89
- </section>
90
- ```
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">
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(...)`, `<Context.Provider>`, `pp.context(token)` | `pp-reactive-v2.js` | 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`; 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
+ - 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
+ - Use `Context.Provider` and `pp.context(...)` for context.
69
+ - Use `pp-for` only on `<template>` and plain `key` for keyed lists.
70
+ - Prefer PulsePoint state and directives over manual `innerHTML` repainting.
71
+ - Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
72
+
73
+ ## Compact Examples
74
+
75
+ Context provider:
76
+
77
+ ```html
78
+ <section>
79
+ <script>
80
+ const ThemeContext = pp.createContext("light");
81
+ const [theme, setTheme] = pp.state("dark");
82
+ </script>
83
+
84
+ <ThemeContext.Provider value="{theme}">
85
+ <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
86
+ Theme: {theme}
87
+ </button>
88
+ </ThemeContext.Provider>
89
+ </section>
90
+ ```
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">
117
117
  <aside class="dashboard-sidebar">...</aside>
118
118
  <main class="dashboard-content" pp-reset-scroll="true">
119
- [[ children | safe ]]
119
+ <slot />
120
120
  </main>
121
121
  </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."
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."
@@ -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(...)`.
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 `[[ layout.* ]]`.
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="[[ layout.dashboard_body_class | default('') ]]">
271
- [[ children | safe ]]
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 `[[ layout.some_key ]]` lookup in `layout.html`.
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 `[[ metadata.* ]]`, while values returned from `layout.py` are exposed separately as `[[ layout.* ]]`.
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
- For example, a page inside `/dashboard/settings` is wrapped by the root layout first and then by the dashboard layout.
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>[[ metadata.title ]]</title>
386
- <meta name="description" content="[[ metadata.description ]]" />
387
+ <title>{{ metadata.title }}</title>
388
+ <meta name="description" content="{{ metadata.description }}" />
387
389
  </head>
388
- <body>
389
- <NavBar />
390
- [[ children | safe ]]
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 synchronous 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.
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 `[[ layout.* ]]`.
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 `[[ layout.* ]]`
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 `[[ layout.* ]]`
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 `[[ my_class ]]`, not `[[ layout.my_class ]]`, because the template string was already rendered before the nested layout pipeline continues.
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 `[[ layout.* ]]` props, return a tuple:
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()` currently runs synchronously in `casp.layout`. If you need async I/O, load it in `page()` instead of `layout.py`.
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 synchronous props or metadata.
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 `[[ layout.dashboard_body_class ]]` in the wrapping `layout.html`.
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 `[[ my_class ]]` instead of the standard `[[ layout.* ]]` namespace.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.0.34",
3
+ "version": "0.1.0",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {