caspian-utils 0.0.19 → 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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Components
3
- description: Use this page when the task mentions `@component`, reusable UI, HTML-first `x-*` component tags, component imports, same-name `.html` templates, or where shared components belong in a Caspian project.
3
+ description: Use this page when the task mentions `@component`, reusable UI, HTML-first `x-*` component tags, component imports, same-name `.html` templates, `merge_classes(...)`, `twMerge(...)`, or where shared components belong in a Caspian project.
4
4
  related:
5
5
  title: Related docs
6
6
  description: Use the structure guide for file placement, the routing guide for route templates, the PulsePoint guide for browser-side scripts, and the data guide for component-owned RPC flows.
@@ -31,7 +31,7 @@ As the app grows, treat `src/components/` as the default home for reusable appli
31
31
 
32
32
  ## Basic Component
33
33
 
34
- This is the simplest pattern: accept props, merge classes or attributes, and return HTML.
34
+ This is the simplest pattern: accept props, assemble the final class value, and return HTML.
35
35
 
36
36
  ```python
37
37
  from casp.component_decorator import component
@@ -53,7 +53,44 @@ Notes:
53
53
 
54
54
  - `children` receives the inner content passed between opening and closing tags.
55
55
  - `**props` lets the component accept additional HTML attributes such as `id`, `data-*`, and `aria-*`.
56
- - The runtime normalizes `class`, `className`, and `class_name` into a merged `class` value before the component is called.
56
+ - The runtime normalizes `class`, `className`, and `class_name` into one `class` value before the component is called.
57
+ - Pass the `merge_classes(...)` result straight into `get_attributes(...)` or the rendered `class` attribute; do not wrap it in another helper.
58
+
59
+ ## Tailwind Merge Contract
60
+
61
+ When `caspian.config.json` has `tailwindcss: true`, Caspian uses a frontend-first Tailwind merge contract.
62
+
63
+ - In Python components, use `merge_classes(...)` to assemble class defaults plus incoming class props.
64
+ - `merge_classes(...)` emits a frontend-ready `{twMerge(...)}` expression instead of doing Python-side Tailwind conflict resolution.
65
+ - In authored PulsePoint markup and scripts, use global `twMerge(...)` directly for attribute expressions and script-local derived values.
66
+
67
+ Python example:
68
+
69
+ ```python
70
+ from casp.component_decorator import component
71
+ from casp.html_attrs import get_attributes, merge_classes
72
+
73
+ @component
74
+ def IconButton(**props) -> str:
75
+ incoming_class = props.pop("class", "")
76
+ final_class = merge_classes("size-4 rounded-md", incoming_class)
77
+
78
+ attributes = get_attributes({
79
+ "class": final_class,
80
+ }, props)
81
+
82
+ return f'<button {attributes}></button>'
83
+ ```
84
+
85
+ Authored PulsePoint examples:
86
+
87
+ ```html
88
+ <p class="{twMerge(baseClass, inputClass)}">Merged badge preview</p>
89
+
90
+ <script>
91
+ const merged = twMerge(baseClass, inputClass);
92
+ </script>
93
+ ```
57
94
 
58
95
  ## Import And Use Components
59
96
 
@@ -26,6 +26,7 @@ Before making feature, tooling, or file-placement decisions in a Caspian project
26
26
  When generating or editing a Caspian app, treat these as the default choices unless the task explicitly requires something else:
27
27
 
28
28
  - Use PulsePoint for reactive frontend behavior.
29
+ - When `caspian.config.json` has `tailwindcss: true`, use Python `merge_classes(...)` plus browser `twMerge(...)` as the only supported Tailwind class-merging path.
29
30
  - Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
30
31
  - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
31
32
 
@@ -42,8 +43,8 @@ The packaged Caspian docs referenced by this index live here:
42
43
  - `mcp.md` - MCP-specific layout, launch flow, and AI routing rules for projects where `caspian.config.json` enables MCP
43
44
  - `database.md` - Prisma schema, migration, seed, and client-generation workflow for projects where `caspian.config.json` enables Prisma, plus Python-side helper caveats
44
45
  - `auth.md` - Session-backed authentication with `casp.auth`, centralized `auth_config.py`, public-vs-private route mode guidance, RPC-first signout guidance, RBAC, and OAuth provider helpers
45
- - `components.md` - Create reusable Python components, template-backed UI, HTML-first `x-*` component tags, and the single-parent root rule for component HTML files
46
- - `pulsepoint.md` - Default reactive frontend runtime contract for component scripts, state, effects, directives, and client-side behaviors
46
+ - `components.md` - Create reusable Python components, template-backed UI, HTML-first `x-*` component tags, the single-parent root rule for component HTML files, and the Python-side `merge_classes(...)` contract when Tailwind CSS is enabled
47
+ - `pulsepoint.md` - Default reactive frontend runtime contract for component scripts, state, effects, directives, client-side behaviors, and direct browser `twMerge(...)` usage when Tailwind CSS is enabled
47
48
  - `fetch-data.md` - Initial server-side data loading and browser-triggered RPC flows with `pp.rpc()`, streaming, uploads, and auth-aware actions
48
49
  - `file-uploads.md` - Route-local file uploads and file-manager flows with `@rpc()`, `pp.rpc()`, Prisma metadata, public asset storage, and BrowserSync ignore rules
49
50
  - `state.md` - Request-scoped server state with `StateManager`, session-backed JSON persistence, and listener callbacks for transient flows
@@ -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.19",
3
+ "version": "0.0.21",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {