creo 0.2.5 → 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/README.md +4 -2
- package/dist/functional/key.d.ts +0 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +293 -146
- package/dist/index.js.map +15 -15
- package/dist/internal/internal_view.d.ts +10 -1
- package/dist/public/primitive.d.ts +9 -10
- package/dist/public/primitives/primitives.d.ts +341 -111
- package/dist/public/state.d.ts +6 -0
- package/dist/public/view.d.ts +27 -12
- package/dist/render/html_render.d.ts +8 -0
- package/dist/render/render_interface.d.ts +14 -0
- package/dist/render/string_render.d.ts +0 -2
- 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
- package/dist/public/event_handle.d.ts +0 -32
- package/dist/render/canvas_render.d.ts +0 -1
- package/dist/render/stream_render.d.ts +0 -1
- package/dist/structures/indexed_list.d.ts +0 -46
- package/dist/structures/list.d.ts +0 -68
package/dist/public/view.d.ts
CHANGED
|
@@ -1,40 +1,55 @@
|
|
|
1
1
|
import type { Key } from "../functional/key";
|
|
2
|
+
import type { Maybe } from "../functional/maybe";
|
|
2
3
|
import type { Use } from "./state";
|
|
3
4
|
import type { $primitive } from "./primitive";
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
export type RefCallback<T> = (value: T | null) => void;
|
|
6
|
+
export type RefObject<T> = {
|
|
7
|
+
current: T | null;
|
|
8
|
+
};
|
|
9
|
+
export type Ref<T> = RefCallback<T> | RefObject<T>;
|
|
10
|
+
/** Apply a value (or null) to either ref shape. Engine + renderer share this. */
|
|
11
|
+
export declare function applyRef<T>(ref: Maybe<Ref<T>>, value: T | null): void;
|
|
12
|
+
export type ViewBody<Props> = {
|
|
11
13
|
render: () => void;
|
|
12
14
|
onMount?: () => void;
|
|
13
15
|
shouldUpdate?: (nextProps: Props) => boolean;
|
|
14
16
|
onUpdateBefore?: () => void;
|
|
15
17
|
onUpdateAfter?: () => void;
|
|
16
|
-
|
|
18
|
+
dispose?: () => void;
|
|
17
19
|
};
|
|
18
20
|
/** Slot callback — passed by the caller at the call site. */
|
|
19
21
|
export type Slot = () => void;
|
|
20
22
|
/** What callers may pass as a slot: a callback or a plain string (rendered as text). */
|
|
21
23
|
export type SlotContent = Slot | string;
|
|
24
|
+
/**
|
|
25
|
+
* Setter handed to the viewFn for publishing an api into the consumer's `ref`.
|
|
26
|
+
* Typically called once during the body. Subsequent calls overwrite.
|
|
27
|
+
*/
|
|
28
|
+
export type RefSetter<Api> = (value: Api) => void;
|
|
22
29
|
export type ViewFn<Props, Api> = {
|
|
23
30
|
(ctx: {
|
|
24
31
|
props: () => Props;
|
|
25
32
|
use: Use;
|
|
26
33
|
slot: Slot;
|
|
27
|
-
|
|
34
|
+
ref: RefSetter<Api>;
|
|
35
|
+
}): ViewBody<Props>;
|
|
28
36
|
[$primitive]?: string;
|
|
29
37
|
};
|
|
30
|
-
/**
|
|
31
|
-
type
|
|
38
|
+
/**
|
|
39
|
+
* Caller-facing props type. Allows `void` when Props is void or all-optional.
|
|
40
|
+
* `ref` is added alongside `key`; its element type is whatever the view
|
|
41
|
+
* publishes via `ctx.ref(...)`.
|
|
42
|
+
*/
|
|
43
|
+
type ViewProps<Props, Api> = Props extends void ? {
|
|
32
44
|
key?: Key;
|
|
45
|
+
ref?: Ref<Api>;
|
|
33
46
|
} | void : {} extends Props ? (Props & {
|
|
34
47
|
key?: Key;
|
|
48
|
+
ref?: Ref<Api>;
|
|
35
49
|
}) | void : Props & {
|
|
36
50
|
key?: Key;
|
|
51
|
+
ref?: Ref<Api>;
|
|
37
52
|
};
|
|
38
|
-
export declare function view<Props = void, Api = void>(body: ViewFn<Props, Api>): (props: ViewProps<Props>, slot?: SlotContent) => void;
|
|
53
|
+
export declare function view<Props = void, Api = void>(body: ViewFn<Props, Api>): (props: ViewProps<Props, Api>, slot?: SlotContent) => void;
|
|
39
54
|
export type PublicView<Props, Api> = ReturnType<typeof view<Props, Api>>;
|
|
40
55
|
export {};
|
|
@@ -8,10 +8,18 @@ export declare class HtmlRender implements IRender<HTMLElement | Text> {
|
|
|
8
8
|
constructor(container: HTMLElement);
|
|
9
9
|
render(view: ViewRecord): void;
|
|
10
10
|
unmount(view: ViewRecord): void;
|
|
11
|
+
/**
|
|
12
|
+
* Re-render only when a stateful DOM property in `nextProps` actually
|
|
13
|
+
* differs from the live DOM. With this in place a 50-input form whose
|
|
14
|
+
* parent re-renders touches the DOM only for the input the user is
|
|
15
|
+
* actually editing.
|
|
16
|
+
*/
|
|
17
|
+
shouldReassert(view: ViewRecord, nextProps: unknown): boolean;
|
|
11
18
|
private findParentDom;
|
|
12
19
|
private findInsertionPoint;
|
|
13
20
|
private setAttributes;
|
|
14
21
|
private diffAttributes;
|
|
22
|
+
private diffEvents;
|
|
15
23
|
private bindEvent;
|
|
16
24
|
private unbindEvent;
|
|
17
25
|
private setAttribute;
|
|
@@ -6,4 +6,18 @@ export interface IRender<Output> {
|
|
|
6
6
|
render(view: ViewRecord): void;
|
|
7
7
|
/** Remove a view's output artifacts. */
|
|
8
8
|
unmount(view: ViewRecord): void;
|
|
9
|
+
/**
|
|
10
|
+
* Optional: returns true if this primitive's live output may have
|
|
11
|
+
* drifted from `nextProps` and needs re-rendering even though
|
|
12
|
+
* `shallowEqual(prev, next)` reports props as unchanged.
|
|
13
|
+
*
|
|
14
|
+
* The DOM renderer uses this to detect user input that changed
|
|
15
|
+
* the live DOM (e.g. typing in `<input value=…>`, toggling a
|
|
16
|
+
* checkbox) without our state ever changing.
|
|
17
|
+
*
|
|
18
|
+
* Called only for primitives whose own props didn't change
|
|
19
|
+
* shallowly — engines should treat absence of this method as
|
|
20
|
+
* "nothing to re-assert".
|
|
21
|
+
*/
|
|
22
|
+
shouldReassert?(view: ViewRecord, nextProps: unknown): boolean;
|
|
9
23
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Create App
|
|
2
|
+
|
|
3
|
+
`creo-create-app` is a CLI that scaffolds a ready-to-run Creo project with Vite and, optionally, a [Hono](https://hono.dev) backend. Use it when you want a new project without wiring up the tooling yourself.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
<div class="pkg-tabs" data-pkg-tabs>
|
|
8
|
+
<div class="pkg-tabs-bar" role="tablist">
|
|
9
|
+
<button class="pkg-tab active" data-pkg="bun" role="tab">bun</button>
|
|
10
|
+
<button class="pkg-tab" data-pkg="npm" role="tab">npm</button>
|
|
11
|
+
<button class="pkg-tab" data-pkg="pnpm" role="tab">pnpm</button>
|
|
12
|
+
<button class="pkg-tab" data-pkg="yarn" role="tab">yarn</button>
|
|
13
|
+
</div>
|
|
14
|
+
<pre class="pkg-panel active" data-pkg="bun"><code>bunx creo-create-app my-app</code></pre>
|
|
15
|
+
<pre class="pkg-panel" data-pkg="npm"><code>npx creo-create-app my-app</code></pre>
|
|
16
|
+
<pre class="pkg-panel" data-pkg="pnpm"><code>pnpm dlx creo-create-app my-app</code></pre>
|
|
17
|
+
<pre class="pkg-panel" data-pkg="yarn"><code>yarn dlx creo-create-app my-app</code></pre>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
Omit the project name to be prompted for it:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bunx creo-create-app
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Interactive prompts
|
|
27
|
+
|
|
28
|
+
The CLI asks two questions:
|
|
29
|
+
|
|
30
|
+
1. **Project name** — becomes the directory name and `package.json` name. Skipped if you passed the name as an argument.
|
|
31
|
+
2. **Include a server (Hono)?** — adds a Hono backend that serves the built static files and exposes a sample `/api/health` endpoint.
|
|
32
|
+
|
|
33
|
+
## Generated layout
|
|
34
|
+
|
|
35
|
+
### Client-only (default)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
my-app/
|
|
39
|
+
├── package.json
|
|
40
|
+
├── tsconfig.json
|
|
41
|
+
├── vite.config.ts
|
|
42
|
+
├── index.html
|
|
43
|
+
├── .gitignore
|
|
44
|
+
└── src/
|
|
45
|
+
├── main.ts # Mounts the creo app
|
|
46
|
+
└── app.ts # Starter counter component
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Scripts:
|
|
50
|
+
|
|
51
|
+
| Script | Command | Description |
|
|
52
|
+
|--------|---------|-------------|
|
|
53
|
+
| `dev` | `vite` | Start dev server with HMR |
|
|
54
|
+
| `build` | `vite build` | Production build to `dist/` |
|
|
55
|
+
| `preview` | `vite preview` | Preview production build |
|
|
56
|
+
|
|
57
|
+
### With server (Hono)
|
|
58
|
+
|
|
59
|
+
Everything above, plus:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
my-app/
|
|
63
|
+
└── src/
|
|
64
|
+
└── server.ts # Hono server with static file serving
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Additional scripts:
|
|
68
|
+
|
|
69
|
+
| Script | Command | Description |
|
|
70
|
+
|--------|---------|-------------|
|
|
71
|
+
| `dev:server` | `bun run --watch src/server.ts` | Start Hono server with watch mode |
|
|
72
|
+
| `start` | `bun run src/server.ts` | Run Hono server in production |
|
|
73
|
+
|
|
74
|
+
During development you run both terminals. Vite proxies `/api/*` requests to the Hono server on `localhost:3000`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Terminal 1 — backend
|
|
78
|
+
bun run dev:server
|
|
79
|
+
|
|
80
|
+
# Terminal 2 — frontend
|
|
81
|
+
bun run dev
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For production:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun run build
|
|
88
|
+
bun run start
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Adding API routes
|
|
92
|
+
|
|
93
|
+
The generated `src/server.ts` is a normal Hono app. Add routes directly:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
app.get("/api/users", (c) => c.json([{ id: 1, name: "Alice" }]));
|
|
97
|
+
|
|
98
|
+
app.use("/*", async (c, next) => {
|
|
99
|
+
console.log(`${c.req.method} ${c.req.url}`);
|
|
100
|
+
await next();
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The server uses Bun's native HTTP via `export default { port, fetch }`, so nothing else is required to run it.
|
|
105
|
+
|
|
106
|
+
## Next steps
|
|
107
|
+
|
|
108
|
+
- Start editing `src/app.ts` — the starter counter is a normal Creo view.
|
|
109
|
+
- Read [Getting Started](#/getting-started) for the core API.
|
|
110
|
+
- Want to ship it? See [Host on Vercel](#/how-to/deploy-vercel).
|
package/docs/events.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
Creo collects event handlers under a single `on` prop on primitives.
|
|
4
|
+
|
|
5
|
+
## Declaring handlers
|
|
6
|
+
|
|
7
|
+
Define event handler functions in the view function body, before returning the ViewBody. Reference them in render:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { view, button, input, text } from "creo";
|
|
11
|
+
import type { PointerEventData, InputEventData } from "creo";
|
|
12
|
+
|
|
13
|
+
const MyForm = view((ctx) => {
|
|
14
|
+
const value = ctx.use("");
|
|
15
|
+
|
|
16
|
+
const handleClick = (e: PointerEventData) => {
|
|
17
|
+
console.log("Clicked at", e.x, e.y);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleInput = (e: InputEventData) => {
|
|
21
|
+
value.set(e.value);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
render() {
|
|
26
|
+
input({
|
|
27
|
+
value: value.get(),
|
|
28
|
+
placeholder: "Type here...",
|
|
29
|
+
on: { input: handleInput },
|
|
30
|
+
});
|
|
31
|
+
button({ on: { click: handleClick } }, () => { text("Submit"); });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Declaring handlers before `return` keeps them as stable references across re-renders and keeps the render function clean. Better still: if you keep the entire `on: { … }` object stable (declared once outside `render`), the renderer skips the per-event diff with a single reference check.
|
|
38
|
+
|
|
39
|
+
## The `on` prop
|
|
40
|
+
|
|
41
|
+
Event handlers live under a single `on` key. Event names are the camelCase form of the DOM event (no `on` prefix). This keeps the renderer free of per-prop event detection: it special-cases one key (`on`) instead of scanning every prop for an `on*` pattern.
|
|
42
|
+
|
|
43
|
+
| Event key | Fires on | Event data type |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `click` | Click / tap | `PointerEventData` |
|
|
46
|
+
| `dblclick` | Double click | `PointerEventData` |
|
|
47
|
+
| `pointerDown` | Pointer button pressed | `PointerEventData` |
|
|
48
|
+
| `pointerUp` | Pointer button released | `PointerEventData` |
|
|
49
|
+
| `pointerMove` | Pointer moved | `PointerEventData` |
|
|
50
|
+
| `keyDown` | Key pressed | `KeyEventData` |
|
|
51
|
+
| `keyUp` | Key released | `KeyEventData` |
|
|
52
|
+
| `focus` | Element focused | `FocusEventData` |
|
|
53
|
+
| `blur` | Element blurred | `FocusEventData` |
|
|
54
|
+
| `input` | Input value changed | `InputEventData` |
|
|
55
|
+
| `change` | Input value committed | `InputEventData` |
|
|
56
|
+
|
|
57
|
+
## Event data types
|
|
58
|
+
|
|
59
|
+
All event data types extend `BaseEventData`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type BaseEventData = {
|
|
63
|
+
stopPropagation: () => void;
|
|
64
|
+
preventDefault: () => void;
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### PointerEventData
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
type PointerEventData = BaseEventData & {
|
|
72
|
+
x: number; // clientX
|
|
73
|
+
y: number; // clientY
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Used by `click`, `dblclick`, `pointerDown`, `pointerUp`, `pointerMove`.
|
|
78
|
+
|
|
79
|
+
### KeyEventData
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
type KeyEventData = BaseEventData & {
|
|
83
|
+
key: string; // e.g. "Enter", "a", "Escape"
|
|
84
|
+
code: string; // e.g. "KeyA", "Enter", "Space"
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Used by `keyDown`, `keyUp`.
|
|
89
|
+
|
|
90
|
+
### InputEventData
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
type InputEventData = BaseEventData & {
|
|
94
|
+
value: string; // current input value
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Used by `input`, `change`.
|
|
99
|
+
|
|
100
|
+
### FocusEventData
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
type FocusEventData = BaseEventData;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Used by `focus`, `blur`. Contains only the base methods.
|
|
107
|
+
|
|
108
|
+
## Event type maps
|
|
109
|
+
|
|
110
|
+
Creo defines two event maps that determine which events a primitive supports:
|
|
111
|
+
|
|
112
|
+
### ContainerEvents
|
|
113
|
+
|
|
114
|
+
Applies to most HTML elements (`div`, `span`, `button`, `li`, etc.):
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
type ContainerEvents = {
|
|
118
|
+
click: (e: PointerEventData) => void;
|
|
119
|
+
dblclick: (e: PointerEventData) => void;
|
|
120
|
+
pointerDown: (e: PointerEventData) => void;
|
|
121
|
+
pointerUp: (e: PointerEventData) => void;
|
|
122
|
+
pointerMove: (e: PointerEventData) => void;
|
|
123
|
+
keyDown: (e: KeyEventData) => void;
|
|
124
|
+
keyUp: (e: KeyEventData) => void;
|
|
125
|
+
focus: (e: FocusEventData) => void;
|
|
126
|
+
blur: (e: FocusEventData) => void;
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### FormEvents
|
|
131
|
+
|
|
132
|
+
Extends `ContainerEvents` with input-specific events. Applies to `input`, `textarea`, `select`:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
type FormEvents = ContainerEvents & {
|
|
136
|
+
input: (e: InputEventData) => void;
|
|
137
|
+
change: (e: InputEventData) => void;
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
### Keyboard navigation
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const KeyNav = view((ctx) => {
|
|
147
|
+
const selected = ctx.use(0);
|
|
148
|
+
|
|
149
|
+
const handleKeyDown = (e: KeyEventData) => {
|
|
150
|
+
if (e.key === "ArrowDown") {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
selected.update(n => n + 1);
|
|
153
|
+
} else if (e.key === "ArrowUp") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
selected.update(n => Math.max(0, n - 1));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
render() {
|
|
161
|
+
div({ tabindex: 0, on: { keyDown: handleKeyDown } }, () => {
|
|
162
|
+
text(`Selected: ${selected.get()}`);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Controlled input
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const SearchBox = view((ctx) => {
|
|
173
|
+
const query = ctx.use("");
|
|
174
|
+
|
|
175
|
+
const handleInput = (e: InputEventData) => query.set(e.value);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
render() {
|
|
179
|
+
input({
|
|
180
|
+
value: query.get(),
|
|
181
|
+
placeholder: "Search...",
|
|
182
|
+
on: { input: handleInput },
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Preventing default behavior
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const NoContextMenu = view((ctx) => ({
|
|
193
|
+
render() {
|
|
194
|
+
div(
|
|
195
|
+
{
|
|
196
|
+
on: {
|
|
197
|
+
click: (e: PointerEventData) => {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
e.stopPropagation();
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
() => {
|
|
204
|
+
text("Right-click has no effect here");
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Stable `on` objects (optional optimization)
|
|
212
|
+
|
|
213
|
+
The renderer short-circuits the per-event diff when `prev.on === next.on`. Two patterns work:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Pattern A — declare the events object once, reuse it across renders.
|
|
217
|
+
const Counter = view(() => {
|
|
218
|
+
const inc = () => count.update(n => n + 1);
|
|
219
|
+
const events = { click: inc };
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
render() {
|
|
223
|
+
button({ class: "btn", on: events }, "+1");
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Pattern B — inline; the renderer diffs sub-keys, which is still cheap.
|
|
229
|
+
return {
|
|
230
|
+
render() {
|
|
231
|
+
button({ class: "btn", on: { click: inc } }, "+1");
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Both are correct. Pattern A skips the sub-diff entirely.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
<div class="pkg-tabs" data-pkg-tabs>
|
|
6
|
+
<div class="pkg-tabs-bar" role="tablist">
|
|
7
|
+
<button class="pkg-tab active" data-pkg="bun" role="tab">bun</button>
|
|
8
|
+
<button class="pkg-tab" data-pkg="npm" role="tab">npm</button>
|
|
9
|
+
<button class="pkg-tab" data-pkg="pnpm" role="tab">pnpm</button>
|
|
10
|
+
<button class="pkg-tab" data-pkg="yarn" role="tab">yarn</button>
|
|
11
|
+
</div>
|
|
12
|
+
<pre class="pkg-panel active" data-pkg="bun"><code>bun add creo</code></pre>
|
|
13
|
+
<pre class="pkg-panel" data-pkg="npm"><code>npm install creo</code></pre>
|
|
14
|
+
<pre class="pkg-panel" data-pkg="pnpm"><code>pnpm add creo</code></pre>
|
|
15
|
+
<pre class="pkg-panel" data-pkg="yarn"><code>yarn add creo</code></pre>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
Creo is a pure JavaScript/TypeScript package with no runtime dependencies. It ships typed for TypeScript 5+, but works just as well from plain JavaScript.
|
|
19
|
+
|
|
20
|
+
## Your first component
|
|
21
|
+
|
|
22
|
+
Components are created with `view()`. The function receives a **context object** (here named `ctx`) and returns a `ViewBody` — an object containing at minimum a `render()` function and, optionally, lifecycle hooks (`onMount`, `onUpdateAfter`, etc.).
|
|
23
|
+
|
|
24
|
+
The context is how your component reads inputs and creates state. It has three members:
|
|
25
|
+
|
|
26
|
+
- **`ctx.props()`** — a function that returns the current props. Always call it (don't destructure) so you read the latest values on every render.
|
|
27
|
+
- **`ctx.use(initial)`** — creates reactive local state, or subscribes to a global `store`. Must be called in the view body, not inside `render()`.
|
|
28
|
+
- **`ctx.slot`** — the children the parent passed in. Call it (`ctx.slot?.()`) to render them.
|
|
29
|
+
|
|
30
|
+
Most code destructures what it needs: `({ props, use, slot }) => ...`.
|
|
31
|
+
|
|
32
|
+
Create `src/app.ts` with a greeting component:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// src/app.ts
|
|
36
|
+
import { view, div } from "creo";
|
|
37
|
+
|
|
38
|
+
export const Greeting = view<{ name: string }>(({ props }) => {
|
|
39
|
+
return {
|
|
40
|
+
render() {
|
|
41
|
+
div({ class: "greeting" }, `Hello, ${props().name}!`);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When a primitive has a single string child, pass it as the slot directly — the engine wraps it as a text node automatically. No `text()` wrapper, no `() => {}` callback. Use a function slot only when you have multiple children or need structure.
|
|
48
|
+
|
|
49
|
+
This is a leaf component. Any component can compose others — the one you mount is called the **root**. Let's wrap `Greeting` in a root `App`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// src/app.ts (continued)
|
|
53
|
+
export const App = view(() => {
|
|
54
|
+
return {
|
|
55
|
+
render() {
|
|
56
|
+
Greeting({ name: "World" });
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Mounting the app
|
|
63
|
+
|
|
64
|
+
Now we have an `App` component — time to put it on the page. Every Creo app starts with `createApp`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/main.ts
|
|
68
|
+
import { createApp, HtmlRender } from "creo";
|
|
69
|
+
import { App } from "./app";
|
|
70
|
+
|
|
71
|
+
const el = document.getElementById("app")!; // <div id="app"></div> in index.html
|
|
72
|
+
createApp(() => App(), new HtmlRender(el)).mount();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`createApp` takes three arguments:
|
|
76
|
+
|
|
77
|
+
1. **A slot callback** (`() => void`) — you call your root component inside it, the same way you'd call a child component in any other render function.
|
|
78
|
+
2. **A renderer** — `HtmlRender` draws to the DOM. Other renderers produce JSON or HTML strings; see [Renderers](#/renderers).
|
|
79
|
+
3. **An optional options object** — for advanced settings like a custom scheduler (below).
|
|
80
|
+
|
|
81
|
+
Calling `.mount()` performs the first render. From then on, Creo re-renders automatically whenever reactive state used by the view changes.
|
|
82
|
+
|
|
83
|
+
> **Tip.** If you don't want to wire this up by hand, [`creo-create-app`](#/create-app) scaffolds exactly this file layout with Vite already configured.
|
|
84
|
+
|
|
85
|
+
### Custom scheduler
|
|
86
|
+
|
|
87
|
+
By default, Creo schedules re-renders via `queueMicrotask`. You can provide a custom scheduler. For example, `requestAnimationFrame` for visual updates:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
createApp(
|
|
91
|
+
() => App(),
|
|
92
|
+
new HtmlRender(document.getElementById("app")!),
|
|
93
|
+
{ scheduler: requestAnimationFrame },
|
|
94
|
+
).mount();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The scheduler receives a `() => void` callback and is responsible for calling it when the next render should happen.
|
|
98
|
+
|
|
99
|
+
## Adding state
|
|
100
|
+
|
|
101
|
+
Use `ctx.use` to create reactive values. Here's a counter that displays the current value and a button to increment it:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { view, div, button, _ } from "creo";
|
|
105
|
+
|
|
106
|
+
const Counter = view(({ use }) => {
|
|
107
|
+
const count = use(0);
|
|
108
|
+
const increment = () => count.update(n => n + 1);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
render() {
|
|
112
|
+
div(_, () => {
|
|
113
|
+
div(_, String(count.get()));
|
|
114
|
+
button({ on: { click: increment } }, "+1");
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Notice the two slot forms in play:
|
|
122
|
+
|
|
123
|
+
- The **outer `div`** has two children (the value and the button). Multiple children means a **function slot** — `() => { ... }` — where each primitive call adds a child to the parent.
|
|
124
|
+
- The **inner `div`** and the **`button`** each have just one string child, so they use the **string slot** form: `div(_, "...")`, `button({ on: { click } }, "+1")`. No `text()` call, no function wrapper.
|
|
125
|
+
|
|
126
|
+
As a rule: reach for a function slot when you need to render more than one thing or use control flow (`if`, `for`); use a string slot whenever a single piece of text is enough.
|
|
127
|
+
|
|
128
|
+
`use()` calls must appear in the body of the view function (before `return`), never inside `render()`. Call order must be stable across re-renders (same rule as React hooks).
|
|
129
|
+
|
|
130
|
+
## Composing components
|
|
131
|
+
|
|
132
|
+
Components accept an optional slot (children) as a second argument. The caller passes a string or `() => void`, and inside the view you receive it as `ctx.slot`:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { view, div, p, text, _ } from "creo";
|
|
136
|
+
|
|
137
|
+
const Card = view(({ slot }) => {
|
|
138
|
+
return {
|
|
139
|
+
render() {
|
|
140
|
+
div({ class: "card" }, slot);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Usage in a parent render:
|
|
146
|
+
Card(_, () => {
|
|
147
|
+
p(_, "Card content");
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
When a component does not need children, omit the second argument:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
Counter({ initial: 0 });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Full example
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { createApp, view, div, h1, ul, li, input, button, HtmlRender, _ } from "creo";
|
|
161
|
+
import type { InputEventData } from "creo";
|
|
162
|
+
|
|
163
|
+
const TodoApp = view(({ use }) => {
|
|
164
|
+
const items = use<string[]>([]);
|
|
165
|
+
const draft = use("");
|
|
166
|
+
|
|
167
|
+
const handleInput = (e: InputEventData) => draft.set(e.value);
|
|
168
|
+
const addItem = () => {
|
|
169
|
+
if (draft.get().trim()) {
|
|
170
|
+
items.update(list => [...list, draft.get().trim()]);
|
|
171
|
+
draft.set("");
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
render() {
|
|
177
|
+
div({ class: "todo-app" }, () => {
|
|
178
|
+
h1(_, "Todo");
|
|
179
|
+
div(_, () => {
|
|
180
|
+
input({
|
|
181
|
+
value: draft.get(),
|
|
182
|
+
placeholder: "New item...",
|
|
183
|
+
on: { input: handleInput },
|
|
184
|
+
});
|
|
185
|
+
button({ on: { click: addItem } }, "Add");
|
|
186
|
+
});
|
|
187
|
+
ul(_, () => {
|
|
188
|
+
for (const item of items.get()) {
|
|
189
|
+
li({ key: item }, item);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
createApp(
|
|
198
|
+
() => TodoApp(),
|
|
199
|
+
new HtmlRender(document.getElementById("app")!),
|
|
200
|
+
).mount();
|
|
201
|
+
```
|