caspian-utils 0.0.20 → 0.0.21

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.
@@ -176,6 +176,7 @@ Use stable, publicly reachable image paths for social cards so crawlers can fetc
176
176
  Keep visual layout data and SEO metadata separate.
177
177
 
178
178
  - Values returned from `layout()` are exposed as `[[ layout.* ]]`.
179
+ - The second dict returned from `page()` as `(page_html, layout_props_dict)` is also exposed to wrapping layouts as `[[ layout.* ]]`.
179
180
  - SEO values are exposed as `[[ metadata.* ]]`.
180
181
  - Do not return `title` or `description` from `layout()` expecting SEO changes.
181
182
  - The layout engine explicitly strips `title` and `description` from layout props to avoid mixing visual props with metadata.
@@ -195,6 +196,7 @@ If an AI agent is deciding where to put SEO fields, apply these rules first.
195
196
  - Prefer module-level `metadata = Metadata(...)` for static routes.
196
197
  - Instantiate `Metadata(...)` inside `page()` when metadata depends on params, fetched records, or generated content.
197
198
  - Put shared defaults in `layout.py` and let leaf pages override only what they need.
199
+ - 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.
198
200
  - Use `extra` for Open Graph and Twitter card tags.
199
201
  - Access `extra` values in templates with bracket syntax such as `metadata['og:image']`.
200
202
  - Keep `layout()` return data in `[[ layout.* ]]` and keep SEO fields in `Metadata(...)`.
@@ -186,6 +186,37 @@ async def page():
186
186
 
187
187
  Use this pattern when the route needs to fetch data, compute metadata, or do other non-blocking server work before rendering the sibling `index.html`.
188
188
 
189
+ `page()` may also return a 2-item tuple: `(page_html, layout_props_dict)`.
190
+
191
+ - The first item is the rendered page HTML, usually `render_page(__file__, page_context)`.
192
+ - The second item must be a dict. Its keys are merged into the wrapping layout context and become available to parent layouts as `[[ layout.* ]]`.
193
+
194
+ 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.
195
+
196
+ Example root layout:
197
+
198
+ ```html
199
+ <body class="[[ layout.dashboard_body_class | default('') ]]">
200
+ [[ children | safe ]]
201
+ </body>
202
+ ```
203
+
204
+ Example route:
205
+
206
+ ```python
207
+ from casp.layout import render_page
208
+
209
+ async def page():
210
+ return (
211
+ render_page(__file__),
212
+ {"dashboard_body_class": "w-screen h-screen overflow-hidden"},
213
+ )
214
+ ```
215
+
216
+ 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`.
217
+
218
+ 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.
219
+
189
220
  When a route owns a file manager or upload UI, keep the owning upload and delete `@rpc()` actions in that same `index.py` and move reusable filesystem or Prisma helpers into `src/lib/`. Do not move ordinary upload behavior into `main.py`. See [file-uploads.md](./file-uploads.md).
190
221
 
191
222
  For static and dynamic metadata rules, inheritance order, and social card fields, see [metadata.md](./metadata.md).
@@ -289,12 +320,47 @@ def layout(context_data):
289
320
 
290
321
  return {
291
322
  "user": user,
323
+ "dashboard_body_class": "w-screen h-screen overflow-hidden",
292
324
  "theme": "dark",
293
325
  }
294
326
  ```
295
327
 
296
328
  `context_data` includes URL parameters such as dynamic route values.
297
329
 
330
+ In the common case, return a dict and let the sibling `layout.html` read those values through `[[ layout.* ]]`.
331
+
332
+ `layout()` currently supports these result shapes:
333
+
334
+ - `dict`: load the sibling `layout.html` and expose the dict as `[[ layout.* ]]`
335
+ - `str`: use that string as the layout content
336
+ - `(layout_html, props_dict)`: use the first item as the layout content and expose the second dict as `[[ layout.* ]]`
337
+ - `None`: fall back to the sibling `layout.html` with no extra layout props
338
+
339
+ 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.
340
+
341
+ Example:
342
+
343
+ ```python
344
+ from casp.layout import render_layout
345
+
346
+ def layout():
347
+ return render_layout(__file__, {"my_class": "size-8"})
348
+ ```
349
+
350
+ 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.
351
+
352
+ If you need both a custom layout string and standard `[[ layout.* ]]` props, return a tuple:
353
+
354
+ ```python
355
+ from casp.layout import render_layout
356
+
357
+ def layout():
358
+ return (
359
+ render_layout(__file__),
360
+ {"dashboard_body_class": "w-screen h-screen overflow-hidden"},
361
+ )
362
+ ```
363
+
298
364
  `layout()` currently runs synchronously in `casp.layout`. If you need async I/O, load it in `page()` instead of `layout.py`.
299
365
 
300
366
  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.
@@ -344,6 +410,8 @@ If an AI agent is choosing where to add or update route code, apply these rules
344
410
  - Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
345
411
  - Use [cache.md](./cache.md) when an `index.py` route should opt into page-level HTML caching.
346
412
  - Use `layout.html` for shared wrappers and `layout.py` for layout-level synchronous props or metadata.
413
+ - 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`.
414
+ - 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.
347
415
  - Keep `<!-- @import ... -->` directives at the top of `index.html` and `layout.html`, above the single authored root element.
348
416
  - Use [metadata.md](./metadata.md) when a route or layout needs SEO fields.
349
417
  - 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.20",
3
+ "version": "0.0.21",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {