creo 0.2.6 → 0.2.7
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/AGENTS.md +156 -0
- package/CHANGELOG.md +138 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +130 -87
- package/dist/index.js.map +10 -10
- package/dist/internal/internal_view.d.ts +6 -1
- package/dist/public/primitive.d.ts +9 -10
- package/dist/public/primitives/primitives.d.ts +341 -111
- package/dist/public/view.d.ts +27 -8
- package/dist/render/html_render.d.ts +1 -0
- package/docs/create-app.md +110 -0
- package/docs/events.md +236 -0
- package/docs/getting-started.md +201 -0
- package/docs/how-to/data-fetching.md +155 -0
- package/docs/how-to/deploy-vercel.md +130 -0
- package/docs/how-to/router.md +111 -0
- package/docs/how-to/styles.md +124 -0
- package/docs/how-to/suspense.md +116 -0
- package/docs/index.md +66 -0
- package/docs/lifecycle.md +173 -0
- package/docs/primitives.md +195 -0
- package/docs/renderers.md +183 -0
- package/docs/state.md +131 -0
- package/docs/store.md +135 -0
- package/docs/view.md +205 -0
- package/package.json +5 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Data Fetching
|
|
2
|
+
|
|
3
|
+
Creo has no built-in data-fetching library. The state primitive (`use()`) is enough — it supports async updates, and render reads the current value synchronously.
|
|
4
|
+
|
|
5
|
+
## The basic pattern
|
|
6
|
+
|
|
7
|
+
Track three things: the data, a loading flag, and an error.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const UserProfile = view<{ id: string }>(({ props, use }) => {
|
|
11
|
+
const user = use<User | null>(null);
|
|
12
|
+
const loading = use(false);
|
|
13
|
+
const error = use<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const load = async () => {
|
|
16
|
+
loading.set(true);
|
|
17
|
+
error.set(null);
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`/api/users/${props().id}`);
|
|
20
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
21
|
+
user.set(await res.json());
|
|
22
|
+
} catch (e) {
|
|
23
|
+
error.set((e as Error).message);
|
|
24
|
+
} finally {
|
|
25
|
+
loading.set(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
onMount: load,
|
|
31
|
+
render() {
|
|
32
|
+
if (loading.get()) return Spinner();
|
|
33
|
+
if (error.get()) return ErrorBanner({ message: error.get()! });
|
|
34
|
+
const u = user.get();
|
|
35
|
+
if (!u) return null;
|
|
36
|
+
UserCard({ user: u });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Using `update()` with async
|
|
43
|
+
|
|
44
|
+
`update()` accepts an async function and chains through pending updates safely:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const refresh = () => user.update(async (current) => {
|
|
48
|
+
const fresh = await api.getUser(current.id);
|
|
49
|
+
return fresh;
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Refetching on prop change
|
|
54
|
+
|
|
55
|
+
Use `shouldUpdate` combined with `onUpdateAfter` to refetch when a prop changes:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const UserProfile = view<{ id: string }>(({ props, use }) => {
|
|
59
|
+
let lastId = props().id;
|
|
60
|
+
const user = use<User | null>(null);
|
|
61
|
+
|
|
62
|
+
const load = async (id: string) => {
|
|
63
|
+
user.set(await api.getUser(id));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
onMount: () => load(lastId),
|
|
68
|
+
onUpdateAfter() {
|
|
69
|
+
if (props().id !== lastId) {
|
|
70
|
+
lastId = props().id;
|
|
71
|
+
load(lastId);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
render() { /* ... */ },
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Sharing data across views
|
|
80
|
+
|
|
81
|
+
For data used by many views, put it in a `store`. Views subscribe with `use(store)` and all re-render when the data updates.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { store } from "creo";
|
|
85
|
+
|
|
86
|
+
type UsersState = {
|
|
87
|
+
byId: Record<string, User>;
|
|
88
|
+
loading: Set<string>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const UsersStore = store.new<UsersState>({ byId: {}, loading: new Set() });
|
|
92
|
+
|
|
93
|
+
export async function loadUser(id: string) {
|
|
94
|
+
const state = UsersStore.get();
|
|
95
|
+
if (state.byId[id] || state.loading.has(id)) return;
|
|
96
|
+
|
|
97
|
+
UsersStore.update((s) => ({
|
|
98
|
+
...s,
|
|
99
|
+
loading: new Set([...s.loading, id]),
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const user = await api.getUser(id);
|
|
103
|
+
|
|
104
|
+
UsersStore.update((s) => {
|
|
105
|
+
const loading = new Set(s.loading);
|
|
106
|
+
loading.delete(id);
|
|
107
|
+
return {
|
|
108
|
+
byId: { ...s.byId, [id]: user },
|
|
109
|
+
loading,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Any view can now do `use(UsersStore)` and read `state.byId[id]` synchronously.
|
|
116
|
+
|
|
117
|
+
## Cancellation
|
|
118
|
+
|
|
119
|
+
For requests that may outlive the view (e.g., user navigates away), use an `AbortController`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const list = use<Item[]>([]);
|
|
123
|
+
let controller: AbortController | null = null;
|
|
124
|
+
|
|
125
|
+
const load = async () => {
|
|
126
|
+
controller?.abort();
|
|
127
|
+
controller = new AbortController();
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch("/api/items", { signal: controller.signal });
|
|
130
|
+
list.set(await res.json());
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if ((e as Error).name === "AbortError") return;
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Avoiding waterfalls
|
|
139
|
+
|
|
140
|
+
If two requests don't depend on each other, kick them off in parallel:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const load = async () => {
|
|
144
|
+
const [u, posts] = await Promise.all([
|
|
145
|
+
api.getUser(id),
|
|
146
|
+
api.getPosts(id),
|
|
147
|
+
]);
|
|
148
|
+
user.set(u);
|
|
149
|
+
postList.set(posts);
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## See also
|
|
154
|
+
|
|
155
|
+
- [Suspense pattern](#/how-to/suspense) — a helper view that wraps the loading/error/data dance into one component.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Host on Vercel
|
|
2
|
+
|
|
3
|
+
A Creo app built with [`creo-create-app`](#/create-app) is a standard Vite project, so Vercel deploys it with zero configuration. This recipe covers both variants:
|
|
4
|
+
|
|
5
|
+
- **Client-only** — static site, served from Vercel's edge.
|
|
6
|
+
- **With Hono server** — static frontend plus a serverless API function.
|
|
7
|
+
|
|
8
|
+
## Client-only (static)
|
|
9
|
+
|
|
10
|
+
### 1. Push the project to GitHub
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git init
|
|
14
|
+
git add .
|
|
15
|
+
git commit -m "init"
|
|
16
|
+
git remote add origin git@github.com:you/my-app.git
|
|
17
|
+
git push -u origin main
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Import it on Vercel
|
|
21
|
+
|
|
22
|
+
Go to [vercel.com/new](https://vercel.com/new), pick the repo, and accept the defaults. Vercel auto-detects Vite:
|
|
23
|
+
|
|
24
|
+
| Setting | Value |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Framework preset | **Vite** |
|
|
27
|
+
| Build command | `vite build` (or `bun run build`) |
|
|
28
|
+
| Output directory | `dist` |
|
|
29
|
+
| Install command | `bun install` (or your preferred package manager) |
|
|
30
|
+
|
|
31
|
+
No `vercel.json` needed.
|
|
32
|
+
|
|
33
|
+
### 3. (Optional) pin it in a config file
|
|
34
|
+
|
|
35
|
+
If you want the settings committed to the repo, add a `vercel.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"buildCommand": "bun run build",
|
|
40
|
+
"outputDirectory": "dist",
|
|
41
|
+
"installCommand": "bun install"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Hash routes work out of the box
|
|
46
|
+
|
|
47
|
+
`creo-router` is hash-based (`/#/about`), so every request is served `index.html` — no rewrite rules needed. If you later move to path-based routing, add this to `vercel.json` so deep links resolve:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## With Hono server (API routes)
|
|
56
|
+
|
|
57
|
+
The Hono server generated by `creo-create-app` runs as a Vercel **Serverless Function**. You keep Vite for the frontend and expose the Hono app under `/api/*`.
|
|
58
|
+
|
|
59
|
+
### 1. Install the Vercel adapter
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bun add @hono/node-server
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Create `api/[[...route]].ts`
|
|
66
|
+
|
|
67
|
+
Vercel treats files in `/api` as serverless functions. The `[[...route]]` catch-all forwards everything to your Hono app:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// api/[[...route]].ts
|
|
71
|
+
import { handle } from "hono/vercel";
|
|
72
|
+
import app from "../src/server";
|
|
73
|
+
|
|
74
|
+
export const config = { runtime: "nodejs" };
|
|
75
|
+
export default handle(app);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Make sure `src/server.ts` exports the Hono `app` as its default export:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/server.ts
|
|
82
|
+
import { Hono } from "hono";
|
|
83
|
+
|
|
84
|
+
const app = new Hono();
|
|
85
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
86
|
+
|
|
87
|
+
// Still runnable locally as a Bun server:
|
|
88
|
+
export default { port: 3000, fetch: app.fetch };
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **Tip.** Keep both exports — the `{ port, fetch }` default lets `bun run src/server.ts` keep working locally; Vercel uses the named `app` import.
|
|
92
|
+
|
|
93
|
+
### 3. Configure `vercel.json`
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"buildCommand": "bun run build",
|
|
98
|
+
"outputDirectory": "dist",
|
|
99
|
+
"installCommand": "bun install",
|
|
100
|
+
"rewrites": [
|
|
101
|
+
{ "source": "/api/:path*", "destination": "/api/[[...route]]" }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. Deploy
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
bunx vercel
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
On push, Vercel builds the frontend with Vite and deploys `api/[[...route]].ts` as a function. The static site and the API share one domain — no CORS, no proxy config.
|
|
113
|
+
|
|
114
|
+
## Environment variables
|
|
115
|
+
|
|
116
|
+
Set them in **Project Settings → Environment Variables**. In your code, read them the same way as any Vite / Node app:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Client (Vite inlines at build time):
|
|
120
|
+
const apiKey = import.meta.env.VITE_API_KEY;
|
|
121
|
+
|
|
122
|
+
// Server function:
|
|
123
|
+
const secret = process.env.SESSION_SECRET;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
- **404 on `/foo` after refresh** — you're on path-based routing without the SPA rewrite. Add the `rewrites` rule from the client-only section.
|
|
129
|
+
- **`Cannot find module 'hono/vercel'`** — install `@hono/node-server` (it ships the Vercel handler) and redeploy.
|
|
130
|
+
- **API routes hit `/api/[[...route]]` literally** — confirm the `rewrites` entry is present and that the filename uses **double square brackets**.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Router
|
|
2
|
+
|
|
3
|
+
Creo ships a separate package, [`creo-router`](https://www.npmjs.com/package/creo-router), that provides a minimal hash-based router built on top of the `store` primitive. It weighs a few hundred bytes gzipped.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add creo creo-router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
`createRouter` takes a list of routes and a fallback, and returns a bundle of tools: a route store, a `navigate` helper, a `RouterView` to render the matched view, and a `Link` that intercepts clicks.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createRouter } from "creo-router";
|
|
17
|
+
|
|
18
|
+
const { routeStore, navigate, RouterView, Link } = createRouter({
|
|
19
|
+
routes: [
|
|
20
|
+
{ path: "/", view: () => HomePage() },
|
|
21
|
+
{ path: "/about", view: () => AboutPage() },
|
|
22
|
+
{ path: "/users/:id", view: () => UserPage() },
|
|
23
|
+
],
|
|
24
|
+
fallback: () => NotFoundPage(),
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Routes support:
|
|
29
|
+
|
|
30
|
+
- Static segments: `/about`
|
|
31
|
+
- Dynamic params: `/users/:id`, read via `route.params.id`
|
|
32
|
+
|
|
33
|
+
## Rendering
|
|
34
|
+
|
|
35
|
+
Mount `RouterView()` wherever you want route content to appear — typically inside a layout view.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { _ } from "creo";
|
|
39
|
+
|
|
40
|
+
const App = view(() => ({
|
|
41
|
+
render() {
|
|
42
|
+
div({ class: "shell" }, () => {
|
|
43
|
+
nav(_, () => {
|
|
44
|
+
Link({ href: "/" }, "Home");
|
|
45
|
+
Link({ href: "/about" }, "About");
|
|
46
|
+
});
|
|
47
|
+
main(_, () => {
|
|
48
|
+
RouterView();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Reading route params
|
|
56
|
+
|
|
57
|
+
`routeStore` is a regular Creo store. Subscribe from any view with `use(routeStore)`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const UserPage = view(({ use }) => {
|
|
61
|
+
const route = use(routeStore);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
render() {
|
|
65
|
+
const { id } = route.get().params;
|
|
66
|
+
h1(_, `User ${id}`);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Programmatic navigation
|
|
73
|
+
|
|
74
|
+
`navigate(path)` updates the hash — the store reacts automatically, and any view subscribed to it re-renders.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const handleSave = async () => {
|
|
78
|
+
await api.save(form);
|
|
79
|
+
navigate("/success");
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Handling the back button
|
|
84
|
+
|
|
85
|
+
The browser's back/forward buttons fire `hashchange`, which the router listens to. No extra work needed.
|
|
86
|
+
|
|
87
|
+
## Active link styling
|
|
88
|
+
|
|
89
|
+
Since `Link` just renders an `<a>`, you can compare against `routeStore` to style it:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const NavLink = view<{ href: string }>(({ props, use, slot }) => {
|
|
93
|
+
const route = use(routeStore);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
render() {
|
|
97
|
+
const p = props();
|
|
98
|
+
const active = route.get().path === p.href;
|
|
99
|
+
Link(
|
|
100
|
+
{ href: p.href, class: active ? "nav-link active" : "nav-link" },
|
|
101
|
+
slot,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
|
|
110
|
+
- The router is **hash-based** (`#/path`). This means no server config is required — works on GitHub Pages, static hosts, and `file://` out of the box.
|
|
111
|
+
- All route changes go through the store, so they integrate with Creo's scheduler: a single render pass handles the route change and any state updates triggered by it.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Styles
|
|
2
|
+
|
|
3
|
+
Creo is framework-agnostic about styling. Every primitive accepts a `class` prop and a `style` string prop — pick whichever approach your project already uses.
|
|
4
|
+
|
|
5
|
+
## Global CSS
|
|
6
|
+
|
|
7
|
+
The simplest option: a plain `.css` file imported once from your entry module.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// main.ts
|
|
11
|
+
import "./styles.css";
|
|
12
|
+
import { createApp, HtmlRender } from "creo";
|
|
13
|
+
// ...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
/* styles.css */
|
|
18
|
+
.button {
|
|
19
|
+
padding: 8px 16px;
|
|
20
|
+
border-radius: 6px;
|
|
21
|
+
background: #4a90d9;
|
|
22
|
+
color: #fff;
|
|
23
|
+
}
|
|
24
|
+
.button:hover { background: #357ac2; }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
button({ class: "button", on: { click: handler } }, "Save");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Conditional classes
|
|
32
|
+
|
|
33
|
+
Plain string concatenation works — no helper needed:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const cls = "nav-link" + (active ? " active" : "");
|
|
37
|
+
a({ href, class: cls }, title);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For more complex cases, a tiny helper keeps the render clean:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const cx = (...xs: (string | false | null | undefined)[]) =>
|
|
44
|
+
xs.filter(Boolean).join(" ");
|
|
45
|
+
|
|
46
|
+
a({ class: cx("nav-link", active && "active", disabled && "is-disabled") }, title);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Inline styles
|
|
50
|
+
|
|
51
|
+
The `style` prop is a string — the same format as HTML's `style` attribute.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
div({ style: `width: ${width}px; color: ${color}` }, () => { /* ... */ });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For dynamic values that change often, inline styles are fine. For static design, prefer classes — they're cheaper to diff.
|
|
58
|
+
|
|
59
|
+
## CSS Modules
|
|
60
|
+
|
|
61
|
+
Vite supports CSS Modules out of the box. Name the file `*.module.css`:
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* Card.module.css */
|
|
65
|
+
.card {
|
|
66
|
+
padding: 16px;
|
|
67
|
+
border: 1px solid #eee;
|
|
68
|
+
}
|
|
69
|
+
.title { font-weight: 600; }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import styles from "./Card.module.css";
|
|
74
|
+
|
|
75
|
+
const Card = view<{ title: string }>(({ props, slot }) => ({
|
|
76
|
+
render() {
|
|
77
|
+
div({ class: styles.card }, () => {
|
|
78
|
+
h2({ class: styles.title }, props().title);
|
|
79
|
+
slot?.();
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `styles` object is keyed by the class names you wrote — the values are hashed and isolated per module.
|
|
86
|
+
|
|
87
|
+
## Tailwind
|
|
88
|
+
|
|
89
|
+
Works with no extra wiring. Install Tailwind, include its CSS, and pass utility strings to `class`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
button({ class: "px-4 py-2 rounded-md bg-blue-500 text-white hover:bg-blue-600" },
|
|
93
|
+
"Save");
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Scoped styles without a CSS toolchain
|
|
97
|
+
|
|
98
|
+
If you want per-component styles without a bundler plugin, declare them inside a `<style>` tag in your HTML shell with a naming convention (BEM or a short prefix).
|
|
99
|
+
|
|
100
|
+
## Dynamic class lists for keyed lists
|
|
101
|
+
|
|
102
|
+
When a class depends on reactive state, compute it in `render()` — it's just a string:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
for (const task of tasks.get()) {
|
|
106
|
+
li(
|
|
107
|
+
{ key: task.id, class: task.done ? "task done" : "task" },
|
|
108
|
+
task.title,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Creo diffs `class` as a single string — no array or object normalization needed.
|
|
114
|
+
|
|
115
|
+
## Setting arbitrary attributes
|
|
116
|
+
|
|
117
|
+
`HtmlAttrs` has an index signature, so you can pass `data-*`, `aria-*`, or any custom attribute directly:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
div(
|
|
121
|
+
{ class: "tab", role: "tab", "aria-selected": "true", "data-tab-id": id },
|
|
122
|
+
title,
|
|
123
|
+
);
|
|
124
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Suspense Pattern
|
|
2
|
+
|
|
3
|
+
Creo has no `<Suspense>` primitive and doesn't need one — you can compose the same behavior from a plain view. The goal: take an async loader, show a fallback while it runs, and render the data on success.
|
|
4
|
+
|
|
5
|
+
## A reusable `Suspense` view
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { view, div, _ } from "creo";
|
|
9
|
+
import type { SlotContent } from "creo";
|
|
10
|
+
|
|
11
|
+
type SuspenseProps<T> = {
|
|
12
|
+
load: () => Promise<T>;
|
|
13
|
+
children: (data: T) => void;
|
|
14
|
+
fallback?: SlotContent;
|
|
15
|
+
error?: (err: Error) => void;
|
|
16
|
+
key?: unknown; // pass a key to force reload when dependencies change
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Suspense = view(<T,>({ props, use }: any) => {
|
|
20
|
+
type Status = "loading" | "ok" | "error";
|
|
21
|
+
const status = use<Status>("loading");
|
|
22
|
+
const data = use<T | null>(null);
|
|
23
|
+
const err = use<Error | null>(null);
|
|
24
|
+
|
|
25
|
+
const run = async () => {
|
|
26
|
+
status.set("loading");
|
|
27
|
+
try {
|
|
28
|
+
const result = await props().load();
|
|
29
|
+
data.set(result);
|
|
30
|
+
status.set("ok");
|
|
31
|
+
} catch (e) {
|
|
32
|
+
err.set(e as Error);
|
|
33
|
+
status.set("error");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
onMount: run,
|
|
39
|
+
render() {
|
|
40
|
+
const p = props();
|
|
41
|
+
switch (status.get()) {
|
|
42
|
+
case "loading":
|
|
43
|
+
if (p.fallback) {
|
|
44
|
+
if (typeof p.fallback === "string") {
|
|
45
|
+
div({ class: "suspense-fallback" }, p.fallback);
|
|
46
|
+
} else {
|
|
47
|
+
p.fallback();
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
div({ class: "suspense-fallback" }, "Loading...");
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
case "error":
|
|
54
|
+
if (p.error) p.error(err.get()!);
|
|
55
|
+
else div({ class: "suspense-error" }, err.get()!.message);
|
|
56
|
+
return;
|
|
57
|
+
case "ok":
|
|
58
|
+
p.children(data.get()!);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const UserProfile = view<{ id: string }>(({ props }) => ({
|
|
70
|
+
render() {
|
|
71
|
+
Suspense({
|
|
72
|
+
key: props().id, // re-mount when id changes
|
|
73
|
+
load: () => fetch(`/api/users/${props().id}`).then(r => r.json()),
|
|
74
|
+
fallback: () => Spinner(),
|
|
75
|
+
error: (e) => ErrorBanner({ message: e.message }),
|
|
76
|
+
children: (user: User) => {
|
|
77
|
+
h1(_, user.name);
|
|
78
|
+
p(_, user.bio);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Why this is different from React Suspense
|
|
86
|
+
|
|
87
|
+
React's `<Suspense>` hooks into a special "throw a promise" protocol baked into the renderer. Creo keeps the model simpler: the async work happens in a view's lifecycle, and the status is ordinary state. You get:
|
|
88
|
+
|
|
89
|
+
- No special reconciler support needed.
|
|
90
|
+
- Full control over loading/error UI without wrapper gymnastics.
|
|
91
|
+
- No "use this hook only under a boundary" gotchas.
|
|
92
|
+
|
|
93
|
+
## Composing with `store`
|
|
94
|
+
|
|
95
|
+
If many views depend on the same resource, move the loading logic into a store and subscribe:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const user = use(UsersStore);
|
|
99
|
+
const loaded = user.get().byId[id];
|
|
100
|
+
|
|
101
|
+
if (!loaded) {
|
|
102
|
+
Spinner();
|
|
103
|
+
loadUser(id); // idempotent — checks in-flight set
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
UserCard({ user: loaded });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then `Suspense` is only useful for one-off async work — anything reused goes into a store.
|
|
111
|
+
|
|
112
|
+
## Caveats
|
|
113
|
+
|
|
114
|
+
- Don't call the `load` function from `render()` — it would fire a new request every re-render. Put it in `onMount()`.
|
|
115
|
+
- For cancellation, pair with `AbortController` (see [data fetching](#/how-to/data-fetching)).
|
|
116
|
+
- If `load` throws synchronously, the error path still works — `await` converts synchronous throws into a rejected promise.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Creo UI Framework
|
|
2
|
+
|
|
3
|
+
Creo is a lightweight, imperative UI framework for building reactive interfaces in TypeScript.
|
|
4
|
+
|
|
5
|
+
## What is Creo?
|
|
6
|
+
|
|
7
|
+
Creo takes a different approach from JSX-based frameworks. Instead of describing UI as a tree of elements returned from render functions, Creo uses **imperative render streams** -- you call primitives as functions, and the framework builds the virtual DOM from those calls.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { view, div, button, text } from "creo";
|
|
11
|
+
|
|
12
|
+
const App = view((ctx) => {
|
|
13
|
+
return {
|
|
14
|
+
render() {
|
|
15
|
+
div({ class: "app" }, () => {
|
|
16
|
+
h1({}, () => { text("Hello, Creo"); });
|
|
17
|
+
p({}, () => { text("An imperative UI framework."); });
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why Creo?
|
|
25
|
+
|
|
26
|
+
- **No JSX, no compiler.** Render functions are plain TypeScript. All control flow (if/else, for loops, ternaries) works naturally.
|
|
27
|
+
- **Immediate state.** Calling `.set()` or `.update()` applies the value instantly. No stale closures, no batching surprises.
|
|
28
|
+
- **Explicit lifecycle.** Named hooks (`mount.before`, `mount.after`, `update.before`, `update.after`) replace dependency-array guessing.
|
|
29
|
+
- **Renderer-agnostic.** The same component tree can target the DOM (`HtmlRender`), a JSON structure (`JsonRender`), or an HTML string (`StringRender`). Write your own renderer by implementing the `IRender` interface.
|
|
30
|
+
- **Lightweight.** No virtual DOM diffing library, no template compiler. Reconciliation is built into the engine with keyed and positional matching.
|
|
31
|
+
|
|
32
|
+
## Quick taste
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createApp, view, div, button, text, HtmlRender } from "creo";
|
|
36
|
+
|
|
37
|
+
const Counter = view<{ initial: number }>(({ props, use }) => {
|
|
38
|
+
const count = use(props().initial);
|
|
39
|
+
const increment = () => count.update(n => n + 1);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
render() {
|
|
43
|
+
div({}, () => {
|
|
44
|
+
text(count.get());
|
|
45
|
+
button({ on: { click: increment } }, () => { text("+1"); });
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
createApp(
|
|
52
|
+
() => Counter({ initial: 0 }),
|
|
53
|
+
new HtmlRender(document.getElementById("app")!),
|
|
54
|
+
).mount();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
- [Getting Started](./getting-started.md) -- installation, first app
|
|
60
|
+
- [view()](./view.md) -- defining components
|
|
61
|
+
- [State](./state.md) -- reactive state management
|
|
62
|
+
- [Events](./events.md) -- handling user interactions
|
|
63
|
+
- [Primitives](./primitives.md) -- built-in HTML elements and custom primitives
|
|
64
|
+
- [Store](./store.md) -- global/shared state (context pattern)
|
|
65
|
+
- [Renderers](./renderers.md) -- HtmlRender, JsonRender, StringRender, custom renderers
|
|
66
|
+
- [Lifecycle](./lifecycle.md) -- mount, update, and disposal hooks
|