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/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.5",
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",
@@ -1,32 +0,0 @@
1
- import type { Wildcard } from "../internal/wildcard";
2
- type EventCallback = (...args: Wildcard[]) => void;
3
- /**
4
- * Delegate that the renderer provides to wire event subscriptions
5
- * to the actual output (DOM addEventListener, etc.).
6
- * Set on the EventHandle after the view's output is created.
7
- */
8
- export type EventDelegate = {
9
- bind(event: string, callback: EventCallback, once?: boolean): void;
10
- unbind(event: string, callback: EventCallback): void;
11
- };
12
- /**
13
- * Handle returned when calling a primitive in the render stream.
14
- * Provides on/once/off for event subscription.
15
- *
16
- * When a delegate is set (by the renderer after mount), calls are
17
- * proxied to the renderer — only events the user subscribes to
18
- * get bound. Before the delegate exists, listeners are stored and
19
- * flushed once the delegate arrives.
20
- */
21
- export declare class EventHandle<Events> {
22
- #private;
23
- on<K extends keyof Events>(event: K, callback: Events[K]): void;
24
- once<K extends keyof Events>(event: K, callback: Events[K]): void;
25
- off<K extends keyof Events>(event: K, callback: Events[K]): void;
26
- /**
27
- * Called by the renderer after the view's output is created.
28
- * Flushes any listeners that were added before the delegate existed.
29
- */
30
- setDelegate(delegate: EventDelegate): void;
31
- }
32
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,46 +0,0 @@
1
- /**
2
- * IndexedList — linked list with O(1) identity-based lookup.
3
- *
4
- * Combines a doubly-linked list (O(1) insert/delete given a node)
5
- * with a Map<T, INode<T>> for O(1) lookup by value identity.
6
- *
7
- * All mutating operations are O(1):
8
- * push, delete, has, first, last, clear
9
- */
10
- import { type INode } from "./list";
11
- import type { Maybe } from "../functional/maybe";
12
- export declare class IndexedList<T> {
13
- #private;
14
- /** Append item to the end. No-op if already present. Returns the node. */
15
- push(item: T): INode<T>;
16
- /** Insert item at the front. No-op if already present. */
17
- unshift(item: T): void;
18
- /** Insert item after ref. No-op if item already present. */
19
- insertAfter(ref: T, item: T): void;
20
- /** Remove item. O(1). */
21
- delete(item: T): void;
22
- /** Check membership. O(1). */
23
- has(item: T): boolean;
24
- /** Number of items. */
25
- get length(): number;
26
- /** Get the first item (head). O(1). */
27
- first(): Maybe<T>;
28
- /** Get the last item (tail). O(1). */
29
- last(): Maybe<T>;
30
- /** Get the linked-list node for an item. O(1). */
31
- getNode(item: T): Maybe<INode<T>>;
32
- /** Positional access. O(n) — prefer getNode + getNext for traversal. */
33
- at(index: number): Maybe<T>;
34
- /**
35
- * Replace item at `pos`, or insert if nothing is there.
36
- * If `pos >= length`, appends to the end.
37
- * Returns the node.
38
- */
39
- upsert(pos: number, item: T): INode<T>;
40
- /** Swap two items in the list. O(1). No-op if either item is missing. */
41
- swap(a: T, b: T): void;
42
- /** Reset to empty. O(1). */
43
- clear(): void;
44
- /** Iterate values in insertion order. */
45
- [Symbol.iterator](): IterableIterator<T>;
46
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Linked list implementation
3
- */
4
- import type { Maybe } from "../functional/maybe";
5
- declare const $next: unique symbol;
6
- declare const $prev: unique symbol;
7
- declare const $owner: unique symbol;
8
- export interface INode<T> {
9
- insertNext(value: T): INode<T>;
10
- insertPrev(value: T): INode<T>;
11
- v: T;
12
- delete(): void;
13
- getNext(): Maybe<INode<T>>;
14
- getPrev(): Maybe<INode<T>>;
15
- isFirst(): boolean;
16
- isLast(): boolean;
17
- }
18
- export declare class ListNode<T> implements INode<T> {
19
- [$owner]: Maybe<IBaseContainer<T>>;
20
- [$next]: Maybe<ListNode<T>>;
21
- [$prev]: Maybe<ListNode<T>>;
22
- v: T;
23
- constructor(node: T, prev: Maybe<ListNode<T>>, next: Maybe<ListNode<T>>, list: IBaseContainer<T>);
24
- isFirst(): boolean;
25
- isLast(): boolean;
26
- delete(): void;
27
- clearFields(): void;
28
- insertNext(value: T): INode<T>;
29
- insertPrev(value: T): INode<T>;
30
- getNext(): Maybe<ListNode<T>>;
31
- getPrev(): Maybe<ListNode<T>>;
32
- getList(): Maybe<IBaseContainer<T>>;
33
- }
34
- interface IBaseContainer<T> {
35
- delete(node: INode<T>): void;
36
- insertNext(ref: INode<T>, value: T): INode<T>;
37
- insertPrev(ref: INode<T>, value: T): INode<T>;
38
- }
39
- export interface IList<T> extends Iterable<INode<T>>, IBaseContainer<T> {
40
- insertStart(value: T): INode<T>;
41
- insertEnd(value: T): INode<T>;
42
- at(n: number): Maybe<INode<T>>;
43
- first(): Maybe<INode<T>>;
44
- last(): Maybe<INode<T>>;
45
- readonly size: number;
46
- [Symbol.iterator](): IterableIterator<INode<T>>;
47
- }
48
- export declare class InternalList<T> implements IList<T> {
49
- #private;
50
- insertStart(value: T): ListNode<T>;
51
- delete(node: INode<T>): void;
52
- at(n: number): Maybe<ListNode<T>>;
53
- get size(): number;
54
- /** Reset the list to empty. O(1). */
55
- clear(): void;
56
- /** O(1) head access. */
57
- first(): Maybe<ListNode<T>>;
58
- /** O(1) tail access. */
59
- last(): Maybe<ListNode<T>>;
60
- insertEnd(value: T): ListNode<T>;
61
- insertNext(ref: INode<T>, value: T): ListNode<T>;
62
- insertPrev(ref: INode<T>, value: T): ListNode<T>;
63
- [Symbol.iterator](): Generator<ListNode<T>, void, unknown>;
64
- }
65
- export declare class List<T> extends InternalList<T> implements IList<T> {
66
- static from<T>(arrayLike: Iterable<T>): List<T>;
67
- }
68
- export {};