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.
@@ -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