create-caspian-app 1.1.0 → 1.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/README.md CHANGED
@@ -171,9 +171,9 @@ src/app/dashboard/layout.py -> wraps every /dashboard/* page
171
171
  | ---------------- | ------------------------------------------------------------------------------------------------------------- |
172
172
  | `index.py` | The page: `page()` returning `html(...)` markup, `metadata`, route-owned `@rpc()` actions, redirects, first-render data |
173
173
  | `layout.py` | The section wrapper: `layout()` returning the shell template (with `<slot />`) plus optional props and server logic |
174
- | `loading.html` | Navigation loading state for the route |
175
- | `not-found.html` | 404 UI |
176
- | `error.html` | Error UI |
174
+ | `loading.py` | Route-scoped navigation loader: synchronous `loading()` returning `html(...)` markup |
175
+ | `not_found.py` | Global 404 page: `page()` returning `html(...)` markup and metadata |
176
+ | `error.py` | Global 500 page: `page()` receiving safe error context and returning `html(...)` markup |
177
177
 
178
178
  `page()` returns `html(r"""...""", **context)`. It can also return a
179
179
  `(page_html, layout_props)` tuple, whose dict keys become `{{ layout.* }}` in a parent layout.
@@ -620,8 +620,9 @@ my-app/
620
620
  │ │ ├── layout.py # Root layout (template + props from layout())
621
621
  │ │ ├── index.py # Home page (markup + logic in one file)
622
622
  │ │ ├── globals.css
623
- │ │ ├── error.html
624
- │ │ ├── not-found.html
623
+ │ │ ├── error.py # Global 500 page
624
+ │ │ ├── not_found.py # Global 404 page
625
+ │ │ ├── dashboard/loading.py # /dashboard navigation loading UI
625
626
  │ │ └── users/[id]/
626
627
  │ │ └── index.py # /users/:id
627
628
  │ ├── components/ # Reusable UI (@component)
@@ -231,7 +231,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
231
231
 
232
232
  - These files are the packaged Caspian documentation layer, not the runtime and not the source of current workspace state.
233
233
  - Use them to help AI answer three questions: which Caspian feature applies, which project files should be inspected next, and which workflow is appropriate once the feature is confirmed as enabled.
234
- - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when deciding what belongs in `index.py`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
234
+ - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` for the general special-file model, then verify the completed Python migration in `main.py` and `.venv/Lib/site-packages/casp/**`: navigation loading UI uses `loading.py`, and global fallback pages use `not_found.py` and `error.py`. This app has no authored `.html` special files.
235
235
  - Use `node_modules/caspian-utils/dist/docs/websockets.md` when deciding how to document or implement app-owned FastAPI WebSockets, browser `WebSocket` clients, origin checks, auth/session checks, message contracts, and the choice between WebSockets, RPC, and SSE.
236
236
  - Verify behavior claims in this order:
237
237
  1. `caspian.config.json`, then `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, `src/app/**`
package/dist/AGENTS.md CHANGED
@@ -70,7 +70,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
70
70
  - Treat `public/` as a URL-root mapping, not a directory registry: an existing `public/icons/app.png` is served as `/icons/app.png` without a per-directory route, mount, or prefix list in `main.py`. `PublicFilesMiddleware` handles only `GET`/`HEAD`, resolves paths beneath `public/`, rejects traversal and symlink escape, and falls through when no file exists. Keep it inside `SecurityHeadersMiddleware` and outside rate limiting, body parsing, sessions, CSRF, auth, RPC, and page routing.
71
71
  - Use `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` when a behavior is controlled by the shipped PulsePoint browser runtime and the task names state, effects, refs, context, portals, directives, `pp.rpc`, uploads, streaming, SPA navigation, or scroll restoration.
72
72
  - Use `node_modules/caspian-utils/dist/docs/websockets.md` when the task names WebSockets, live bidirectional channels, socket origin checks, socket auth/session behavior, broadcast managers, or native browser `WebSocket` clients.
73
- - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when the task asks what belongs in `index.py`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
73
+ - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` for the general special-file model, then verify the completed Python migration in `main.py` and `.venv/Lib/site-packages/casp/**`: routes use `index.py`, layouts use `layout.py`, loading UI uses `loading.py`, and global fallback pages use `not_found.py` and `error.py`. This app has no authored `.html` special files.
74
74
  - When `caspian.config.json` has `prisma: true`, database reads and writes from Python routes, layouts, RPC actions, upload flows, auth flows, and helpers must use the generated Prisma Python ORM in `src/lib/prisma/**`. Do not create a separate database fetch layer with raw drivers, hand-written SQL helpers, JSON manifests, app-specific HTTP fetches, or browser-side data fetches to replace the ORM. Use raw SQL only as a narrow Prisma ORM fallback when the generated client cannot express a query clearly.
75
75
  - Treat `npx prisma db seed` as a delicate, potentially destructive operation. In this workspace, seed scripts may clear tables before inserting fresh records. Before running that command, an AI agent must propose the exact command, warn that it can delete or overwrite database data including production data if the datasource is wrong, confirm the datasource when practical, and wait for explicit user approval.
76
76
  - Component-first page composition is the highest-priority authoring rule for this workspace (see `.github/copilot-instructions.md`). Build pages as a short assembly of `x-*` chunk components (top menu, sidebar, header, content sections, cards, forms, footer) and keep each chunk's long markup inside its own focused single-file `html(...)` component, so the page template in `src/app/**/index.py` stays small instead of holding a wall of HTML. Plan the chunk breakdown before writing the route, not as a later cleanup pass.
@@ -0,0 +1,113 @@
1
+ from typing import Optional
2
+
3
+ from casp.component_decorator import html
4
+ from casp.layout import Metadata
5
+
6
+
7
+ metadata = Metadata(
8
+ title="Application Error",
9
+ description="An unexpected error occurred.",
10
+ extra={"robots": "noindex, nofollow"},
11
+ )
12
+
13
+
14
+ def page(error_message: str, error_trace: Optional[str] = None):
15
+ return html(r"""
16
+ <main class="container py-10">
17
+ <div class="mx-auto max-w-2xl rounded bg-white p-6 shadow-sm">
18
+ <div class="flex items-start gap-4">
19
+ <svg class="h-10 w-10 text-red-600"
20
+ viewBox="0 0 24 24"
21
+ fill="none"
22
+ xmlns="http://www.w3.org/2000/svg"
23
+ aria-hidden="true">
24
+ <path d="M11.001 2a1 1 0 0 1 .998 0L21 7v9a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7l8.001-5z"
25
+ stroke="currentColor"
26
+ stroke-width="1.5"
27
+ stroke-linecap="round"
28
+ stroke-linejoin="round" />
29
+ <path d="M12 8v4"
30
+ stroke="currentColor"
31
+ stroke-width="1.5"
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round" />
34
+ <path d="M12 16h.01"
35
+ stroke="currentColor"
36
+ stroke-width="1.5"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round" />
39
+ </svg>
40
+
41
+ <div class="flex-1">
42
+ <h1 class="text-2xl font-semibold text-red-600">Something went wrong</h1>
43
+ <p class="mt-2 text-sm text-gray-600">
44
+ We're sorry—an unexpected error occurred. Try reloading or return to
45
+ the homepage.
46
+ </p>
47
+ <p class="mt-3 text-sm text-gray-700">{{ error_message }}</p>
48
+
49
+ <div class="mt-4 flex flex-wrap gap-2">
50
+ <a href="/"
51
+ class="inline-block rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800 hover:bg-gray-200">
52
+ Home
53
+ </a>
54
+ <button class="inline-block rounded bg-blue-600 px-3 py-1.5 text-sm text-white"
55
+ onclick="window.location.reload()">
56
+ Reload
57
+ </button>
58
+ {% if error_trace %}
59
+ <button class="inline-block rounded border border-gray-200 bg-transparent px-3 py-1.5 text-sm text-black"
60
+ onclick="setShowTrace(current => !current)">
61
+ {showTrace ? "Hide details" : "Show details"}
62
+ </button>
63
+ {% endif %}
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ {% if error_trace %}
69
+ <section class="mt-4" hidden="{!showTrace}">
70
+ <div class="mb-2 flex items-center justify-between">
71
+ <div class="text-xs text-gray-500">Error details (development only)</div>
72
+ <div class="flex gap-2">
73
+ <button class="rounded border bg-gray-100 px-2 py-1 text-xs text-black"
74
+ onclick="copyTrace()">
75
+ {traceCopied ? "Copied" : "Copy"}
76
+ </button>
77
+ <button class="rounded border bg-gray-100 px-2 py-1 text-xs text-black"
78
+ onclick="downloadTrace()">
79
+ Download
80
+ </button>
81
+ </div>
82
+ </div>
83
+ <pre pp-ref="{tracePre}"
84
+ class="overflow-auto rounded bg-gray-100 p-4 text-xs text-black">{{ error_trace }}</pre>
85
+ </section>
86
+ {% endif %}
87
+
88
+ <script>
89
+ const [showTrace, setShowTrace] = pp.state(false);
90
+ const [traceCopied, setTraceCopied] = pp.state(false);
91
+ const tracePre = pp.ref(null);
92
+
93
+ async function copyTrace() {
94
+ if (!tracePre.current) return;
95
+ await navigator.clipboard.writeText(tracePre.current.innerText);
96
+ setTraceCopied(true);
97
+ setTimeout(() => setTraceCopied(false), 2000);
98
+ }
99
+
100
+ function downloadTrace() {
101
+ if (!tracePre.current) return;
102
+ const blob = new Blob([tracePre.current.innerText], { type: "text/plain" });
103
+ const url = URL.createObjectURL(blob);
104
+ const link = document.createElement("a");
105
+ link.href = url;
106
+ link.download = "error_trace.txt";
107
+ link.click();
108
+ URL.revokeObjectURL(url);
109
+ }
110
+ </script>
111
+ </div>
112
+ </main>
113
+ """, error_message=error_message, error_trace=error_trace)
@@ -1,3 +1,8 @@
1
+ from casp.component_decorator import html
2
+
3
+
4
+ def page():
5
+ return html(r"""
1
6
  <div
2
7
  class="min-h-screen bg-background text-foreground selection:bg-primary/30 flex flex-col items-center justify-between py-8 md:py-12 px-6 md:px-24 overflow-x-hidden relative"
3
8
  >
@@ -157,3 +162,4 @@
157
162
  </a>
158
163
  </div>
159
164
  </div>
165
+ """)
@@ -0,0 +1,32 @@
1
+ def layout():
2
+ return r"""
3
+ <!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>{{ metadata.title | default("Caspian App") }}</title>
9
+ <meta
10
+ name="description"
11
+ content="{{ metadata.description | default('Powered by Caspian Framework') }}"
12
+ />
13
+ {% if metadata.robots %}
14
+ <meta name="robots" content="{{ metadata.robots }}" />
15
+ {% endif %}
16
+ <link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16" />
17
+ <link href="/css/styles.css" rel="stylesheet" />
18
+ <script type="module" src="/js/main.js"></script>
19
+ </head>
20
+
21
+ <body
22
+ style="
23
+ opacity: 0;
24
+ pointer-events: none;
25
+ user-select: none;
26
+ transition: opacity 0.18s ease-out;
27
+ "
28
+ >
29
+ <slot />
30
+ </body>
31
+ </html>
32
+ """
@@ -1,18 +1,32 @@
1
- <div class="bg-gray-50 flex h-screen items-center justify-center px-4">
2
- <div class="text-center">
3
- <h1 class="text-9xl font-black text-gray-200">404</h1>
4
-
5
- <p class="text-2xl font-bold tracking-tight text-gray-900 sm:text-4xl">
6
- Uh-oh!
7
- </p>
8
-
9
- <p class="mt-4 text-gray-500">We can't find that page.</p>
10
-
11
- <a
12
- href="/"
13
- class="mt-6 inline-block rounded bg-indigo-600 px-5 py-3 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring"
14
- >
15
- Go Back Home
16
- </a>
17
- </div>
18
- </div>
1
+ from casp.component_decorator import html
2
+ from casp.layout import Metadata
3
+
4
+
5
+ metadata = Metadata(
6
+ title="Page Not Found",
7
+ description="The page you are looking for does not exist.",
8
+ extra={"robots": "noindex, nofollow"},
9
+ )
10
+
11
+
12
+ def page():
13
+ return html(r"""
14
+ <main class="flex min-h-screen items-center justify-center bg-gray-50 px-4">
15
+ <div class="text-center">
16
+ <h1 class="text-9xl font-black text-gray-200">404</h1>
17
+
18
+ <p class="text-2xl font-bold tracking-tight text-gray-900 sm:text-4xl">
19
+ Uh-oh!
20
+ </p>
21
+
22
+ <p class="mt-4 text-gray-500">We can't find that page.</p>
23
+
24
+ <a
25
+ href="/"
26
+ class="mt-6 inline-block rounded bg-indigo-600 px-5 py-3 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring"
27
+ >
28
+ Go Back Home
29
+ </a>
30
+ </div>
31
+ </main>
32
+ """)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,111 +0,0 @@
1
- <div class="container py-10">
2
- <div class="max-w-2xl mx-auto bg-white shadow-sm rounded p-6">
3
- <div class="flex items-start gap-4">
4
- <svg class="w-10 h-10 text-red-600"
5
- viewBox="0 0 24 24"
6
- fill="none"
7
- xmlns="http://www.w3.org/2000/svg"
8
- aria-hidden>
9
- <path d="M11.001 2a1 1 0 0 1 .998 0L21 7v9a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7l8.001-5z"
10
- stroke="currentColor"
11
- stroke-width="1.5"
12
- stroke-linecap="round"
13
- stroke-linejoin="round" />
14
- <path d="M12 8v4"
15
- stroke="currentColor"
16
- stroke-width="1.5"
17
- stroke-linecap="round"
18
- stroke-linejoin="round" />
19
- <path d="M12 16h.01"
20
- stroke="currentColor"
21
- stroke-width="1.5"
22
- stroke-linecap="round"
23
- stroke-linejoin="round" />
24
- </svg>
25
-
26
- <div class="flex-1">
27
- <h1 class="text-2xl font-semibold text-red-600">
28
- Something went wrong
29
- </h1>
30
- <p class="mt-2 text-sm text-gray-600">
31
- We're sorry — an unexpected error occurred. Try reloading or return to
32
- the homepage.
33
- </p>
34
-
35
- <p class="mt-3 text-sm text-gray-700">{{ error_message }}</p>
36
-
37
- <div class="mt-4 flex flex-wrap gap-2">
38
- <a href="/"
39
- class="inline-block px-3 py-1.5 bg-gray-100 border border-gray-200 rounded text-sm text-gray-800 hover:bg-gray-200">Home</a>
40
- <button class="inline-block px-3 py-1.5 bg-blue-600 text-white rounded text-sm"
41
- onclick="reloadPage()">
42
- Reload
43
- </button>
44
- {% if error_trace %}
45
- <button id="toggleTraceBtn"
46
- class="inline-block px-3 py-1.5 bg-transparent border border-gray-200 rounded text-sm text-black"
47
- onclick="setShowTrace(showTrace => !showTrace)">
48
- {showTrace ? "Hide details" : "Show details"}
49
- </button>
50
- {% endif %}
51
- </div>
52
- </div>
53
- </div>
54
-
55
- {% if error_trace %}
56
- <div id="traceArea"
57
- class="mt-4"
58
- hidden="{!showTrace}">
59
- <div class="flex items-center justify-between mb-2">
60
- <div class="text-xs text-gray-500">Error details (soft copy)</div>
61
- <div class="flex gap-2">
62
- <button class="px-2 py-1 text-xs bg-gray-100 border rounded text-black"
63
- onclick="copyTrace()">
64
- {showCopyTrace ? "Copied" : "Copy"}
65
- </button>
66
- <button class="px-2 py-1 text-xs bg-gray-100 border rounded text-black"
67
- onclick="downloadTrace()">
68
- Download
69
- </button>
70
- </div>
71
- </div>
72
- <pre id="tracePre"
73
- class="p-4 bg-gray-100 rounded overflow-auto text-xs text-black">{{ error_trace }}</pre>
74
- </div>
75
- {% endif %}
76
- </div>
77
-
78
- <script>
79
- const [showTrace, setShowTrace] = pp.state(false);
80
- const [showCopyTrace, setShowCopyTrace] = pp.state(false);
81
-
82
- function reloadPage() {
83
- window.location.reload();
84
- }
85
-
86
- function copyTrace() {
87
- const tracePre = document.getElementById("tracePre");
88
- navigator.clipboard.writeText(tracePre.innerText);
89
-
90
- setShowCopyTrace(true);
91
- setTimeout(() => {
92
- setShowCopyTrace(false);
93
- }, 2000);
94
- }
95
-
96
- function downloadTrace() {
97
- const tracePre = document.getElementById("tracePre");
98
- const blob = new Blob([tracePre.innerText], {
99
- type: "text/plain",
100
- });
101
- const url = URL.createObjectURL(blob);
102
- const a = document.createElement("a");
103
- a.href = url;
104
- a.download = "error_trace.txt";
105
- document.body.appendChild(a);
106
- a.click();
107
- document.body.removeChild(a);
108
- URL.revokeObjectURL(url);
109
- }
110
- </script>
111
- </div>
@@ -1,26 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport"
7
- content="width=device-width, initial-scale=1.0" />
8
- <title>{{ metadata.title | default("Caspian App") }}</title>
9
- <meta name="description"
10
- content="{{ metadata.description | default('Powered by Caspian Framework') }}" />
11
- <link rel="icon"
12
- href="/favicon.ico"
13
- type="image/x-icon"
14
- sizes="16x16" />
15
- </head>
16
-
17
- <body style="
18
- opacity: 0;
19
- pointer-events: none;
20
- user-select: none;
21
- transition: opacity 0.18s ease-out;
22
- ">
23
- <slot />
24
- </body>
25
-
26
- </html>