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,173 @@
1
+ # Lifecycle
2
+
3
+ Creo provides explicit, named lifecycle hooks on the `ViewBody` object. There are no dependency arrays — each hook has a clear purpose and timing.
4
+
5
+ ## Overview
6
+
7
+ | Hook | When it runs |
8
+ |------|-------------|
9
+ | `onMount` | After the view's first render (DOM/output exists) |
10
+ | `shouldUpdate(nextProps)` | Before a re-render, to decide whether it should proceed |
11
+ | `onUpdateBefore` | Before each re-render (not called on first render) |
12
+ | `onUpdateAfter` | After each re-render (not called on first render) |
13
+
14
+ All hooks are optional properties on the object returned from a view function.
15
+
16
+ ## onMount
17
+
18
+ Runs after the view's first render and after all children have been rendered. The DOM/output exists at this point. Use it for post-mount side effects like focusing an element, starting timers, or fetching data:
19
+
20
+ ```ts
21
+ const ItemList = view(({ use }) => {
22
+ const data = use<string[]>([]);
23
+
24
+ return {
25
+ onMount() {
26
+ fetch("/api/items")
27
+ .then(r => r.json())
28
+ .then(items => data.set(items));
29
+ },
30
+ render() {
31
+ for (const item of data.get()) {
32
+ li(_, item);
33
+ }
34
+ },
35
+ };
36
+ });
37
+ ```
38
+
39
+ `onMount` callbacks are batched — all views that mounted in the same render loop have their `onMount` called after the entire tree is settled.
40
+
41
+ ## shouldUpdate
42
+
43
+ A predicate that decides whether the view should re-render when it receives new props. Return `true` to allow the render, `false` to skip it. This is equivalent to `React.memo`'s comparison function:
44
+
45
+ ```ts
46
+ const ExpensiveList = view<{ items: string[]; label: string }>(({ props }) => {
47
+ return {
48
+ shouldUpdate(nextProps) {
49
+ return nextProps.items !== props().items;
50
+ },
51
+ render() {
52
+ div(_, () => {
53
+ text(props().label);
54
+ for (const item of props().items) {
55
+ li(_, item);
56
+ }
57
+ });
58
+ },
59
+ };
60
+ });
61
+ ```
62
+
63
+ If `shouldUpdate` is not defined, Creo compares props by **shallow equality**. The view re-renders when its own props shallow-differ, when a subscribed `use()` value changes, or when the slot's structure changes — a parent re-render alone doesn't force a child re-render.
64
+
65
+ ## onUpdateBefore
66
+
67
+ Runs synchronously before each re-render (not on the first render). Use it for pre-render calculations or logging:
68
+
69
+ ```ts
70
+ const Animated = view(({ use }) => {
71
+ const value = use(0);
72
+ let prevValue = 0;
73
+
74
+ return {
75
+ onUpdateBefore() {
76
+ prevValue = value.get();
77
+ },
78
+ render() {
79
+ div({ class: prevValue !== value.get() ? "changed" : "" }, () => {
80
+ text(String(value.get()));
81
+ });
82
+ },
83
+ };
84
+ });
85
+ ```
86
+
87
+ ## onUpdateAfter
88
+
89
+ Runs synchronously after each re-render (not on the first render). The DOM/output reflects the new state:
90
+
91
+ ```ts
92
+ const WordCount = view<{ text: string }>(({ props, use }) => {
93
+ const count = use(0);
94
+
95
+ return {
96
+ onUpdateAfter() {
97
+ // Props just changed; derive a value from them and push it somewhere.
98
+ count.set(props().text.trim().split(/\s+/).filter(Boolean).length);
99
+ console.log(`re-rendered with ${count.get()} words`);
100
+ },
101
+ render() {
102
+ div(_, () => {
103
+ p(_, props().text);
104
+ p({ class: "meta" }, `${count.get()} words`);
105
+ });
106
+ },
107
+ };
108
+ });
109
+ ```
110
+
111
+ `onUpdateAfter` is for observing the result of a render — reading measurements, pushing metrics, scheduling follow-up work. It fires after DOM mutations are applied, so any reads you do see the latest layout.
112
+
113
+ ## Cleanup
114
+
115
+ When a view is removed from the tree (its parent no longer renders it), the view and all its descendants are disposed automatically during reconciliation. The engine calls the renderer's `unmount` to clean up output artifacts and removes the view from the dirty queue.
116
+
117
+ There is no dedicated unmount hook. For cleanup of resources (timers, subscriptions, event listeners) started in `onMount`, tie them to module-scoped stores or rely on `setInterval`/`setTimeout` completing on page unload. If you need guaranteed teardown, pair the resource with a reactive `store` and clear it from a parent view when the child is conditionally removed.
118
+
119
+ ```ts
120
+ const Poller = view(({ use }) => {
121
+ const data = use("");
122
+
123
+ return {
124
+ onMount() {
125
+ setInterval(() => {
126
+ fetch("/api/status")
127
+ .then(r => r.text())
128
+ .then(t => data.set(t));
129
+ }, 5000);
130
+ },
131
+ render() {
132
+ text(data.get());
133
+ },
134
+ };
135
+ });
136
+ ```
137
+
138
+ ## Complete example
139
+
140
+ ```ts
141
+ const Dashboard = view<{ userId: string }>(({ props, use }) => {
142
+ const profile = use<{ name: string } | null>(null);
143
+ let renderCount = 0;
144
+
145
+ return {
146
+ onMount() {
147
+ fetch(`/api/users/${props().userId}`)
148
+ .then(r => r.json())
149
+ .then(data => profile.set(data));
150
+ },
151
+ shouldUpdate(nextProps) {
152
+ return nextProps.userId !== props().userId;
153
+ },
154
+ onUpdateBefore() {
155
+ renderCount++;
156
+ console.log(`Re-render #${renderCount}`);
157
+ },
158
+ onUpdateAfter() {
159
+ console.log("Dashboard updated");
160
+ },
161
+ render() {
162
+ div({ class: "dashboard" }, () => {
163
+ const p = profile.get();
164
+ if (p) {
165
+ h1(_, p.name);
166
+ } else {
167
+ text("Loading...");
168
+ }
169
+ });
170
+ },
171
+ };
172
+ });
173
+ ```
@@ -0,0 +1,195 @@
1
+ # Primitives
2
+
3
+ Primitives are the leaf-level building blocks in Creo. They correspond to HTML elements and are called as functions inside `render()`.
4
+
5
+ ## Built-in HTML elements
6
+
7
+ Creo exports pre-defined primitives for all standard HTML elements. Import them directly:
8
+
9
+ ```ts
10
+ import { div, span, p, h1, button, input, text } from "creo";
11
+ ```
12
+
13
+ ### Calling primitives
14
+
15
+ Primitives accept two optional arguments:
16
+
17
+ 1. **Props** -- an object with HTML attributes and event handlers.
18
+ 2. **Slot** -- a `() => void` callback for child content, or a `PendingView[]` for passthrough children.
19
+
20
+ ```ts
21
+ // No props, no children
22
+ br();
23
+ hr();
24
+
25
+ // Props only
26
+ input({ type: "text", placeholder: "Enter name" });
27
+ img({ src: "/logo.png", alt: "Logo" });
28
+
29
+ // Props and children
30
+ div({ class: "card", id: "main" }, () => {
31
+ h1({}, () => { text("Title"); });
32
+ p({}, () => { text("Content goes here."); });
33
+ });
34
+
35
+ // Children only (pass undefined or omit props)
36
+ div({}, () => {
37
+ text("Hello");
38
+ });
39
+ ```
40
+
41
+ ### text()
42
+
43
+ `text()` renders a text node. It accepts a string or number:
44
+
45
+ ```ts
46
+ text("Hello, world");
47
+ text(42);
48
+ text(count.get());
49
+ ```
50
+
51
+ `text()` does not accept children.
52
+
53
+ ## Available elements
54
+
55
+ ### Layout / structural
56
+
57
+ `div`, `span`, `section`, `article`, `aside`, `nav`, `header`, `footer`, `main`
58
+
59
+ ### Sectioning
60
+
61
+ `address`, `hgroup`, `search`
62
+
63
+ ### Text / headings
64
+
65
+ `p`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `pre`, `code`, `em`, `strong`, `small`, `br`, `hr`, `blockquote`
66
+
67
+ ### Links and labels
68
+
69
+ `a` (with `href`, `target` attrs), `label` (with `for` attr)
70
+
71
+ ### Text semantics
72
+
73
+ `abbr`, `b`, `bdi`, `bdo`, `cite`, `data`, `dfn`, `i`, `kbd`, `mark`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `sub`, `sup`, `time`, `u`, `varEl`, `wbr`
74
+
75
+ ### Lists
76
+
77
+ `ul`, `ol`, `li`, `dl`, `dt`, `dd`
78
+
79
+ ### Tables
80
+
81
+ `table`, `thead`, `tbody`, `tfoot`, `tr`, `th` (with `colspan`, `rowspan`, `scope`), `td` (with `colspan`, `rowspan`), `caption`, `colgroup`, `col`
82
+
83
+ ### Forms
84
+
85
+ `form`, `button`, `input`, `textarea`, `select`, `option`, `fieldset`, `legend`, `datalist`, `optgroup`, `output`, `progress`, `meter`
86
+
87
+ Form elements like `input`, `textarea`, and `select` support `FormEvents` (includes `onInput`, `onChange`). Other elements use `ContainerEvents`.
88
+
89
+ ### Media
90
+
91
+ `img`, `video`, `audio`, `canvas`, `source`, `track`, `map`, `area`, `picture`
92
+
93
+ ### Embedded
94
+
95
+ `iframe`, `embed`, `object`, `portal`, `svg`
96
+
97
+ ### Interactive
98
+
99
+ `details` (with `open`), `summary`, `dialog` (with `open`), `menu`
100
+
101
+ ### Figure
102
+
103
+ `figure`, `figcaption`
104
+
105
+ ## HtmlAttrs
106
+
107
+ All built-in primitives share a common attribute base:
108
+
109
+ ```ts
110
+ type HtmlAttrs = {
111
+ class?: string;
112
+ id?: string;
113
+ style?: string;
114
+ title?: string;
115
+ tabindex?: number;
116
+ hidden?: boolean;
117
+ role?: string;
118
+ draggable?: boolean;
119
+ [attr: string]: unknown; // open index signature for any HTML attribute
120
+ };
121
+ ```
122
+
123
+ The open index signature means you can pass any attribute -- Creo will set it via `setAttribute`.
124
+
125
+ ## The html() factory
126
+
127
+ `html(tag)` creates a primitive for any HTML tag at runtime. All built-in primitives are created this way:
128
+
129
+ ```ts
130
+ import { html } from "creo";
131
+
132
+ // Create a custom element primitive
133
+ const myWidget = html("my-widget");
134
+
135
+ // Use it in render
136
+ myWidget({ class: "fancy" }, () => {
137
+ text("Inside custom element");
138
+ });
139
+ ```
140
+
141
+ You can specify custom attribute and event types via generics:
142
+
143
+ ```ts
144
+ const video = html<
145
+ HtmlAttrs & { src?: string; controls?: boolean; autoplay?: boolean },
146
+ ContainerEvents
147
+ >("video");
148
+ ```
149
+
150
+ `html()` caches primitives by tag name -- calling `html("div")` twice returns the same primitive.
151
+
152
+ ## The primitive() factory
153
+
154
+ For completely custom primitives (not backed by an HTML tag), use `primitive()`:
155
+
156
+ ```ts
157
+ import { primitive } from "creo";
158
+ import type { PrimitiveComponent } from "creo";
159
+
160
+ type CanvasAttrs = { width: number; height: number };
161
+ type CanvasEvents = { click: (e: PointerEventData) => void };
162
+
163
+ const myCanvas: PrimitiveComponent<CanvasAttrs, CanvasEvents> = primitive<CanvasAttrs, CanvasEvents>();
164
+ ```
165
+
166
+ Custom primitives need a render handler registered on the renderer to produce output. See [Renderers](./renderers.md) for details.
167
+
168
+ ## Passing children
169
+
170
+ Primitives accept children in two forms:
171
+
172
+ ### Slot callback
173
+
174
+ A `() => void` function called at the call site. The engine collects child calls made inside it:
175
+
176
+ ```ts
177
+ div({ class: "wrapper" }, () => {
178
+ p({}, () => { text("Hello"); });
179
+ span({}, () => { text("World"); });
180
+ });
181
+ ```
182
+
183
+ ### PendingView array (passthrough)
184
+
185
+ When a view receives `ctx.children` from its parent, it can pass that array directly as the second argument:
186
+
187
+ ```ts
188
+ const Card = view((ctx) => ({
189
+ render() {
190
+ div({ class: "card" }, ctx.children);
191
+ },
192
+ }));
193
+ ```
194
+
195
+ This avoids re-collecting children and preserves the parent's pending views directly.
@@ -0,0 +1,183 @@
1
+ # Renderers
2
+
3
+ Creo separates the component model from the output target. Renderers implement the `IRender` interface to translate the virtual DOM into a specific output format.
4
+
5
+ ## Built-in renderers
6
+
7
+ ### HtmlRender
8
+
9
+ Renders to the browser DOM. This is the primary renderer for web applications.
10
+
11
+ ```ts
12
+ import { createApp, HtmlRender } from "creo";
13
+
14
+ const container = document.getElementById("app")!;
15
+ createApp(() => App(), new HtmlRender(container)).mount();
16
+ ```
17
+
18
+ `HtmlRender` handles:
19
+ - Creating and updating DOM elements
20
+ - Attribute diffing (only changed attributes are touched)
21
+ - Event listener binding and cleanup
22
+ - DOM properties (`value`, `checked`, `selected`) set directly instead of via `setAttribute`
23
+ - Keyed reordering of child nodes
24
+ - Boolean attribute handling (`disabled`, `hidden`, etc.)
25
+ - `autofocus` support on mount
26
+
27
+ ### JsonRender
28
+
29
+ Produces a JSON tree representation of the UI. Useful for testing and serialization.
30
+
31
+ ```ts
32
+ import { createApp, JsonRender } from "creo";
33
+ import type { JsonNode } from "creo";
34
+
35
+ const renderer = new JsonRender();
36
+ createApp(() => App(), renderer).mount();
37
+
38
+ const tree: JsonNode | undefined = renderer.root;
39
+ // {
40
+ // type: "div",
41
+ // props: { class: "app" },
42
+ // children: [ ... ],
43
+ // key: undefined
44
+ // }
45
+ ```
46
+
47
+ The `JsonNode` type:
48
+
49
+ ```ts
50
+ type JsonNode = {
51
+ type: string;
52
+ props: Record<string, unknown>;
53
+ children: JsonNode[];
54
+ key?: string | number;
55
+ };
56
+ ```
57
+
58
+ ### StringRender
59
+
60
+ Produces an HTML string from the virtual DOM. Useful for server-side rendering.
61
+
62
+ ```ts
63
+ import { createApp, StringRender } from "creo";
64
+
65
+ const renderer = new StringRender();
66
+ createApp(() => App(), renderer).mount();
67
+
68
+ const html: string = renderer.renderToString();
69
+ // "<div><h1>Hello</h1><p>World</p></div>"
70
+ ```
71
+
72
+ `StringRender` is pull-based -- `render()` and `unmount()` are essentially no-ops. Call `renderToString()` to walk the virtual DOM and build the HTML string on demand.
73
+
74
+ ## The IRender interface
75
+
76
+ All renderers implement `IRender<Output>`:
77
+
78
+ ```ts
79
+ interface IRender<Output> {
80
+ /** Create output if view is new, or update if existing. */
81
+ render(view: BaseView): void;
82
+
83
+ /** Remove a view's output artifacts. Called on disposal. */
84
+ unmount(view: BaseView): void;
85
+
86
+ /** Register render handlers for custom primitive components. */
87
+ registerPrimitive(
88
+ entries: [PrimitiveComponent<any, any>, PrimitiveRenderHandler<Output>][],
89
+ ): void;
90
+ }
91
+ ```
92
+
93
+ The `Output` type parameter describes what each primitive produces (e.g., `HTMLElement | Text` for HtmlRender, `JsonNode` for JsonRender, `string` for StringRender).
94
+
95
+ ## Custom renderers
96
+
97
+ To create a custom renderer, implement `IRender<YourOutput>`:
98
+
99
+ ```ts
100
+ import type { IRender, PrimitiveRenderHandler } from "creo";
101
+ import type { PrimitiveComponent } from "creo";
102
+ import type { BaseView } from "creo"; // available via internal types
103
+
104
+ class CanvasRender implements IRender<void> {
105
+ private ctx: CanvasRenderingContext2D;
106
+
107
+ constructor(canvas: HTMLCanvasElement) {
108
+ this.ctx = canvas.getContext("2d")!;
109
+ }
110
+
111
+ render(view: BaseView): void {
112
+ // Draw or update based on view.props, view.renderRef, etc.
113
+ }
114
+
115
+ unmount(view: BaseView): void {
116
+ // Clean up any resources
117
+ view.renderRef = undefined;
118
+ }
119
+
120
+ registerPrimitive(
121
+ entries: [PrimitiveComponent<any, any>, PrimitiveRenderHandler<void>][],
122
+ ): void {
123
+ // Store handlers for custom primitives
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## Registering custom primitives
129
+
130
+ When you create a primitive with `primitive()` (not `html()`), you must register a render handler on the renderer so it knows how to produce output:
131
+
132
+ ```ts
133
+ import { primitive, createApp, HtmlRender } from "creo";
134
+
135
+ // Define a custom primitive
136
+ const sparkline = primitive<{ data: number[]; width: number; height: number }>();
137
+
138
+ // Create renderer and register the handler
139
+ const renderer = new HtmlRender(document.getElementById("app")!);
140
+ renderer.registerPrimitive([
141
+ [sparkline, {
142
+ render(view) {
143
+ const canvas = document.createElement("canvas");
144
+ canvas.width = view.props.width;
145
+ canvas.height = view.props.height;
146
+ // draw sparkline on canvas...
147
+ return canvas;
148
+ },
149
+ }],
150
+ ]);
151
+
152
+ // Now sparkline() can be used in render functions
153
+ createApp(() => App(), renderer).mount();
154
+ ```
155
+
156
+ The `PrimitiveRenderHandler<Output>` interface:
157
+
158
+ ```ts
159
+ interface PrimitiveRenderHandler<Output> {
160
+ render(view: BaseView): Output;
161
+ }
162
+ ```
163
+
164
+ ## Scheduler integration
165
+
166
+ The renderer is paired with a scheduler via `createApp` options. The scheduler controls when re-renders happen:
167
+
168
+ ```ts
169
+ // Default: queueMicrotask (immediate, within same task)
170
+ createApp(() => App(), new HtmlRender(el)).mount();
171
+
172
+ // requestAnimationFrame (synced to display refresh)
173
+ createApp(() => App(), new HtmlRender(el), {
174
+ scheduler: requestAnimationFrame,
175
+ }).mount();
176
+
177
+ // Custom scheduler
178
+ createApp(() => App(), new HtmlRender(el), {
179
+ scheduler: (cb) => setTimeout(cb, 16),
180
+ }).mount();
181
+ ```
182
+
183
+ The `Scheduler` type is `(callback: () => void) => void`.
package/docs/state.md ADDED
@@ -0,0 +1,131 @@
1
+ # State
2
+
3
+ Creo's state system provides reactive values that trigger re-renders when changed.
4
+
5
+ ## Creating state
6
+
7
+ Call `use(initial)` in the view function body (before `return`):
8
+
9
+ ```ts
10
+ import { view, text } from "creo";
11
+
12
+ const Counter = view(({ use }) => {
13
+ const count = use(0);
14
+ const name = use("untitled");
15
+ const items = use<string[]>([]);
16
+
17
+ return {
18
+ render() {
19
+ text(String(count.get()));
20
+ },
21
+ };
22
+ });
23
+ ```
24
+
25
+ ### Rules
26
+
27
+ - Call `use()` **before** returning the ViewBody, never inside `render()`.
28
+ - Call order must be stable across re-renders (same as React hooks). Do not call `use()` inside conditionals or loops.
29
+ - On the first render, `use(initial)` creates a new `Reactive<T>` instance. On subsequent renders, it returns the existing instance at the same position (the `initial` argument is ignored).
30
+
31
+ ## Reading state
32
+
33
+ Use `.get()` to read the current value:
34
+
35
+ ```ts
36
+ const count = use(0);
37
+
38
+ return {
39
+ render() {
40
+ text(String(count.get())); // reads current value
41
+ },
42
+ };
43
+ ```
44
+
45
+ `.get()` always returns the latest committed value, including any changes made by `.set()` or `.update()` earlier in the same cycle.
46
+
47
+ ## Setting state
48
+
49
+ ### .set(value)
50
+
51
+ Replace the current value and schedule a re-render:
52
+
53
+ ```ts
54
+ const name = use("Alice");
55
+
56
+ const rename = () => name.set("Bob");
57
+ ```
58
+
59
+ `.set()` applies the value **immediately** -- calling `.get()` right after `.set()` returns the new value. A re-render is then scheduled through the engine's scheduler.
60
+
61
+ ### .update(fn)
62
+
63
+ Apply a function to the current value:
64
+
65
+ ```ts
66
+ const count = use(0);
67
+
68
+ const increment = () => count.update(n => n + 1);
69
+ const decrement = () => count.update(n => n - 1);
70
+ ```
71
+
72
+ Like `.set()`, the update is applied immediately and a re-render is scheduled.
73
+
74
+ ### Async updates
75
+
76
+ `.update()` also accepts async functions. The value is applied and render is scheduled when the promise resolves:
77
+
78
+ ```ts
79
+ const data = use<string[]>([]);
80
+
81
+ const fetchData = () => data.update(async current => {
82
+ const response = await fetch("/api/items");
83
+ const items = await response.json();
84
+ return items;
85
+ });
86
+ ```
87
+
88
+ ## State vs plain variables
89
+
90
+ Use `use()` for values that should trigger re-renders when they change. For ephemeral values that do not affect the rendered output, a plain `let` variable is sufficient:
91
+
92
+ ```ts
93
+ const MyComponent = view(({ use }) => {
94
+ const count = use(0); // reactive -- triggers re-render on change
95
+ let lastClickTime = 0; // not reactive -- no re-render needed
96
+
97
+ const handleClick = () => {
98
+ lastClickTime = Date.now();
99
+ count.update(n => n + 1);
100
+ };
101
+
102
+ return {
103
+ render() {
104
+ button({ on: { click: handleClick } }, () => {
105
+ text(String(count.get()));
106
+ });
107
+ },
108
+ };
109
+ });
110
+ ```
111
+
112
+ ## The Reactive interface
113
+
114
+ Both `use(initial)` (local state) and `use(store)` (store binding) return `Reactive<T>`:
115
+
116
+ ```ts
117
+ import type { Reactive } from "creo";
118
+
119
+ function doubleReactive(r: Reactive<number>): void {
120
+ r.update(n => n * 2);
121
+ }
122
+ ```
123
+
124
+ ### Full API
125
+
126
+ | Method | Description |
127
+ |--------|-------------|
128
+ | `.get(): T` | Read the current value |
129
+ | `.set(value: T): void` | Set a new value immediately, schedule re-render |
130
+ | `.update(fn: (current: T) => T): void` | Apply a sync transform, schedule re-render |
131
+ | `.update(fn: (current: T) => Promise<T>): void` | Apply an async transform, schedule re-render on resolve |