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.
@@ -1,36 +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
- type ViewBodyBase<Props> = {
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> = {
5
13
  render: () => void;
6
14
  onMount?: () => void;
7
15
  shouldUpdate?: (nextProps: Props) => boolean;
8
16
  onUpdateBefore?: () => void;
9
17
  onUpdateAfter?: () => void;
10
- };
11
- export type ViewBody<Props, Api> = Api extends void ? ViewBodyBase<Props> : ViewBodyBase<Props> & {
12
- api: Api;
18
+ dispose?: () => void;
13
19
  };
14
20
  /** Slot callback — passed by the caller at the call site. */
15
21
  export type Slot = () => void;
16
22
  /** What callers may pass as a slot: a callback or a plain string (rendered as text). */
17
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;
18
29
  export type ViewFn<Props, Api> = {
19
30
  (ctx: {
20
31
  props: () => Props;
21
32
  use: Use;
22
33
  slot: Slot;
23
- }): ViewBody<Props, Api>;
34
+ ref: RefSetter<Api>;
35
+ }): ViewBody<Props>;
24
36
  [$primitive]?: string;
25
37
  };
26
- /** Resolves to the caller-facing props type. Allows `void` when Props is void or all-optional. */
27
- type ViewProps<Props> = Props extends void ? {
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 ? {
28
44
  key?: Key;
45
+ ref?: Ref<Api>;
29
46
  } | void : {} extends Props ? (Props & {
30
47
  key?: Key;
48
+ ref?: Ref<Api>;
31
49
  }) | void : Props & {
32
50
  key?: Key;
51
+ ref?: Ref<Api>;
33
52
  };
34
- 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;
35
54
  export type PublicView<Props, Api> = ReturnType<typeof view<Props, Api>>;
36
55
  export {};
@@ -19,6 +19,7 @@ export declare class HtmlRender implements IRender<HTMLElement | Text> {
19
19
  private findInsertionPoint;
20
20
  private setAttributes;
21
21
  private diffAttributes;
22
+ private diffEvents;
22
23
  private bindEvent;
23
24
  private unbindEvent;
24
25
  private setAttribute;
@@ -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
+ ```