create-caspian-app 1.4.1 → 1.4.3
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.
|
@@ -78,7 +78,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
78
78
|
- When `caspian.config.json` has `websocket: true`, every named socket shares **one** endpoint — `SOCKET_PATH` in `src/lib/websocket/sockets.py`, wired once in `main.py` — and the function is named in a query parameter, so socket names are application-wide and a route never declares its own path. `pp.socket(...)` already knows that endpoint, so do not pass `websocket_path`/`websocket_url` into templates. Keep the socket layer itself under `src/lib/websocket/**`.
|
|
79
79
|
- For route creation, every route is one `src/app/**/index.py`: `page()` returns the page markup via `html(...)`, and the same module owns metadata, `@rpc()` actions, auth checks, caching, and redirects. Shared section wrappers live in `layout.py`, whose `layout()` returns the wrapper template (optionally with a props dict). Non-visual routes (redirect-only or action-only) are `index.py` files whose `page()` returns a `Response`.
|
|
80
80
|
- Keep route-specific logic in that route's `index.py`. Move code into `src/lib/**` only when it is genuinely reusable across routes, components, integrations, or features; do not extract one-route orchestration just to make it look generic.
|
|
81
|
-
- Write every authored route, layout, and component template with one parent HTML element or one imported `x-*` component tag as its root, and keep any `<script>` inside that root rather than after it. A component **may** instead have sibling top-level nodes: that is a fragment (the `<>…</>` shape), the compiler frames it with a `<!--pp:id-->…<!--/pp-->` comment pair, and the browser materializes it into `<pp-fragment style="display: contents">` at mount — so it adds no element to the DOM and is the only shape that survives inside `<tbody>`/`<select>`. **A fragment cannot receive props**: passing any attribute (including `pp-ref`) on its `<x-*>` tag raises `FragmentPropsError`, because there is no root element for forwarding to land on. Use a single native root whenever the component takes props. A
|
|
81
|
+
- Write every authored route, layout, and component template with one parent HTML element or one imported `x-*` component tag as its root, and keep any `<script>` inside that root rather than after it. A component **may** instead have sibling top-level nodes: that is a fragment (the `<>…</>` shape), the compiler frames it with a `<!--pp:id-->…<!--/pp-->` comment pair, and the browser materializes it into `<pp-fragment style="display: contents">` at mount — so it adds no element to the DOM and is the only shape that survives inside `<tbody>`/`<select>`. **A fragment cannot receive props**: passing any attribute (including `pp-ref`) on its `<x-*>` tag raises `FragmentPropsError`, because there is no root element for forwarding to land on. Use a single native root whenever the component takes props. A route or layout with sibling top-level nodes is instead wrapped in a layout-neutral `<div pp-component style="display: contents">` boundary host, so use plain siblings when a wrapper `<div>` would be meaningless. The same host appears around a component whose authored root is another `x-*` tag, carrying the parent's forwarded props — an extra `display: contents` div in rendered DOM is expected output. Fragment syntax is implicit: never hand-write `<pp-fragment>` or `<!--pp:…-->` markers, which are compiler output.
|
|
82
82
|
- When the user asks for a dashboard, admin area, account area, or any grouped child-route section, follow the same mental model as the Next.js App Router: create a parent folder with `layout.py` and place the child routes beneath it. Use a normal folder such as `dashboard/` when the segment should appear in the URL, and use `(group)/` only when it should not.
|
|
83
83
|
- In grouped section layouts with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content scroll container that should reset on child-route navigation, usually the main pane. Leave persistent shell scrollers such as sidebars or rails unmarked so SPA navigation can preserve their scroll position.
|
|
84
84
|
- **`loading.py` is optional — but when navigation loading UI is wanted, it is the mechanism, not a hand-built spinner.** Most subtrees have no loader and navigate with a plain fade; that is correct default behavior, so do not create `loading.py` files that were not asked for. When a task _does_ ask for a loading state, skeleton, or progress indicator _while moving from one route to another_, the answer is a `loading.py` in the closest subtree folder plus `pp-loading-content="true"` on the pane it should replace in that subtree's `layout.py`. Never hand-roll it with a spinner component, a global `isLoading` store, a `pp:navigation:start`/`pp:navigation:complete` listener, a manual overlay, or a `fetch`-driven page swap: `casp/loading.py` already collects the files, `caspian_config.py` derives each one's URL scope from its folder, and the browser runtime resolves the closest ancestor scope and runs the swap and fade. Contract: `def loading():` is **synchronous and takes no parameters** (an `async def` raises `TypeError`, a missing function raises `AttributeError`); scope is folder-derived, with `(group)` segments stripped (so a loader placed directly inside `(marketing)/` becomes the app-wide `/` fallback, not the group's) and a `[id]` folder never matching a real URL (so dynamic routes put their loader on the static parent); the markup is collected once and injected as raw HTML, so Jinja `{{ }}` interpolates but `<x-*>` tags, `{ }` bindings, and `<script>` do **not** work inside it — plain elements and CSS only; and `pp-loading-transition='{"fadeIn":…,"fadeOut":…}'` inside the loader overrides the 250 ms default. Copy the shipped pair: `src/app/dashboard/loading.py` with the `pp-loading-content="true"` bar in `src/app/dashboard/layout.py`.
|
|
@@ -93,7 +93,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
93
93
|
- Treat `pp-component` on routes, layouts, and components as compiler-injected by the Python side; do not add it manually in authored templates unless the task is explicitly about runtime internals. Author owned PulsePoint logic as a plain `<script>` inside the component root.
|
|
94
94
|
- `layout()` can be synchronous or async in the installed runtime. Keep async layout work focused on shared layout props or metadata; use `page()` or `@rpc()` when the work belongs to a specific route or user action.
|
|
95
95
|
- Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
|
|
96
|
-
- In `layout.py`, `layout()` returns `html(r"""...""", **context)` — the same entrypoint pages and components use. It is _deferred_: `children` is the page beneath the layout and does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children`/`layout`/`metadata` merged in (those three always win over author context). Also accepted: `(html(...), props_dict)`, a props dict alone (passthrough `<slot />` shell), `None`, or a bare template string
|
|
96
|
+
- In `layout.py`, `layout()` returns `html(r"""...""", **context)` — the same entrypoint pages and components use. It is _deferred_: `children` is the page beneath the layout and does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children`/`layout`/`metadata` merged in (those three always win over author context). Also accepted: `(html(...), props_dict)`, a props dict alone (passthrough `<slot />` shell), `None`, or a bare template string. A layout that never places its children — no `<slot />` and no `{{ children }}` — raises `LayoutChildrenError` rather than serving an empty shell.
|
|
97
97
|
- Never author markup as a Python f-string. `{{ x }}` is server interpolation and `{ x }` is a PulsePoint binding inside `html(...)`; an f-string inverts both, skips autoescaping while still marking the result trusted, and skips the `<x-*>` scope stash. The `templates` gate freezes the existing f-string components in `settings/fstring-components.json` and fails on new ones.
|
|
98
98
|
- Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
|
|
99
99
|
- Route, layout, and component templates must keep exactly one authored top-level parent node so Caspian can inject `pp-component` after component expansion. In source, that parent may be a native HTML element or a single imported `x-*` component tag, but it must resolve to one final HTML root. Keep any owned PulsePoint script inside that same parent.
|
package/dist/AGENTS.md
CHANGED
|
@@ -60,7 +60,7 @@ Authoring is **Python-only and single-file**. There are no `.html` sidecars in t
|
|
|
60
60
|
|
|
61
61
|
- `layout()` returns `html(r"""...""", **context)` — but **deferred**: `children` (the page below) does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children` / `layout` / `metadata` merged in. Those three names are engine-owned and always win over author context.
|
|
62
62
|
- The layout must place its children — `<slot />` or `{{ children }}` — or it raises `LayoutChildrenError`.
|
|
63
|
-
- Also accepted returns: `(html(...), props_dict)` (props become `{{ layout.* }}` for the subtree), a bare props `dict`, `None`, or a
|
|
63
|
+
- Also accepted returns: `(html(...), props_dict)` (props become `{{ layout.* }}` for the subtree), a bare props `dict`, `None`, or a plain template string.
|
|
64
64
|
- Deferral is keyed on the `layout()` frame only; a component or helper called from a layout still renders eagerly.
|
|
65
65
|
- Grouped sections (dashboard/admin/account) = parent folder + `layout.py` + child routes, exactly like the App Router. Put `pp-reset-scroll="true"` on the content pane that should reset on child navigation; leave shell scrollers unmarked.
|
|
66
66
|
|
|
@@ -295,7 +295,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
295
295
|
- **PulsePoint is not React and its templates are not JSX.** This workspace's guidance compares PulsePoint to React in exactly two places — the `pp.*` hook API inside `<script>`, and how components are split by responsibility — and that comparison stops at the markup. Template files are plain HTML. Never generate `{cond && (<div/>)}`, `{cond ? <A/> : <B/>}`, `{list.map(item => (<tr/>))}`, `className`, `htmlFor`, camelCase `onClick`, `style={{...}}`, `dangerouslySetInnerHTML`, or `<>…</>`. Use `hidden="{!cond}"` for conditionals, `<template pp-for="item in list">` with `key="{item.id}"` for lists, and **always quote brace attributes** — `class="{...}"`, never `class={...}`. The unquoted form is invalid HTML: the parser splits the value on spaces into junk attributes, the component root never compiles, and the route serves a blank page with no console error (the body's `opacity: 0` reveal never fires). There is no `pp-if`, `pp-show`, `pp-else`, or `pp-key`. Sanity check before finishing any template: it must still be valid HTML with every `{}` deleted. See `node_modules/caspian-utils/dist/docs/pulsepoint.md` sections "PulsePoint Is Not JSX", "Complete Directive And API Surface", and "Conditional rendering".
|
|
296
296
|
- Split single-file Python components by responsibility, using the same mental model as React components — **for decomposition and single-root shape only, never for syntax** (see the rule above). A page with tabs should usually have one component for the tab shell and separate components for each substantial tab panel. A section with its own form, table, toolbar, or list should usually be its own component with data and options passed by props, not an unrelated block inside a giant Python file.
|
|
297
297
|
- **Authoring is Python-only, single-file, and `html(...)` is the one markup entrypoint — pages, layouts and components alike.** A route is one `index.py` whose `page()` returns `html(r"""...""", **context)`; a layout is one `layout.py` whose `layout()` returns `html(r"""...""", **context)` (optionally as `(html(...), props_dict)`); a component is one `.py` returning `html(...)`. Inside `html(...)`, `{{ ... }}` is server-side Jinja and `{ ... }` stays for PulsePoint. **Never author markup as a Python f-string**: the brace dialects invert (`{x}` becomes server interpolation and a PulsePoint binding must be written `{{x}}`), the string is not autoescaped yet `Component.acall` still marks it trusted, and the `<x-*>` scope stash is skipped so a directly-called component cannot resolve nested tags. The `templates` gate enforces this — existing f-string components are frozen in `settings/fstring-components.json` and any NEW one fails `npm run check`; converting one means deleting its baseline line. Prefer `r"""..."""` when the markup's `<script>` contains backslashes. See `node_modules/caspian-utils/dist/docs/components.md` and `file-conventions.md`.
|
|
298
|
-
- **A layout's `html(...)` is deferred, not rendered at call time**, because `children` is the page beneath it and does not exist yet. `html(...)` called from a `layout()` returns a `LayoutTemplate` — the unrendered source plus the author's context — and the layout engine renders it once with `children`/`layout`/`metadata` merged in; those three engine-owned names always win over author context. Deferral is keyed on the `layout()` frame specifically, so a component the layout calls, or a helper in the same file, still renders eagerly. The
|
|
298
|
+
- **A layout's `html(...)` is deferred, not rendered at call time**, because `children` is the page beneath it and does not exist yet. `html(...)` called from a `layout()` returns a `LayoutTemplate` — the unrendered source plus the author's context — and the layout engine renders it once with `children`/`layout`/`metadata` merged in; those three engine-owned names always win over author context. Deferral is keyed on the `layout()` frame specifically, so a component the layout calls, or a helper in the same file, still renders eagerly. The other accepted shapes (a bare template string, `(str, props)`, a props `dict`, `None`) take the same engine path, because `LayoutTemplate`'s string value _is_ the raw source. A layout that places its children nowhere (no `<slot />`, no `{{ children }}`) now raises `LayoutChildrenError` instead of serving an empty shell.
|
|
299
299
|
- In a prop-receiving single-file Python component, `x-*` attributes arrive as raw string kwargs (including unevaluated strings such as `"{permOpen}"`) and do not become browser `pp.props` automatically. Forward every browser-facing prop onto the single native root with `get_attributes({...}, props)`, render `<root {{ attributes }}>`, and pass `attributes=attributes` into `html(...)`. Props accepted by Python but not re-emitted are silently absent from `pp.props`; no server error or browser warning is raised. Remember that forwarded names are real DOM attributes, so avoid unintended native collisions such as `title` when a component-specific name like `user-name` is appropriate. A named Python parameter is consumed out of `**props`, so it is no longer in the passthrough dict and must be listed explicitly in the `get_attributes` defaults. Forwarding also does not preserve types: a brace expression (`volume="{vol}"`) is evaluated in parent scope and keeps its real type, but a literal server value renders as a string, so `volume="0"` makes `volume === 0` false; a valueless attribute becomes `true`; `None`/`False`/`""` are omitted entirely so the prop reads `undefined` rather than `false`; and JS reserved words such as `class` are dropped from `pp.props`. When an icon toggle, `hidden`, or class binding silently does nothing, verify the prop is on the rendered root before debugging the expression. See `node_modules/caspian-utils/dist/docs/components.md` "Receiving Props In A Python Component" and "Every Prop A Template Reads Must Be Forwarded To The Root."
|
|
300
300
|
- Composition is Python-import-driven everywhere: a module's `x-*` tags (page, layout, or component) resolve from the Component objects imported into that module; the Python import is the only import mechanism, enforced by the compiler and the `templates` gate rule `import-comment`. Runtime resolution precedence inside a component's output is inherited ancestor components, then the module's own Python imports. Slot content (children) resolves in the scope where it was authored, so the module that writes an `x-*` tag in markup must import that component. For directories whose names are not valid Python identifiers (hyphens, `(group)`), bind the component with `Name = importlib.import_module("src.app.some-dir.Name").Name`.
|
|
301
301
|
- For first-party HTML interactivity in this workspace, PulsePoint is the required default. Use PulsePoint `on*` event attributes, `pp.state`, refs, effects, directives, and `pp.rpc()` instead of inventing id/data-attribute driven JavaScript with `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or parallel client state. For simple forms, bind `onsubmit` in the HTML, convert named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`, and validate/normalize that payload in Python; do not add `pp-ref` to each input, create a form ref, and attach an effect-managed submit listener just to collect submitted values.
|
|
@@ -315,8 +315,8 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
315
315
|
- The `pp` component-script API mirrors React hooks **inside the `<script>` only** — the surrounding markup is never JSX: `state`, `effect`, `layoutEffect`, `ref`, `memo`, `callback`, `reducer`, `context`, `portal`, `id`, `syncExternalStore`, `imperativeHandle`, `transition`, `deferredValue`, `optimistic`, `errorBoundary`, plus `props`. Use `pp.id()` for generated `id`/`for`/`aria-*` values, `pp.syncExternalStore(...)` for sources the component does not own, and wrap failure-prone subtrees in a parent with `pp.errorBoundary()` instead of letting a render throw reach the console. Verify against `public/js/pp-reactive-v2.min.js` and see `pulsepoint.md` "Hooks and runtime API".
|
|
316
316
|
- For grouped-subtree SPA navigation UX, the current browser runtime keeps unmarked shell scrollers stable and uses `pp-reset-scroll="true"` on the content pane that should reset. Check `pulsepoint.md`, `routing.md`, and `public/js/pp-reactive-v2.min.js` before changing that behavior.
|
|
317
317
|
- Before updating docs, verify runtime-specific claims such as middleware order, route param injection, `layout()` behavior, `StateManager` persistence, safe public-file serving, response header, or session-secret behavior against the current `main.py` and installed `casp` package, especially `.venv/Lib/site-packages/casp/runtime_security.py`, rather than copying older notes.
|
|
318
|
-
- When generating or reviewing page templates, layout templates, or component markup, single-root is the default shape: one authored top-level parent element or one imported `x-*` root, with any owned `<script>` kept inside that same root. **
|
|
319
|
-
- **A multi-root component is a fragment — the `<>…</>` shape
|
|
318
|
+
- When generating or reviewing page templates, layout templates, or component markup, single-root is the default shape: one authored top-level parent element or one imported `x-*` root, with any owned `<script>` kept inside that same root. **A component may instead have sibling top-level nodes**, which make it a fragment, framed by the comment-pair boundary described below. `TemplateRootError` covers a component template with no root at all, or with an `x-*` tag as its only root. Prefer a single native root anyway — it is the only shape that can receive props. **For a page (`index.py`) or layout (`layout.py`)**: sibling top-level nodes are wrapped in a layout-neutral `<div pp-component="…" style="display: contents">` boundary host, so a page whose sections are genuinely siblings does not need a meaningless wrapper `<div>`. The same host is emitted for a _composition component_ whose authored root is another `x-*` tag, carrying the parent's forwarded props (which is what makes them reach `pp.props`) and `pp-ref-forward` — so an extra `display: contents` div between two component roots in rendered DOM is expected output, not a bug. Keep the owned `<script>` inside the template either way: the host is the boundary, so one script still covers every root.
|
|
319
|
+
- **A multi-root component is a fragment — the `<>…</>` shape.** When a component's `html(...)` has sibling top-level nodes, the compiler frames them with the comment pair `<!--pp:id-->…<!--/pp-->`, which `materializeRangeBoundaries` turns into a live `<pp-fragment style="display: contents" pp-component="id">` at mount, before the boundary scan. So a fragment component adds **no element** to the rendered tree, and it is the only shape that survives inside `<tbody>`/`<tr>`/`<select>`/`<optgroup>`, where a `display: contents` wrapper is foster-parented out by the HTML parser (the browser runtime deliberately leaves markers under those parents as comments, so the grouping renders but the fragment owns no identity there — a stateful fragment needs a context an element could also live in). The syntax is **implicit**: siblings in the template, nothing to hand-write. Still never type `<pp-fragment>` or `<!--pp:…-->` yourself — those are compiler/runtime output, and an authored marker is refused by the subtree render cache. **A fragment cannot receive props**: with no root element there is nowhere for `get_attributes(...)` forwarding to land and `pp.props` would be silently empty, so passing any attribute (including `pp-ref`) on the `<x-*>` tag of a fragment component raises `FragmentPropsError` at compile time — give the component a single native root when it needs props. Fragments are excluded from the subtree render cache; a fragment nested inside a cached subtree still has its marker id re-minted per instance.
|
|
320
320
|
- Form controls are controlled _or_ uncontrolled for an element's lifetime. `value="{state}"` / `checked="{state}"` is controlled; the lowercase HTML attributes `defaultvalue="{expr}"` / `defaultchecked="{expr}"` are the uncontrolled form and are real PulsePoint syntax (the camelCase React spellings are not). Binding `value` to state that starts `undefined` flips the mode and makes the runtime log `[PP-WARN] <input#x> changed from uncontrolled to controlled` once — fix the initial state, do not add both attributes.
|
|
321
321
|
- When generating or reviewing sign-in flows, do not ask the sign-in page to decide redirect targets by re-implementing `next` support or post-login routing. In this stack, redirect behavior is already owned by the Caspian auth runtime plus `src/lib/auth/auth_config.py`; protected-route guest redirects, auth-route redirects, and the default destination are centralized there, with `default_signin_redirect` defaulting to `/dashboard`.
|
|
322
322
|
- Component markup is server-deferred in an inert `<template>`. `main.py` finalizes every page through `defer_component_roots(...)`, which wraps each outermost `pp-component` root in `<template pp-component="…">`. The browser never parses/validates/fetches `<template>` contents, so raw `{...}` placeholders never reach live DOM at first paint. During `mount()`, PulsePoint captures and empties each plain component `<script>` before materializing `template[pp-component]` into live DOM, then evaluates that captured source in component scope; the same guard applies to scripts introduced by later morphs. Because of this, `{...}` is safe in ANY attribute or position — SVG geometry (`d`, `viewBox`, `points`, `transform`), URL attributes (`src`, `srcset`, `href`, `poster`), form `value`/date/number/color, and text placed directly inside `<table>`/`<select>`. Do NOT add per-tag workarounds to dodge browser first-paint validation: no static-path `hidden` toggles just to avoid binding `d`, no `data-*` URL holders, no gating `<img src>` behind `hidden`, and no SSR-resolving an initial value only to prevent a validation flash. Two compiler transforms still apply for different reasons and stay: `pp-style` (so `.html` source-file HTML/CSS tooling does not choke on `style="{...}"`) and the `<input>`/`<select>`/`checked`/`defaultvalue`/`<textarea>` value rewrites (attribute-vs-property correctness for controlled form fields), not first-paint validation.
|
|
@@ -354,7 +354,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
354
354
|
|
|
355
355
|
Use this map before making changes.
|
|
356
356
|
|
|
357
|
-
If the task generates or edits route, layout, or component HTML templates, check `routing.md`, `components.md`, and `pulsepoint.md` before writing markup. Enforce the root-shape contract there: one authored root by default, any owned `<script>` inside that root — relaxed to a comment-pair fragment boundary for a multi-root component (which then cannot take props) and to a `display: contents` boundary host for a multi-root
|
|
357
|
+
If the task generates or edits route, layout, or component HTML templates, check `routing.md`, `components.md`, and `pulsepoint.md` before writing markup. Enforce the root-shape contract there: one authored root by default, any owned `<script>` inside that root — relaxed to a comment-pair fragment boundary for a multi-root component (which then cannot take props) and to a `display: contents` boundary host for a multi-root page or layout. For reactive behavior, button clicks, form events, uploads, filters, toggles, and list updates, use PulsePoint in the template first instead of standard DOM-event wiring. For normal form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref`/`pp.effect` listener boilerplate.
|
|
358
358
|
|
|
359
359
|
- Project layout and file placement: read `node_modules/caspian-utils/dist/docs/index.md` and `node_modules/caspian-utils/dist/docs/project-structure.md`. Verify against the current workspace tree.
|
|
360
360
|
- File conventions and special route files: read `node_modules/caspian-utils/dist/docs/file-conventions.md` and `node_modules/caspian-utils/dist/docs/routing.md`. Verify against `main.py`, `.venv/Lib/site-packages/casp/layout.py`, `.venv/Lib/site-packages/casp/loading.py`, and `.venv/Lib/site-packages/casp/caspian_config.py`.
|