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 ADDED
@@ -0,0 +1,156 @@
1
+ # Agent Guide for Creo
2
+
3
+ ## What is Creo?
4
+
5
+ Creo is an imperative UI framework with virtual DOM. No JSX — views are function calls (`div()`, `text()`, `span()`). State is reactive (`use()`, `store`). Reconciliation uses Vue 3-style keyed diffing with LIS.
6
+
7
+ ## Project Layout
8
+
9
+ ```
10
+ packages/creo/ — Core creo framework
11
+ src/internal/engine.ts — Core: reconciler, dirty queue, render loop
12
+ src/internal/internal_view.ts — ViewRecord type, flags, structural comparison
13
+ src/internal/orchestrator.ts — Tracks current active engine
14
+ src/public/view.ts — view() factory, ViewBody, Slot types
15
+ src/public/state.ts — State, Reactive, Use types
16
+ src/public/store.ts — Store (global reactive state)
17
+ src/public/app.ts — createApp() entry point
18
+ src/public/primitives/ — HTML element factories (div, span, text, etc.)
19
+ src/public/primitive.ts — $primitive symbol, EventHandlerProps
20
+ src/render/html_render.ts — DOM renderer (event delegation, attribute diffing)
21
+ src/render/json_render.ts — JSON AST renderer (testing)
22
+ src/render/string_render.ts — HTML string renderer (SSR)
23
+ src/render/render_interface.ts — IRender<Output> interface
24
+ src/functional/lis.ts — Longest Increasing Subsequence (keyed reconciliation)
25
+ src/functional/shallow_equal.ts — Object comparison (no Object.keys allocation)
26
+ packages/creo-router/ — Hash-based router (separate package)
27
+ packages/creo-create-app/ — CLI scaffolding tool
28
+ packages/creo-create-tauri-app/ — CLI scaffolding tool (Tauri)
29
+ scripts/version.ts — Version bump orchestrator
30
+ scripts/publish.ts — Build + publish orchestrator
31
+ ```
32
+
33
+ ## Key Architecture Decisions
34
+
35
+ ### ViewRecord is a plain object (not pooled)
36
+ Views are GC'd objects with direct parent pointers (`view.parent: Maybe<ViewRecord>`). Children are arrays (`view.children: Maybe<ViewRecord[]>`). No arena allocator, no numeric IDs, no linked lists.
37
+
38
+ ### Composites have no DOM footprint
39
+ Unlike React (which doesn't either) or the old creo (which used comment nodes), composite views set `renderRef = true` as a mounted marker. No DOM nodes are created for composites.
40
+
41
+ ### Primitives store `{ element, prevProps }` as renderRef
42
+ The `kind` discriminant was removed. Use `view.flags & F_PRIMITIVE` to distinguish primitives from composites.
43
+
44
+ ### Slot children (sc) are pre-collected
45
+ When a view has a slot, `newView()` calls `#collect(slot, [], res)` to eagerly evaluate the slot and store results in `view.sc`. This enables structural comparison (`hasScStructuralChange`) without re-running the slot.
46
+
47
+ ### scHost optimization
48
+ Composites that receive a slot track `view.scHost` — the primitive whose `.children` contains the live slot children. When slot children have prop changes but identical structure, `#propagateScProps` updates live children directly, skipping the composite's reconcile.
49
+
50
+ ### preCollectedSc
51
+ `#patchOrReplace` passes `pendView.sc` to `nextProps` as `preCollectedSc`, avoiding double slot evaluation. The sc items get re-parented to the old view.
52
+
53
+ ### Event delegation
54
+ HTML renderer uses Inferno-style delegation: one listener per event type on the container element. Handlers are stored on elements via `Symbol.for("creo.ev")`. Zero closures per element — handler mapping happens at dispatch time.
55
+
56
+ ### textContent shortcut
57
+ When a primitive has a single text child, the renderer sets `element.textContent` directly instead of creating a Text node. The text child gets `F_TEXT_CONTENT` flag.
58
+
59
+ ### Dispose optimization
60
+ When a primitive with DOM is disposed, its children go through `#disposeVirtual` (skips `renderer.unmount`) since removing the parent element removes all descendants from DOM automatically.
61
+
62
+ ## Flags (internal_view.ts)
63
+
64
+ ```
65
+ F_PENDING = 1 — View not yet initialized (body is null)
66
+ F_DIRTY = 1 << 1 — Needs reconcile + render
67
+ F_MOVED = 1 << 2 — Position changed, needs DOM repositioning
68
+ F_PRIMITIVE = 1 << 3 — Is an HTML element/text, not a composite
69
+ F_TEXT_CONTENT = 1 << 4 — Text rendered via parent's textContent
70
+ ```
71
+
72
+ ## Render Loop Flow
73
+
74
+ ```
75
+ markDirty(view) → add to #dirtyQueue Set → schedule()
76
+
77
+ render() processes #dirtyQueue:
78
+ for each view in Set (visits items added during iteration):
79
+ 1. initViewBody(view) — if F_PENDING, call viewFn once
80
+ 2. isNew = !view.renderRef
81
+ 3. if F_DIRTY:
82
+ a. reconcile(view) — collect pending children, diff with old
83
+ b. renderer.render(view) — mount or update DOM
84
+ c. clear F_DIRTY, F_MOVED
85
+ 4. else (F_MOVED only):
86
+ a. renderer.render(view) — reposition DOM
87
+ b. clear F_MOVED
88
+ #dirtyQueue.clear()
89
+ fire deferred callbacks (onMount, onUpdateAfter)
90
+ ```
91
+
92
+ ## Reconciliation Algorithm
93
+
94
+ ### Non-keyed
95
+ Position-based: `old[i]` paired with `pending[i]`. Same viewFn → `nextProps`. Different → dispose + init. Extras disposed or mounted.
96
+
97
+ ### Keyed (Vue 3-style)
98
+ 1. **Head sync** — match from start while keys equal
99
+ 2. **Tail sync** — match from end while keys equal
100
+ 3. **Simple cases** — pure insertion or pure removal
101
+ 4. **Middle diff** — build `newKeyToIndex` map, compute `newIdxToOldIdx`, find LIS. Views in LIS stay; others get `markMoved`.
102
+
103
+ ## Testing
104
+
105
+ ```bash
106
+ bun test packages/creo/src/ # All tests
107
+ bun test packages/creo/src/render/render.spec.ts # Renderer + state tests
108
+ bun test packages/creo/src/internal/ # Virtual DOM correctness
109
+ bun test packages/creo/src/render/benchmark.spec.ts # Performance benchmarks
110
+ ```
111
+
112
+ Tests use happy-dom for DOM simulation. The benchmark tests validate correctness of create/update/swap/select/remove/clear/replace operations on 1000-row tables.
113
+
114
+ ## Common Patterns When Modifying
115
+
116
+ ### Adding a new primitive
117
+ Add to `packages/creo/src/public/primitives/primitives.ts`:
118
+ ```ts
119
+ export const myTag = html<HtmlAttrs & { myProp?: string }, ContainerEvents>("my-tag");
120
+ ```
121
+ Export from `packages/creo/src/index.ts`.
122
+
123
+ ### Changing the engine
124
+ - `packages/creo/src/internal/engine.ts` is the core — reconciler, dirty queue, render loop
125
+ - After changes, run `bun test packages/creo/src/` (all tests must pass)
126
+ - Test with `cd docs && bun run dev` and open the playground recipes (simple-todo, advanced-todo, table, chess) for visual verification
127
+
128
+ ### Changing the HTML renderer
129
+ - `html_render.ts` — DOM operations, event delegation, attribute diffing
130
+ - `findParentDom(view)` walks parent chain for nearest primitive element
131
+ - `findInsertionPoint(view)` scans siblings + recurses up for composites
132
+ - After changes, verify with both todo and router examples
133
+
134
+ ## Conventions
135
+
136
+ ### `_` for Empty Props
137
+ Always use `_` (from `@/functional/maybe`, re-exported by `creo`) instead of `{}` when a primitive or view needs no props:
138
+ ```ts
139
+ div(_, () => { ... }); // not div({}, () => { ... })
140
+ h1(_, "Title"); // not h1({}, "Title")
141
+ ```
142
+
143
+ ### Inline Strings
144
+ Prefer passing strings directly as slots instead of wrapping in `() => text(...)`. The engine auto-wraps strings into text nodes:
145
+ ```ts
146
+ button({ on: { click: handler } }, "Click me"); // not () => text("Click me")
147
+ li(_, "Item text"); // not () => text("Item text")
148
+ span({ class: "label" }, title); // string variable works too
149
+ ```
150
+ Use `text()` only for dynamic values or when mixing text with other elements inside a function slot.
151
+
152
+ ### Changing ViewRecord shape
153
+ - Update type in `internal_view.ts`
154
+ - Update `newView()` in `engine.ts` (initializes all fields)
155
+ - Update `#disposeVirtual()` if new field needs cleanup
156
+ - JSON/String renderers may need updates if they access the field
package/CHANGELOG.md ADDED
@@ -0,0 +1,138 @@
1
+ # 0.2.7
2
+ 1. Add support for new api `dispose`
3
+ 2. Add `ref` to primitives (DOM element) and composite views (exposed API)
4
+ 3. Provider-side rename: `ctx.expose(...)` is now `ctx.ref(...)`, matching the consumer-side `ref` prop. `Expose<Api>` is now `RefSetter<Api>`.
5
+ 4. Event handlers move from flat `on*` props to a nested `on: { ... }` object — e.g. `button({ on: { click: handler } })`. Removes the per-prop `isEventProp` charCode scan from the renderer's hot path; the `on` object's reference identity short-circuits the event diff. `EventHandlerProps<>` is removed.
6
+ 5. Drop `mouseEnter` / `mouseLeave` — use `pointerEnter` / `pointerLeave`. PointerEvents already cover mouse, touch, and pen via the `pointerType` field, so the duplicate mouse-only events were dead weight.
7
+
8
+ ## Migration from 0.2.6
9
+
10
+ ### Event handlers: flat `on*` props → nested `on: { ... }` object
11
+
12
+ Event names lose the `on` prefix and the leading-capital — they become the camelCase form already used in `ContainerEvents` / `FormEvents` / `MediaEvents` (`click`, `pointerDown`, `keyDown`, `volumeChange`, …).
13
+
14
+ ```ts
15
+ // Before
16
+ button({ class: "btn", onClick: handleClick }, "Save");
17
+ input({
18
+ value: v.get(),
19
+ onInput: handleInput,
20
+ onKeyDown: handleKey,
21
+ onBlur: handleBlur,
22
+ });
23
+ details({ onToggle: (e) => log(e.open) }, () => summary(_, "more"));
24
+
25
+ // After
26
+ button({ class: "btn", on: { click: handleClick } }, "Save");
27
+ input({
28
+ value: v.get(),
29
+ on: { input: handleInput, keyDown: handleKey, blur: handleBlur },
30
+ });
31
+ details({ on: { toggle: (e) => log(e.open) } }, () => summary(_, "more"));
32
+ ```
33
+
34
+ Mapping table for every prop the framework defines:
35
+
36
+ | Before | After (key inside `on: { ... }`) |
37
+ | ------------- | -------------------------------- |
38
+ | `onClick` | `click` |
39
+ | `onDblclick` | `dblclick` |
40
+ | `onPointerDown` | `pointerDown` |
41
+ | `onPointerUp` | `pointerUp` |
42
+ | `onPointerMove` | `pointerMove` |
43
+ | `onPointerCancel` | `pointerCancel` |
44
+ | `onPointerEnter` | `pointerEnter` |
45
+ | `onPointerLeave` | `pointerLeave` |
46
+ | `onMouseEnter` | use `pointerEnter` |
47
+ | `onMouseLeave` | use `pointerLeave` |
48
+ | `onKeyDown` | `keyDown` |
49
+ | `onKeyUp` | `keyUp` |
50
+ | `onFocus` | `focus` |
51
+ | `onBlur` | `blur` |
52
+ | `onInput` | `input` |
53
+ | `onChange` | `change` |
54
+ | `onScroll` | `scroll` |
55
+ | `onLoad` | `load` |
56
+ | `onError` | `error` |
57
+ | `onToggle` | `toggle` |
58
+ | `onVolumeChange` | `volumeChange` |
59
+ | `onPlay` / `onPause` / `onEnded` | `play` / `pause` / `ended` |
60
+ | `onTimeUpdate` | `timeUpdate` |
61
+ | `onLoadedMetadata` | `loadedMetadata` |
62
+ | `onLoadedData` | `loadedData` |
63
+ | `onCanPlay` | `canPlay` |
64
+ | `onCanPlayThrough` | `canPlayThrough` |
65
+ | `onDurationChange` | `durationChange` |
66
+ | `onRateChange` | `rateChange` |
67
+ | `onSeeking` / `onSeeked` | `seeking` / `seeked` |
68
+ | `onStalled` / `onWaiting` | `stalled` / `waiting` |
69
+
70
+ Only primitive event props move. Custom `on*` props you define on your own views (`TodoItem({ onEdit, onSave })`, etc.) are plain props — leave them as-is.
71
+
72
+ **Optional perf tip.** If you hoist the events object once, the renderer skips the per-event diff via `prev.on === next.on`:
73
+
74
+ ```ts
75
+ const events = { click: handleClick, pointerDown: handleDown };
76
+ // inside render():
77
+ button({ class: "btn", on: events }, "Save");
78
+ ```
79
+
80
+ Inline `on: { ... }` still works; the renderer falls back to a sub-key diff, which is still cheap.
81
+
82
+ ### Exposing an API: `ctx.expose(api)` → `ctx.ref(api)`
83
+
84
+ Pure rename — same semantics, same `applyRef` wiring. The provider verb now uses the same word as the consumer noun.
85
+
86
+ ```ts
87
+ // Before
88
+ const TextInput = view<{ placeholder: string }, { focus: () => void }>(
89
+ ({ props, expose }) => {
90
+ let el: HTMLInputElement | null = null;
91
+ expose({ focus: () => el?.focus() });
92
+ return {
93
+ render() {
94
+ input({
95
+ placeholder: props().placeholder,
96
+ ref: (e) => { el = e as HTMLInputElement; },
97
+ });
98
+ },
99
+ };
100
+ },
101
+ );
102
+
103
+ // After
104
+ const TextInput = view<{ placeholder: string }, { focus: () => void }>(
105
+ ({ props, ref }) => {
106
+ let el: HTMLInputElement | null = null;
107
+ ref({ focus: () => el?.focus() });
108
+ return {
109
+ render() {
110
+ input({
111
+ placeholder: props().placeholder,
112
+ ref: (e) => { el = e as HTMLInputElement; },
113
+ });
114
+ },
115
+ };
116
+ },
117
+ );
118
+ ```
119
+
120
+ The `Expose<Api>` type export is now `RefSetter<Api>`.
121
+
122
+ The consumer side is unchanged: `TextInput({ placeholder: "Email", ref: myRef })`.
123
+
124
+ ### Removed type exports
125
+
126
+ - `Expose<Api>` → renamed to `RefSetter<Api>`
127
+ - `EventHandlerProps<Events>` → removed. The replacement shape is just `{ on?: Partial<Events> }`, applied automatically by `PrimitiveProps<Attrs, Events>`.
128
+
129
+ # 0.2.6
130
+
131
+ 1. Cache first mount render props for primitives
132
+ 2. Fix re-bounding event to primitives
133
+ 3. Fix old children slice handling to avoid incorrect data duplicity
134
+ 4. Support chainable state & store
135
+ 5. Improve child placement lookup performance from O(N) to O(1)
136
+ 6. Improve "live-DOM" value handling: value, mute, checked
137
+ 7. Make stores to use publicly visible symbol Symbol.for("creo.store")
138
+ 8.
package/README.md CHANGED
@@ -336,8 +336,10 @@ bun test packages/creo/src/ # Run tests
336
336
  bun run build # Build all packages
337
337
  bun run typecheck # Type-check
338
338
 
339
- # Run examples:
340
- cd examples/todo && bun install && bun run dev
339
+ # Run the docs site (with the live recipe playground — simple-todo, advanced-todo, table, chess, etc.):
340
+ cd docs && bun install && bun run dev
341
+
342
+ # Run a standalone example:
341
343
  cd examples/router && bun install && bun run dev
342
344
 
343
345
  # Version management:
@@ -1,2 +1 @@
1
1
  export type Key = number | string;
2
- export declare function generateNextKey(): string;
package/dist/index.d.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  export { createApp } from "./public/app";
2
- export { view } from "./public/view";
3
- export type { ViewBody, ViewFn, Slot, SlotContent, PublicView } from "./public/view";
2
+ export { view, applyRef } from "./public/view";
3
+ export type { ViewBody, ViewFn, Slot, SlotContent, PublicView, Ref, RefCallback, RefObject, RefSetter, } from "./public/view";
4
4
  export type { Reactive, Use } from "./public/state";
5
5
  export { State } from "./public/state";
6
6
  export { Store, store, isStore } from "./public/store";
7
7
  export { $primitive } from "./public/primitive";
8
- export type { PrimitiveProps, EventHandlerProps } from "./public/primitive";
8
+ export type { PrimitiveProps } from "./public/primitive";
9
9
  export { html, text, div, span, section, article, aside, nav, header, footer, main, address, hgroup, search, p, h1, h2, h3, h4, h5, h6, pre, code, em, strong, small, br, hr, a, blockquote, label, abbr, b, bdi, bdo, cite, data, dfn, i, kbd, mark, q, rp, rt, ruby, s, samp, sub, sup, time, u, varEl, wbr, del, ins, ul, ol, li, dl, dt, dd, table, thead, tbody, tfoot, tr, th, td, caption, colgroup, col, form, button, input, textarea, select, option, fieldset, legend, datalist, optgroup, output, progress, meter, img, video, audio, canvas, source, track, map, area, picture, iframe, embed, object, portal, svg, details, summary, dialog, menu, figure, figcaption, script, noscript, template, slot, } from "./public/primitives/primitives";
10
10
  export type { BaseEventData, PointerEventData, KeyEventData, InputEventData, FocusEventData, MediaEventData, ScrollEventData, LoadEventData, ErrorEventData, ToggleEventData, ContainerEvents, FormEvents, MediaEvents, DisclosureEvents, HtmlAttrs, } from "./public/primitives/primitives";
11
11
  export type { IRender } from "./render/render_interface";
12
12
  export { HtmlRender } from "./render/html_render";
13
13
  export { JsonRender } from "./render/json_render";
14
14
  export type { JsonNode } from "./render/json_render";
15
- export { HtmlStringRender, StringRender } from "./render/string_render";
15
+ export { HtmlStringRender } from "./render/string_render";
16
16
  export { Engine, type Scheduler } from "./internal/engine";
17
- export { type Maybe, type None, type Just, just, withDefault, _ } from "./functional/maybe";
17
+ export { type Maybe, type None, type Just, just, withDefault, _, } from "./functional/maybe";
18
18
  export type { Key } from "./functional/key";
19
19
  export { shallowEqual } from "./functional/shallow_equal";
20
20
  export { assertNever } from "./functional/assert";