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
package/docs/store.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Store
|
|
2
|
+
|
|
3
|
+
Creo's store system provides globally visible reactive data. A store is created outside views and can be read/written from any view via `use()`.
|
|
4
|
+
|
|
5
|
+
## Creating a store
|
|
6
|
+
|
|
7
|
+
Use `store.new(initial)` at module scope:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { store } from "creo";
|
|
11
|
+
|
|
12
|
+
const ThemeStore = store.new<"light" | "dark">("light");
|
|
13
|
+
const UserStore = store.new<{ name: string; role: string } | null>(null);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Store instances are typically defined at module scope and imported where needed.
|
|
17
|
+
|
|
18
|
+
## Reading from a view
|
|
19
|
+
|
|
20
|
+
Use `use(store)` inside a view function to subscribe and get a reactive accessor:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { view, div, text } from "creo";
|
|
24
|
+
|
|
25
|
+
const ThemedButton = view(({ use }) => {
|
|
26
|
+
const theme = use(ThemeStore); // re-renders when ThemeStore changes
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
render() {
|
|
30
|
+
button({ class: theme.get() === "dark" ? "btn-dark" : "btn-light" }, () => {
|
|
31
|
+
text("Click me");
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`use(store)` subscribes the view to changes. When the store value is updated via `.set()` or `.update()`, all subscribed views are scheduled for re-render. Subscriptions are automatically cleaned up when the view is disposed.
|
|
39
|
+
|
|
40
|
+
## Setting values
|
|
41
|
+
|
|
42
|
+
Call `.set()` or `.update()` on the store directly -- from anywhere, including outside views:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// From a handler
|
|
46
|
+
const toggle = () => {
|
|
47
|
+
const current = ThemeStore.get();
|
|
48
|
+
ThemeStore.set(current === "light" ? "dark" : "light");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// From outside any view
|
|
52
|
+
ThemeStore.set("dark");
|
|
53
|
+
|
|
54
|
+
// Using update
|
|
55
|
+
ThemeStore.update(current => current === "light" ? "dark" : "light");
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Complete example
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createApp, store, view, div, button, span, text, HtmlRender } from "creo";
|
|
62
|
+
|
|
63
|
+
// 1. Create the store
|
|
64
|
+
const CounterStore = store.new(0);
|
|
65
|
+
|
|
66
|
+
// 2. Writer component
|
|
67
|
+
const IncrementButton = view(({ use }) => {
|
|
68
|
+
const counter = use(CounterStore);
|
|
69
|
+
const increment = () => counter.update(n => n + 1);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
render() {
|
|
73
|
+
button({ on: { click: increment } }, () => { text("+1"); });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 3. Reader component
|
|
79
|
+
const DisplayCount = view(({ use }) => {
|
|
80
|
+
const counter = use(CounterStore);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
render() {
|
|
84
|
+
span({}, () => {
|
|
85
|
+
text(`Count: ${counter.get()}`);
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 4. App
|
|
92
|
+
const App = view(() => ({
|
|
93
|
+
render() {
|
|
94
|
+
div({}, () => {
|
|
95
|
+
IncrementButton();
|
|
96
|
+
DisplayCount();
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// 5. Mount
|
|
102
|
+
createApp(() => App(), new HtmlRender(document.getElementById("app")!)).mount();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Store vs State
|
|
106
|
+
|
|
107
|
+
Both store and local state use the same `use()` function and return the same `Reactive<T>` interface (`get`, `set`, `update`). The difference:
|
|
108
|
+
|
|
109
|
+
| | Store | State |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Created with | `store.new(initial)` | `use(initial)` inside a view |
|
|
112
|
+
| Scope | Global -- shared across views | Local -- private to the view |
|
|
113
|
+
| Setting from outside | `MyStore.set(value)` | Not possible |
|
|
114
|
+
| Subscribes view | Yes -- `use(store)` re-renders on change | Yes -- `use(value)` re-renders on change |
|
|
115
|
+
|
|
116
|
+
## Store type
|
|
117
|
+
|
|
118
|
+
The `Store<T>` class is exported for type annotations:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import type { Store } from "creo";
|
|
122
|
+
|
|
123
|
+
function resetStore<T>(s: Store<T>, value: T): void {
|
|
124
|
+
s.set(value);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Full API
|
|
129
|
+
|
|
130
|
+
| Method | Description |
|
|
131
|
+
|--------|-------------|
|
|
132
|
+
| `.get(): T` | Read the current value |
|
|
133
|
+
| `.set(value: T): void` | Set a new value, re-render all subscribers |
|
|
134
|
+
| `.update(fn: (current: T) => T): void` | Apply a sync transform, re-render subscribers |
|
|
135
|
+
| `.update(fn: (current: T) => Promise<T>): void` | Apply an async transform, re-render on resolve |
|
package/docs/view.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# view()
|
|
2
|
+
|
|
3
|
+
`view()` is the core API for defining components in Creo.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
function view<Props = void, Api = void>(
|
|
9
|
+
viewFn: ViewFn<Props, Api>
|
|
10
|
+
): (props: Props & { key?: Key }, slot?: Slot) => void;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When `Props` is `void` (no props), the returned function can be called with no arguments:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const App = view((ctx) => ({
|
|
17
|
+
render() { /* ... */ },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
App(); // no args needed
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ViewFn and context
|
|
24
|
+
|
|
25
|
+
The function passed to `view()` receives a **context object** (`ctx`) with three fields:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const MyComponent = view<{ title: string }>((ctx) => {
|
|
29
|
+
// ctx.props -- function that returns the current props object
|
|
30
|
+
// ctx.use -- factory to create reactive state or subscribe to stores
|
|
31
|
+
// ctx.children -- pre-collected PendingView[] from the parent slot
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
render() { /* ... */ },
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ctx.props
|
|
40
|
+
|
|
41
|
+
A function that returns the current props passed by the parent. Call `ctx.props()` to read:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const Label = view<{ text: string }>((ctx) => ({
|
|
45
|
+
render() {
|
|
46
|
+
span({}, () => { text(ctx.props().text); });
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Because `props` is a function, calling it always returns the latest values -- whether inside `render()`, lifecycle hooks, or event handlers:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const Good = view<{ title: string }>((ctx) => ({
|
|
55
|
+
render() { text(ctx.props().title); }, // always current
|
|
56
|
+
}));
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### ctx.use
|
|
60
|
+
|
|
61
|
+
A factory function to create reactive state or subscribe to stores. See [State](./state.md) and [Store](./store.md).
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const count = ctx.use(0); // Reactive<number> — local state
|
|
65
|
+
const items = ctx.use<string[]>([]); // Reactive<string[]> — local state
|
|
66
|
+
const theme = ctx.use(ThemeStore); // Reactive<string> — store subscription
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### ctx.children
|
|
70
|
+
|
|
71
|
+
An array of `PendingView` objects representing the children passed by the caller's slot. Pass `ctx.children` directly to a primitive's second argument to render them:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const Wrapper = view((ctx) => ({
|
|
75
|
+
render() {
|
|
76
|
+
div({ class: "wrapper" }, ctx.children);
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If the caller provided no slot, `ctx.children` is an empty array.
|
|
82
|
+
|
|
83
|
+
## ViewBody
|
|
84
|
+
|
|
85
|
+
The viewFn must return a `ViewBody` object. The only required field is `render`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
type ViewBody<Props, Api> = {
|
|
89
|
+
render: () => void;
|
|
90
|
+
mount?: {
|
|
91
|
+
before?: () => void;
|
|
92
|
+
after?: () => void;
|
|
93
|
+
};
|
|
94
|
+
update?: {
|
|
95
|
+
should?: (nextProps: Props) => boolean;
|
|
96
|
+
before?: () => void;
|
|
97
|
+
after?: () => void;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When `Api` is provided (not `void`), ViewBody also includes an `api` field. See [Exposing an API](#exposing-an-api) below.
|
|
103
|
+
|
|
104
|
+
### render()
|
|
105
|
+
|
|
106
|
+
Called on every render cycle. Inside `render()`, call primitives and child components imperatively. The order of calls defines the virtual DOM structure:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
render() {
|
|
110
|
+
div({ class: "header" }, () => {
|
|
111
|
+
h1({}, () => { text("Title"); });
|
|
112
|
+
});
|
|
113
|
+
div({ class: "body" }, () => {
|
|
114
|
+
text("Content");
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
All standard JavaScript control flow works inside render:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
render() {
|
|
123
|
+
if (isEditing.get()) {
|
|
124
|
+
input({ value: draft.get(), on: { input: handleInput } });
|
|
125
|
+
} else {
|
|
126
|
+
span({}, () => { text(value.get()); });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const item of items.get()) {
|
|
130
|
+
ListItem({ key: item.id, data: item });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Lifecycle hooks
|
|
136
|
+
|
|
137
|
+
See [Lifecycle](./lifecycle.md) for details on `mount` and `update`.
|
|
138
|
+
|
|
139
|
+
## Calling components
|
|
140
|
+
|
|
141
|
+
Components are called as functions. The first argument is props, the second is an optional slot:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// No props, no children
|
|
145
|
+
MyComponent();
|
|
146
|
+
|
|
147
|
+
// With props
|
|
148
|
+
Counter({ initial: 5 });
|
|
149
|
+
|
|
150
|
+
// With children (slot)
|
|
151
|
+
Card({}, () => {
|
|
152
|
+
text("Inside the card");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// With props and children
|
|
156
|
+
Section({ title: "Info" }, () => {
|
|
157
|
+
Paragraph({ text: "Details here" });
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Keys
|
|
162
|
+
|
|
163
|
+
Pass `key` in the props object to help reconciliation identify items across re-renders:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
for (const user of users.get()) {
|
|
167
|
+
UserCard({ key: user.id, name: user.name });
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Exposing an API via `ref`
|
|
172
|
+
|
|
173
|
+
Both primitives and composite views accept a `ref` prop on the call site, and
|
|
174
|
+
both fill it through the same `Ref<T>` machinery. The difference is on the
|
|
175
|
+
provider side: the renderer pushes the DOM `Element` into a primitive's ref;
|
|
176
|
+
inside a composite, you push an API object yourself by calling `ctx.ref(...)`.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { view, type RefObject } from "creo";
|
|
180
|
+
|
|
181
|
+
const TextInput = view<{ placeholder: string }, { focus: () => void }>(
|
|
182
|
+
({ props, ref }) => {
|
|
183
|
+
const inputRef: RefObject<HTMLInputElement> = { current: null };
|
|
184
|
+
ref({
|
|
185
|
+
focus: () => inputRef.current?.focus(),
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
render() {
|
|
189
|
+
input({ placeholder: props().placeholder, ref: inputRef });
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Consumer:
|
|
196
|
+
const myInput: RefObject<{ focus: () => void }> = { current: null };
|
|
197
|
+
TextInput({ placeholder: "Email", ref: myInput });
|
|
198
|
+
// After mount:
|
|
199
|
+
myInput.current?.focus();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`ref` works the same on primitives — `div({ ref })` populates `.current` (or
|
|
203
|
+
fires the callback) with the underlying `Element` after mount, and `null` on
|
|
204
|
+
unmount. The provider verb is `ctx.ref(...)` on a composite; the consumer
|
|
205
|
+
noun is `ref` in props on either side.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"docs"
|
|
16
19
|
],
|
|
17
20
|
"scripts": {
|
|
18
21
|
"build": "bun run build.ts",
|