caspian-utils 0.1.17 → 0.1.18

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, `merge_classes(...)`, `twMerge(...)`, 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, forwarding Python component props to `pp.props`, `get_attributes(...)`, `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.
@@ -257,8 +257,110 @@ def UserCard(user, **props):
257
257
  """, attrs=attrs, user=user)
258
258
  ```
259
259
 
260
- The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and `transform_scripts(...)` rewrites the plain `<script>` to `type="text/pp"`. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
261
-
260
+ The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and `transform_scripts(...)` rewrites the plain `<script>` to `type="text/pp"`. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
261
+
262
+ ### Receiving Props In A Python Component
263
+
264
+ There are two separate prop handoffs in a single-file Python component, and the Python component is the bridge between them:
265
+
266
+ 1. Caspian converts attributes on the parent-authored `x-*` tag from kebab-case to camelCase and calls the Python component with them as string keyword arguments. PulsePoint expressions are not evaluated at this stage: `open="{permOpen}"` arrives in Python as the literal string `"{permOpen}"`, and `on-apply="{applyPermissions}"` arrives as `onApply="{applyPermissions}"`.
267
+ 2. The Python component must deliberately re-emit the props it wants the browser component to receive as attributes on its rendered root. Build those attributes with `get_attributes({...}, props)`, place `{{ attributes }}` on the single native root, and pass `attributes=attributes` to `html(...)`.
268
+ 3. PulsePoint derives `pp.props` from that rendered root. It evaluates pure `{expression}` attribute values in the parent component's scope, then exposes kebab-case root attribute names as camelCase keys such as `on-apply` -> `pp.props.onApply`.
269
+
270
+ Python function parameters do not automatically become root attributes or browser props. A parameter that is accepted but not included in `get_attributes(...)` is server-only and is discarded when the Python call returns.
271
+
272
+ Minimal end-to-end example:
273
+
274
+ Parent template:
275
+
276
+ ```html
277
+ <!-- @import { UserPermissionsDialog } from "../../components/UserPermissionsDialog.py" -->
278
+
279
+ <div>
280
+ <button onclick="setPermOpen(true)">Edit permissions</button>
281
+ <x-user-permissions-dialog
282
+ open="{permOpen}"
283
+ value="{permValue}"
284
+ on-open-change="{setPermOpen}"
285
+ on-apply="{applyPermissions}"
286
+ />
287
+
288
+ <script>
289
+ const [permOpen, setPermOpen] = pp.state(false);
290
+ const [permValue, setPermValue] = pp.state([]);
291
+
292
+ function applyPermissions(nextValue) {
293
+ setPermValue(nextValue);
294
+ setPermOpen(false);
295
+ }
296
+ </script>
297
+ </div>
298
+ ```
299
+
300
+ `UserPermissionsDialog.py`:
301
+
302
+ ```python
303
+ from casp.component_decorator import component, html
304
+ from casp.html_attrs import get_attributes, merge_classes
305
+
306
+ @component
307
+ def UserPermissionsDialog(
308
+ open=None,
309
+ value=None,
310
+ onOpenChange=None,
311
+ onApply=None,
312
+ **props,
313
+ ):
314
+ incoming_class = props.pop("class", "")
315
+ attributes = get_attributes({
316
+ "class": merge_classes("permissions-dialog", incoming_class),
317
+ "open": open,
318
+ "value": value,
319
+ "onOpenChange": onOpenChange,
320
+ "onApply": onApply,
321
+ }, props)
322
+
323
+ # html
324
+ return html("""
325
+ <section {{ attributes }} hidden="{!open}">
326
+ <p>Selected permissions: {value.length}</p>
327
+ <button onclick="onOpenChange(false)">Cancel</button>
328
+ <button onclick="onApply(value)">Apply</button>
329
+
330
+ <script>
331
+ const { open, value, onOpenChange, onApply } = pp.props;
332
+ </script>
333
+ </section>
334
+ """, attributes=attributes)
335
+ ```
336
+
337
+ The browser can evaluate `permOpen`, `permValue`, `setPermOpen`, and `applyPermissions` because their literal brace expressions survived the Python render and were placed on the child root. If `{{ attributes }}` or `attributes=attributes` is missing, `pp.props` has none of those forwarded keys and may be completely empty.
338
+
339
+ Pitfalls:
340
+
341
+ - **Silent empty `pp.props`:** accepting `open`, `value`, or callback parameters in Python without re-emitting them on the root raises no server error and no browser warning. The component renders, but those keys are absent from `pp.props` and values such as `pp.props.open` are `undefined`.
342
+ - **Reserved/native attribute collisions:** forwarded props are real DOM attributes. A prop named `title` produces the native `title="..."` tooltip on the root. Prefer a component-specific non-native name such as `user-name` (Python `userName`, browser `pp.props.userName`) when native behavior is not intended.
343
+ - `get_attributes(...)` omits empty strings. To pass a reactive boolean, prefer a pure expression such as `disabled="{isDisabled}"` rather than relying on a bare empty attribute to survive the Python bridge.
344
+
345
+ ### HTML Attribute Helper Contract
346
+
347
+ `casp.html_attrs.get_attributes(defaults, overrides=None)` builds one Jinja-safe attribute string for a component root:
348
+
349
+ - It processes the first dictionary, then the optional second dictionary. A non-empty value in `overrides` replaces the same normalized key from `defaults`. An omitted/empty override does not delete an already-renderable default. This is why the usual component pattern passes authored defaults first and remaining `**props` second.
350
+ - It resolves Python-safe aliases before normalization: `class_name` and `className` become `class`, `html_for` and `htmlFor` become `for`, `defaultValue` becomes `defaultvalue`, and `defaultChecked` becomes `defaultchecked`.
351
+ - It converts every other camelCase key to kebab-case, so `onOpenChange` renders as `on-open-change` and later becomes `pp.props.onOpenChange` in PulsePoint.
352
+ - It omits keys whose value is `None`, `False`, an empty string, or an empty/falsy list, tuple, or set. `True` renders as the explicit string `"true"`; non-empty iterables are space-joined.
353
+ - It HTML-escapes attribute values and returns `Markup`, so `{{ attributes }}` is not double-escaped by Jinja.
354
+ - Passing `**props` as the second dictionary is the passthrough contract for unconsumed `id`, `data-*`, `aria-*`, reactive expressions, callbacks, and other boundary attributes. Pop or otherwise consume a prop first when it should not be forwarded or when it has already been merged into a default.
355
+
356
+ `casp.html_attrs.merge_classes(*classes)` flattens truthy strings and truthy items from lists, tuples, or sets:
357
+
358
+ - When `caspian.config.json` has `tailwindcss: false`, it joins the class parts with spaces.
359
+ - When `tailwindcss: true`, it emits a PulsePoint expression such as `{twMerge("base classes", incomingClass)}` so the browser's global `twMerge(...)` resolves Tailwind conflicts after parent-scope prop evaluation.
360
+ - An incoming pure `{expression}` becomes an expression argument rather than a quoted string. An existing `{twMerge(...)}` expression is preserved or incorporated without nesting another `twMerge(...)` call.
361
+ - Empty inputs produce an empty string, which `get_attributes(...)` omits.
362
+ - When combining a default class with incoming `**props`, remove the incoming `class` from `props` before passing `props` as overrides; otherwise that later override replaces the merged class attribute.
363
+
262
364
  Rules for inline `html(...)`:
263
365
 
264
366
  - The single-root rule still applies: exactly one top-level element with any `<script>` nested inside it.
@@ -567,9 +567,11 @@ Consumer example for a child component that receives the token through props:
567
567
 
568
568
  When a child component needs the same token object, pass it from the provider scope as a prop such as `theme-token="{ThemeContext}"`.
569
569
 
570
- ## Props and nested components
571
-
572
- - Child component props are derived from DOM attributes.
570
+ ## Props and nested components
571
+
572
+ In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
573
+
574
+ - Child component props are derived from DOM attributes.
573
575
  - Attribute names are converted from kebab-case to camelCase for the prop bag.
574
576
  - Native `on*` attributes and `pp-component` are not included in props.
575
577
  - Empty attributes become boolean `true` props.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {