caspian-utils 0.1.16 → 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.
@@ -418,10 +418,10 @@ Global helpers exposed through the `pp` singleton and also merged into the compo
418
418
  Notes:
419
419
 
420
420
  - The global `pp` singleton auto-mounts once the runtime is loaded and the DOM is ready. Manual `pp.mount()` is still safe because it short-circuits after the first mount.
421
- - `pp.state` setters accept either a value or an updater function.
422
- - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. They may return only a synchronous cleanup function; returning a promise warns and is ignored, so start async work inside the effect instead.
423
- - A function used through `pp-ref` may return a synchronous cleanup function. PulsePoint runs it when that callback ref is replaced or detached; callbacks that do not return cleanup retain the legacy `callback(null)` detach behavior.
424
- - `pp.portal(ref)` defaults to `document.body` when no target is provided.
421
+ - `pp.state` setters accept either a value or an updater function.
422
+ - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. They may return only a synchronous cleanup function; returning a promise warns and is ignored, so start async work inside the effect instead.
423
+ - A function used through `pp-ref` may return a synchronous cleanup function. PulsePoint runs it when that callback ref is replaced or detached; callbacks that do not return cleanup retain the legacy `callback(null)` detach behavior.
424
+ - `pp.portal(ref)` defaults to `document.body` when no target is provided.
425
425
  - Older docs may call the RPC helper `pp.fetchFunction()`. In the current bundled runtime the implemented global API is `pp.rpc()`.
426
426
  - Keep template-facing bindings at the top level so the AST-based exporter can see them.
427
427
  - For predictable code generation, prefer passing an explicit dependency array to `pp.effect`, `pp.layoutEffect`, `pp.memo`, and `pp.callback`.
@@ -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.
@@ -586,22 +588,23 @@ Nested components:
586
588
  - Nested roots that contain their own `script` block are masked during parent template compilation.
587
589
  - Scriptless nested component roots are not fully masked during parent template compilation in the current source. Avoid generating child-local interpolations inside a nested root unless that child has its own `script` block.
588
590
 
589
- ## Template expressions and attributes
590
-
591
- - Use `{expression}` in text nodes and attribute values.
592
- - Follow HTML-first attribute naming. Native event attributes are lowercase DOM event attributes (`onclick`, `oninput`, `onsubmit`). Every other attribute on a component boundary is a prop: kebab-case names become camel-cased only inside `pp.props` (`selected-value` becomes `selectedValue`, `open-change` becomes `openChange`, and `on-open-change` becomes `onOpenChange`). A function prop does not need an `on-` prefix; that prefix is only an API naming convention. Use `on-click` when a component intentionally exposes an `onClick` prop, because lowercase `onclick` is reserved for the native DOM event on the boundary element.
593
- - Pure bindings like `value="{count}"` are evaluated as expressions.
591
+ ## Template expressions and attributes
592
+
593
+ - Use `{expression}` in text nodes and attribute values.
594
+ - Follow HTML-first attribute naming. Native event attributes are lowercase DOM event attributes (`onclick`, `oninput`, `onsubmit`). Every other attribute on a component boundary is a prop: kebab-case names become camel-cased only inside `pp.props` (`selected-value` becomes `selectedValue`, `open-change` becomes `openChange`, and `on-open-change` becomes `onOpenChange`). A function prop does not need an `on-` prefix; that prefix is only an API naming convention. Use `on-click` when a component intentionally exposes an `onClick` prop, because lowercase `onclick` is reserved for the native DOM event on the boundary element.
595
+ - Pure bindings like `value="{count}"` are evaluated as expressions.
594
596
  - For dynamic inline styles in authored templates, prefer `pp-style="{styleText}"` over `style="{styleText}"` so HTML/CSS tooling does not parse the brace expression as raw CSS.
595
597
  - `pp-style` is an authoring alias. The compiler rewrites it to a native `style` attribute in rendered output.
596
598
  - If the same element already has a static `style` attribute, `pp-style` is merged into that `style` value during compilation.
597
599
  - Mixed text like `class="card {isActive ? 'active' : ''}"` is supported.
598
- - Arrays in template expressions are joined without commas.
599
- - `null`, `undefined`, and boolean expression results render as an empty string in text output.
600
- - Plain objects, functions, and symbols are invalid template children; the runtime warns and omits them instead of rendering accidental strings such as `[object Object]`.
600
+ - Arrays in template expressions are joined without commas.
601
+ - `null`, `undefined`, and boolean expression results render as an empty string in text output.
602
+ - Plain objects, functions, and symbols are invalid template children; the runtime warns and omits them instead of rendering accidental strings such as `[object Object]`.
601
603
  - Supported boolean attributes are normalized so truthy values emit the bare attribute and falsy values remove it.
604
+ - Boolean expressions bound to string-valued attributes such as `aria-pressed`, `aria-expanded`, and `data-*` serialize as the literal strings `"true"` and `"false"`, including on nested component boundaries.
602
605
  - `<textarea value="{draft}"></textarea>` is normalized into textarea content.
603
606
  - Use `pp-spread="{...attrs}"` to spread an object expression into attributes.
604
- - `pp-spread` omits nullish values, omits known HTML boolean attributes when their value is `false`, emits them bare when `true`, preserves string-valued `aria-*`/`data-*` booleans, and escapes `&`, `"`, and `<` in emitted attribute values.
607
+ - `pp-spread` omits nullish values, omits known HTML boolean attributes when their value is `false`, emits them bare when `true`, preserves string-valued `aria-*`/`data-*` booleans, and escapes `&`, `"`, and `<` in emitted attribute values.
605
608
  - Use plain `key` for keyed diffing. `pp-key` is not implemented.
606
609
 
607
610
  Example:
@@ -632,23 +635,23 @@ The render pipeline wraps each top-level `pp-component` root in an inert `<templ
632
635
  - Do not add per-tag workarounds to dodge first-paint validation (static-path `hidden` toggles instead of binding `d`, `data-*` URL holders, `hidden`-gated `<img src>`, or an SSR-resolved initial value). The deferral removes the whole class at once.
633
636
  - `pp-style` and the `<input>`/`<select>`/`checked`/`defaultvalue`/`<textarea>` value rewrites still apply — but for different reasons that deferral does not replace: authoring-source tooling (`style="{...}"` breaks HTML/CSS linters) and attribute-vs-property correctness for controlled form fields.
634
637
 
635
- ## Refs
636
-
637
- - 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())`.
638
- - `pp-ref` works on native elements and on `x-*` component tags. On a component tag it resolves in the parent's scope and binds to the component's concrete root DOM element, so `<x-input pp-ref="{nameInput}" />` exposes the rendered `<input>` without a wrapper or `querySelector(...)`.
639
- - Use the bare-name form, such as `pp-ref="nameInput"`, when the ref object or callback is already available under that name in scope. The runtime resolves this form by scope lookup.
640
- - Use the brace-expression form, such as `pp-ref="{nameInput}"`, `pp-ref="{registerRef(id)}"`, or `pp-ref="{el => setNode(el)}"`, when the compiler should capture an expression. This is the preferred general form for component tags and dynamic or callback expressions.
641
- - `pp.ref(null)` is the normal way to create a ref object.
642
- - Callback refs and `{ current }` refs are both supported.
643
- - Captured brace-form refs are compiled into an internal `data-pp-ref` token and rebound after render.
644
- - Component refs are attached before layout effects and passive effects run, and detach with `null` (or a callback's returned synchronous cleanup) when the owning element unmounts.
645
- - A composition component whose root is another `x-*` component forwards the ref through Caspian's layout-neutral component hosts to the eventual concrete DOM root.
646
- - Plain `pp-ref` bindings are preserved across rerenders, including no-op rerenders that skip DOM diffing.
638
+ ## Refs
639
+
640
+ - 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())`.
641
+ - `pp-ref` works on native elements and on `x-*` component tags. On a component tag it resolves in the parent's scope and binds to the component's concrete root DOM element, so `<x-input pp-ref="{nameInput}" />` exposes the rendered `<input>` without a wrapper or `querySelector(...)`.
642
+ - Use the bare-name form, such as `pp-ref="nameInput"`, when the ref object or callback is already available under that name in scope. The runtime resolves this form by scope lookup.
643
+ - Use the brace-expression form, such as `pp-ref="{nameInput}"`, `pp-ref="{registerRef(id)}"`, or `pp-ref="{el => setNode(el)}"`, when the compiler should capture an expression. This is the preferred general form for component tags and dynamic or callback expressions.
644
+ - `pp.ref(null)` is the normal way to create a ref object.
645
+ - Callback refs and `{ current }` refs are both supported.
646
+ - Captured brace-form refs are compiled into an internal `data-pp-ref` token and rebound after render.
647
+ - Component refs are attached before layout effects and passive effects run, and detach with `null` (or a callback's returned synchronous cleanup) when the owning element unmounts.
648
+ - A composition component whose root is another `x-*` component forwards the ref through Caspian's layout-neutral component hosts to the eventual concrete DOM root.
649
+ - Plain `pp-ref` bindings are preserved across rerenders, including no-op rerenders that skip DOM diffing.
647
650
  - Ref callbacks may be called with `null` during cleanup.
648
651
  - The runtime generates `data-pp-ref` internally. Do not author it.
649
652
  - Do not author `pp-event-owner`, `pp-owner`, or `pp-dynamic-*` attributes by hand.
650
653
 
651
- Example:
654
+ Example:
652
655
 
653
656
  ```html
654
657
  <div>
@@ -658,29 +661,29 @@ Example:
658
661
  <script>
659
662
  const nameInput = pp.ref(null);
660
663
  </script>
661
- </div>
662
- ```
663
-
664
- Component example:
665
-
666
- ```html
667
- <div>
668
- <x-input pp-ref="{nameInput}" name="name" />
669
- <button onclick="nameInput.current?.focus()">Focus</button>
670
-
671
- <script>
672
- const nameInput = pp.ref(null);
673
- </script>
674
- </div>
675
- ```
664
+ </div>
665
+ ```
666
+
667
+ Component example:
668
+
669
+ ```html
670
+ <div>
671
+ <x-input pp-ref="{nameInput}" name="name" />
672
+ <button onclick="nameInput.current?.focus()">Focus</button>
673
+
674
+ <script>
675
+ const nameInput = pp.ref(null);
676
+ </script>
677
+ </div>
678
+ ```
676
679
 
677
680
  ## Lists and keyed diffing
678
681
 
679
682
  - Use `pp-for` only on `<template>`.
680
683
  - Supported forms are `item in items` and `(item, index) in items`.
681
- - Collections may be arrays or synchronous iterables such as `Set`, `Map`, typed arrays, strings, and generators. Non-null non-iterable values warn and render as empty lists.
684
+ - Collections may be arrays or synchronous iterables such as `Set`, `Map`, typed arrays, strings, and generators. Non-null non-iterable values warn and render as empty lists.
682
685
  - Loop content can contain interpolations, events, refs, and nested components.
683
- - Event handlers inside loops capture each rendered item, so later collection changes do not retarget an existing row handler.
686
+ - Event handlers inside loops capture each rendered item, so later collection changes do not retarget an existing row handler.
684
687
  - Use stable unique `key` values on repeated sibling elements.
685
688
  - Duplicate keys trigger warnings and reduce diff quality.
686
689
  - Keyed reconciliation preserves DOM identity across reorders and insertions.
@@ -737,9 +740,9 @@ Example:
737
740
  - Push navigation resets window scroll to the top.
738
741
  - Saved history-entry scroll state is used during history traversal instead of relying only on the live DOM scroll at the time the user clicks Back or Forward.
739
742
  - Unmarked scrollable containers may keep their outgoing scroll on push navigation so shared shells such as sidebars, rails, and docs nav panes stay stable across child-route changes.
740
- - `pp-reset-scroll="true"` on a scroll container opts that container into reset-on-navigation behavior. Use it on the main content pane of a grouped shell when page content should start at the top on each child-route navigation.
741
- - `pp-scroll-key="stable-name"` gives a scroll container a stable restoration identity. Prefer it when an element has no stable `id`, or when its classes or position can change between routes.
742
- - `body[pp-reset-scroll="true"]` is the global override for routes that should reset window scroll and every scrollable element.
743
+ - `pp-reset-scroll="true"` on a scroll container opts that container into reset-on-navigation behavior. Use it on the main content pane of a grouped shell when page content should start at the top on each child-route navigation.
744
+ - `pp-scroll-key="stable-name"` gives a scroll container a stable restoration identity. Prefer it when an element has no stable `id`, or when its classes or position can change between routes.
745
+ - `body[pp-reset-scroll="true"]` is the global override for routes that should reset window scroll and every scrollable element.
743
746
  - Navigation dispatches `pp:navigation:start`, `pp:navigation:complete`, and `pp:navigation:error` events on `document`.
744
747
 
745
748
  RPC notes:
@@ -881,9 +884,9 @@ These are current runtime caveats that matter for authors and AI tools:
881
884
  - Caspian already injects `pp-component` and rewrites owned scripts to `type="text/pp"` during render.
882
885
  - Nested roots without their own `script[type="text/pp"]` block are not fully isolated during parent template compilation.
883
886
  - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
884
- - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
885
- - Callback refs may return synchronous cleanup functions, which run instead of a later `callback(null)` detach call.
886
- - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
887
+ - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
888
+ - Callback refs may return synchronous cleanup functions, which run instead of a later `callback(null)` detach call.
889
+ - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
887
890
  - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
888
891
  - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
889
892
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {