@pyreon/mcp 0.12.15 → 0.13.1
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1324 -424
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
- package/src/api-reference.ts +1493 -477
- package/src/tests/api-reference.test.ts +521 -0
package/lib/index.js
CHANGED
|
@@ -12750,135 +12750,123 @@ var StdioServerTransport = class {
|
|
|
12750
12750
|
|
|
12751
12751
|
//#endregion
|
|
12752
12752
|
//#region package.json
|
|
12753
|
-
var version = "0.
|
|
12753
|
+
var version = "0.13.1";
|
|
12754
12754
|
|
|
12755
12755
|
//#endregion
|
|
12756
12756
|
//#region src/api-reference.ts
|
|
12757
12757
|
const API_REFERENCE = {
|
|
12758
12758
|
"reactivity/signal": {
|
|
12759
|
-
signature: "
|
|
12759
|
+
signature: "<T>(initialValue: T, options?: { name?: string }) => Signal<T>",
|
|
12760
12760
|
example: `const count = signal(0)
|
|
12761
|
-
|
|
12762
|
-
//
|
|
12763
|
-
count() // 0
|
|
12764
|
-
|
|
12765
|
-
// Write:
|
|
12766
|
-
count.set(5) // sets to 5
|
|
12767
|
-
|
|
12768
|
-
// Update:
|
|
12761
|
+
count() // 0 (subscribes to updates)
|
|
12762
|
+
count.set(5) // sets to 5
|
|
12769
12763
|
count.update(n => n + 1) // 6
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
count.
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
- \`
|
|
12776
|
-
- \`
|
|
12764
|
+
count.peek() // 6 (does NOT subscribe)`,
|
|
12765
|
+
notes: "Create a reactive signal. The returned value is a CALLABLE FUNCTION — `count()` reads (and subscribes), `count.set(v)` writes, `count.update(fn)` derives, `count.peek()` reads without subscribing. This is NOT a `.value` getter/setter pattern (React/Vue) — Pyreon signals are functions. Optional `{ name }` for debugging; auto-injected by `@pyreon/vite-plugin` in dev mode. See also: computed, effect, batch.",
|
|
12766
|
+
mistakes: `- \`count.value\` — does not exist. Use \`count()\` to read
|
|
12767
|
+
- \`count = 5\` — reassigning the variable replaces the signal, does not write to it. Use \`count.set(5)\`
|
|
12768
|
+
- \`signal(5)\` called with an argument after creation — reads and ignores the argument (dev mode warns). Use \`.set(5)\` to write
|
|
12769
|
+
- \`const [val, setVal] = signal(0)\` — signals are not destructurable tuples. The whole return value IS the signal
|
|
12770
|
+
- \`{count}\` in JSX — renders the signal function itself, not its value. Use \`{count()}\` or \`{() => count()}\`
|
|
12771
|
+
- \`.peek()\` inside \`effect()\` / \`computed()\` — bypasses tracking, creates stale reads. Only use \`.peek()\` for loop-prevention guards`
|
|
12777
12772
|
},
|
|
12778
12773
|
"reactivity/computed": {
|
|
12779
|
-
signature: "
|
|
12774
|
+
signature: "<T>(fn: () => T, options?: { equals?: (a: T, b: T) => boolean }) => Computed<T>",
|
|
12780
12775
|
example: `const count = signal(0)
|
|
12781
12776
|
const doubled = computed(() => count() * 2)
|
|
12782
|
-
|
|
12783
12777
|
doubled() // 0
|
|
12784
12778
|
count.set(5)
|
|
12785
12779
|
doubled() // 10`,
|
|
12786
|
-
notes: "Dependencies auto-tracked
|
|
12787
|
-
mistakes: `- \`computed(() => count)\`
|
|
12788
|
-
-
|
|
12780
|
+
notes: "Create a memoized derived value. Dependencies auto-tracked on each evaluation — no dependency array needed (unlike React `useMemo`). Only recomputes when a tracked signal actually changes. Custom `equals` function prevents downstream effects from firing on structurally-equal updates (default: `Object.is`). See also: signal, effect.",
|
|
12781
|
+
mistakes: `- \`computed(() => count)\` — must CALL the signal: \`computed(() => count())\`
|
|
12782
|
+
- Using \`computed()\` for side effects — use \`effect()\` instead; computed is for pure derivation
|
|
12783
|
+
- Expecting \`computed()\` to re-run when a \`.peek()\`-read signal changes — \`.peek()\` bypasses tracking`
|
|
12789
12784
|
},
|
|
12790
12785
|
"reactivity/effect": {
|
|
12791
|
-
signature: "
|
|
12786
|
+
signature: "(fn: () => (() => void) | void) => () => void",
|
|
12792
12787
|
example: `const count = signal(0)
|
|
12793
|
-
|
|
12794
|
-
// Auto-tracks count() dependency:
|
|
12795
12788
|
const dispose = effect(() => {
|
|
12796
|
-
console.log("Count
|
|
12789
|
+
console.log("Count:", count())
|
|
12790
|
+
onCleanup(() => console.log("cleaning up"))
|
|
12797
12791
|
})
|
|
12798
|
-
|
|
12799
|
-
// With onCleanup:
|
|
12800
|
-
effect(() => {
|
|
12801
|
-
const handler = () => console.log(count())
|
|
12802
|
-
window.addEventListener("resize", handler)
|
|
12803
|
-
onCleanup(() => window.removeEventListener("resize", handler))
|
|
12804
|
-
})
|
|
12805
|
-
|
|
12806
|
-
// Or return cleanup (also works):
|
|
12792
|
+
// Or return cleanup directly:
|
|
12807
12793
|
effect(() => {
|
|
12808
12794
|
const handler = () => console.log(count())
|
|
12809
12795
|
window.addEventListener("resize", handler)
|
|
12810
12796
|
return () => window.removeEventListener("resize", handler)
|
|
12811
12797
|
})`,
|
|
12812
|
-
notes: "Returns a dispose function.
|
|
12813
|
-
mistakes: `-
|
|
12814
|
-
- \`effect(() => { count })\`
|
|
12798
|
+
notes: "Run a side effect that auto-tracks signal dependencies and re-runs when they change. Returns a dispose function that unsubscribes. The effect function can return a cleanup callback (equivalent to calling `onCleanup()` inside the body) — the cleanup runs before each re-execution and on final dispose. For DOM-specific effects with lighter overhead, use `renderEffect()` instead. See also: onCleanup, computed, renderEffect.",
|
|
12799
|
+
mistakes: `- Passing a dependency array — Pyreon auto-tracks; no array needed
|
|
12800
|
+
- \`effect(() => { count })\` — must call the signal: \`effect(() => { count() })\`
|
|
12801
|
+
- Nesting \`effect()\` inside \`effect()\` — use \`computed()\` for derived values instead
|
|
12802
|
+
- Creating signals inside an effect — they re-create on every run; create once outside`
|
|
12803
|
+
},
|
|
12804
|
+
"reactivity/batch": {
|
|
12805
|
+
signature: "(fn: () => void) => void",
|
|
12806
|
+
example: `const a = signal(1)
|
|
12807
|
+
const b = signal(2)
|
|
12808
|
+
batch(() => {
|
|
12809
|
+
a.set(10)
|
|
12810
|
+
b.set(20)
|
|
12811
|
+
})
|
|
12812
|
+
// Effects that read both a() and b() fire once, not twice`,
|
|
12813
|
+
notes: "Group multiple signal writes so subscribers fire only once — after the batch completes. Uses pointer swap (zero allocation). Essential when updating 3+ signals that downstream effects read together; without batch, each `.set()` triggers an independent notification pass. See also: signal, effect.",
|
|
12814
|
+
mistakes: `- Reading a signal inside \`batch()\` and expecting the NEW value before the batch completes — reads inside the batch see the new value (writes are synchronous), but effects fire only after the batch callback returns
|
|
12815
|
+
- Forgetting \`batch()\` when updating 3+ related signals — causes N intermediate re-renders`
|
|
12815
12816
|
},
|
|
12816
12817
|
"reactivity/onCleanup": {
|
|
12817
|
-
signature: "
|
|
12818
|
+
signature: "(fn: () => void) => void",
|
|
12818
12819
|
example: `effect(() => {
|
|
12819
12820
|
const handler = () => console.log(count())
|
|
12820
12821
|
window.addEventListener("resize", handler)
|
|
12821
12822
|
onCleanup(() => window.removeEventListener("resize", handler))
|
|
12822
12823
|
})`,
|
|
12823
|
-
notes: "
|
|
12824
|
-
mistakes: `- Using onCleanup outside an effect — it only works inside effect() or renderEffect()
|
|
12825
|
-
- Confusing with onUnmount — onCleanup is for effects, onUnmount is for
|
|
12824
|
+
notes: "Register a cleanup function inside an `effect()` or `renderEffect()`. Runs before each re-execution of the effect (when dependencies change) and once on final dispose. Equivalent to returning a cleanup function from the effect body — both forms work, `onCleanup` is useful when you need to register cleanup at a different point than the end of the body. See also: effect.",
|
|
12825
|
+
mistakes: `- Using \`onCleanup\` outside an effect — it only works inside \`effect()\` or \`renderEffect()\` body
|
|
12826
|
+
- Confusing with \`onUnmount\` — \`onCleanup\` is for effects, \`onUnmount\` is for component lifecycle`
|
|
12826
12827
|
},
|
|
12827
|
-
"reactivity/
|
|
12828
|
-
signature: "
|
|
12829
|
-
example: `
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
// Updates subscribers only once:
|
|
12833
|
-
batch(() => {
|
|
12834
|
-
a.set(10)
|
|
12835
|
-
b.set(20)
|
|
12828
|
+
"reactivity/watch": {
|
|
12829
|
+
signature: "<T>(source: () => T, callback: (next: T, prev: T) => void, options?: WatchOptions) => () => void",
|
|
12830
|
+
example: `watch(() => count(), (next, prev) => {
|
|
12831
|
+
console.log(\`changed from \${prev} to \${next}\`)
|
|
12836
12832
|
})`,
|
|
12837
|
-
notes: "
|
|
12833
|
+
notes: "Explicit reactive watcher — tracks `source` and fires `callback` when it changes. Unlike `effect()`, the callback receives both `next` and `prev` values and does NOT auto-track signals read inside the callback body. `source` is evaluated at setup time to establish tracking; reading browser globals there still fires SSR lint rules. Returns a dispose function. See also: effect, computed.",
|
|
12834
|
+
mistakes: `- Reading browser globals in the \`source\` function — it runs at setup time (not just in mounted context), so \`no-window-in-ssr\` fires on \`window.X\` there
|
|
12835
|
+
- Expecting signals read inside the \`callback\` to be tracked — only the \`source\` function establishes tracking; the callback is untracked`
|
|
12838
12836
|
},
|
|
12839
12837
|
"reactivity/createStore": {
|
|
12840
|
-
signature: "
|
|
12838
|
+
signature: "<T extends object>(initial: T) => T",
|
|
12841
12839
|
example: `const store = createStore({
|
|
12842
|
-
|
|
12843
|
-
|
|
12840
|
+
todos: [{ text: 'Learn Pyreon', done: false }],
|
|
12841
|
+
filter: 'all',
|
|
12844
12842
|
})
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
store.
|
|
12848
|
-
store.
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
"reactivity/createResource": {
|
|
12852
|
-
signature: "createResource<T>(fetcher: () => Promise<T>, options?: ResourceOptions): Resource<T>",
|
|
12853
|
-
example: `const users = createResource(() => fetch("/api/users").then(r => r.json()))
|
|
12854
|
-
|
|
12855
|
-
// In JSX:
|
|
12856
|
-
<Show when={!users.loading()}>
|
|
12857
|
-
<For each={users()} by={u => u.id}>
|
|
12858
|
-
{user => <li>{user.name}</li>}
|
|
12859
|
-
</For>
|
|
12860
|
-
</Show>`,
|
|
12861
|
-
notes: "Integrates with Suspense. Access .loading(), .error(), and call resource() for the value."
|
|
12843
|
+
store.todos[0].done = true // fine-grained — only 'done' subscribers fire
|
|
12844
|
+
store.todos.push({ text: 'Build app', done: false }) // array methods work`,
|
|
12845
|
+
notes: "Create a deeply reactive proxy-based object. Mutations at any depth trigger fine-grained updates — `store.todos[0].done = true` only re-runs effects that read `store.todos[0].done`, not effects that read `store.todos.length` or other items. No immer, no spread-copy, no `produce()` — just mutate. Works with nested objects, arrays, Maps, and Sets. See also: signal.",
|
|
12846
|
+
mistakes: `- Replacing the entire store object — \`store = { ... }\` replaces the variable, not the proxy. Mutate properties instead: \`store.filter = "active"\`
|
|
12847
|
+
- Destructuring store properties at setup — \`const { filter } = store\` captures the value once, losing reactivity. Read \`store.filter\` inside reactive scopes
|
|
12848
|
+
- Using \`createStore\` for simple scalar state — use \`signal()\` for primitives; \`createStore\` adds proxy overhead that only pays off for nested objects`
|
|
12862
12849
|
},
|
|
12863
12850
|
"reactivity/untrack": {
|
|
12864
|
-
signature: "
|
|
12865
|
-
example: `
|
|
12866
|
-
|
|
12867
|
-
//
|
|
12868
|
-
effect(() => {
|
|
12869
|
-
const name = untrack(() => userName())
|
|
12870
|
-
console.log("Count changed:", count(), "user is", name)
|
|
12851
|
+
signature: "(fn: () => T) => T",
|
|
12852
|
+
example: `effect(() => {
|
|
12853
|
+
const current = count() // tracked — effect re-runs on count change
|
|
12854
|
+
const other = untrack(() => otherSignal()) // NOT tracked — just reads the current value
|
|
12871
12855
|
})`,
|
|
12872
|
-
notes:
|
|
12856
|
+
notes: `Execute a function reading signals WITHOUT subscribing to them. Alias for \`runUntracked\`. Use inside effects when you need to read a signal's current value as a one-shot snapshot without the effect re-running when that signal changes. See also: signal, effect.`,
|
|
12857
|
+
mistakes: "- Using `untrack` as the default — signals should be tracked by default; `untrack` is the escape hatch for specific optimization or loop-prevention cases"
|
|
12873
12858
|
},
|
|
12874
12859
|
"core/h": {
|
|
12875
|
-
signature: "h<P>(type: ComponentFn<P> | string | symbol, props: P | null, ...children: VNodeChild[]): VNode",
|
|
12876
|
-
example:
|
|
12877
|
-
const vnode = h("div", { class: "container" },
|
|
12860
|
+
signature: "h<P extends Props>(type: ComponentFn<P> | string | symbol, props: P | null, ...children: VNodeChild[]): VNode",
|
|
12861
|
+
example: `const vnode = h("div", { class: "container" },
|
|
12878
12862
|
h("h1", null, "Hello"),
|
|
12879
12863
|
h(Counter, { initial: 0 })
|
|
12880
12864
|
)`,
|
|
12881
|
-
notes: "Low-level API
|
|
12865
|
+
notes: "Create a VNode from a component function, HTML tag string, or symbol (Fragment, Portal). Low-level API — prefer JSX which compiles to `h()` calls (or `_tpl()` + `_bind()` for template-optimized paths). Children are stored in `vnode.children`; components must merge them via `props.children = vnode.children.length === 1 ? vnode.children[0] : vnode.children`. See also: Fragment, Dynamic, lazy.",
|
|
12866
|
+
mistakes: `- \`h("div", "text")\` — second arg is always props (or null). Text children go in the third+ positions: \`h("div", null, "text")\`
|
|
12867
|
+
- \`h(MyComponent, { children: <span /> })\` — children go as rest args, not a prop: \`h(MyComponent, null, <span />)\`
|
|
12868
|
+
- \`h("input", { className: "x" })\` — use \`class\` not \`className\` (Pyreon uses standard HTML attributes)
|
|
12869
|
+
- \`h("input", { onChange: handler })\` — use \`onInput\` for keypress-by-keypress updates (native DOM events)`
|
|
12882
12870
|
},
|
|
12883
12871
|
"core/Fragment": {
|
|
12884
12872
|
signature: "Fragment: symbol",
|
|
@@ -12889,7 +12877,8 @@ const vnode = h("div", { class: "container" },
|
|
|
12889
12877
|
</>
|
|
12890
12878
|
|
|
12891
12879
|
// h() API:
|
|
12892
|
-
h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))
|
|
12880
|
+
h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))`,
|
|
12881
|
+
notes: "Symbol used as the type for fragment VNodes that group children without producing a wrapper DOM element. In JSX, `<>...</>` compiles to `h(Fragment, null, ...)`. Useful when a component needs to return multiple sibling elements. See also: h."
|
|
12893
12882
|
},
|
|
12894
12883
|
"core/onMount": {
|
|
12895
12884
|
signature: "onMount(fn: () => CleanupFn | void): void",
|
|
@@ -12898,67 +12887,128 @@ h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))`
|
|
|
12898
12887
|
|
|
12899
12888
|
onMount(() => {
|
|
12900
12889
|
const id = setInterval(() => count.update(n => n + 1), 1000)
|
|
12901
|
-
return () => clearInterval(id) // cleanup
|
|
12890
|
+
return () => clearInterval(id) // cleanup on unmount
|
|
12902
12891
|
})
|
|
12903
12892
|
|
|
12904
|
-
return <div>{count()}</div>
|
|
12893
|
+
return <div>{() => count()}</div>
|
|
12905
12894
|
}`,
|
|
12906
|
-
notes: "
|
|
12907
|
-
mistakes: `- Forgetting cleanup: \`onMount(() => { const id = setInterval(...) })\`
|
|
12895
|
+
notes: "Register a callback that runs after the component mounts into the DOM. The callback can optionally return a cleanup function that runs on unmount — this is the idiomatic pattern for event listeners, timers, and subscriptions. Must be called during component setup (the synchronous function body), not inside effects or async callbacks. See also: onUnmount, onUpdate.",
|
|
12896
|
+
mistakes: `- Forgetting cleanup: \`onMount(() => { const id = setInterval(...) })\` leaks the interval. Return cleanup: \`return () => clearInterval(id)\`
|
|
12897
|
+
- Using \`onMount\` + separate \`onUnmount\` for paired setup/teardown — prefer returning cleanup from \`onMount\` instead
|
|
12898
|
+
- Calling \`onMount\` inside an \`effect()\` or async callback — it only works during synchronous component setup
|
|
12899
|
+
- Accessing DOM refs before mount — the callback runs AFTER mount, which is the right place for DOM measurements`
|
|
12908
12900
|
},
|
|
12909
12901
|
"core/onUnmount": {
|
|
12910
12902
|
signature: "onUnmount(fn: () => void): void",
|
|
12911
12903
|
example: `onUnmount(() => {
|
|
12912
12904
|
console.log("Component removed from DOM")
|
|
12913
|
-
})
|
|
12905
|
+
})`,
|
|
12906
|
+
notes: "Register a callback that runs when the component is removed from the DOM. For paired setup/teardown, prefer returning a cleanup function from `onMount` instead — it co-locates the cleanup with the setup. `onUnmount` is useful when cleanup needs to reference state computed separately from the mount callback. See also: onMount."
|
|
12907
|
+
},
|
|
12908
|
+
"core/onUpdate": {
|
|
12909
|
+
signature: "onUpdate(fn: () => void): void",
|
|
12910
|
+
example: `onUpdate(() => {
|
|
12911
|
+
console.log("Component updated, DOM is current")
|
|
12912
|
+
})`,
|
|
12913
|
+
notes: "Register a callback that runs after the component updates (reactive dependencies change and DOM patches complete). Rarely needed — most update logic belongs in `effect()` or `computed()`. Useful for imperative DOM measurements that need to run after all reactive updates have flushed. See also: onMount, onUnmount."
|
|
12914
|
+
},
|
|
12915
|
+
"core/onErrorCaptured": {
|
|
12916
|
+
signature: "onErrorCaptured(fn: (error: unknown) => boolean | void): void",
|
|
12917
|
+
example: `onErrorCaptured((error) => {
|
|
12918
|
+
console.error("Caught:", error)
|
|
12919
|
+
return false // stop propagation
|
|
12920
|
+
})`,
|
|
12921
|
+
notes: "Register an error handler that captures errors thrown by descendant components. Return `false` to prevent the error from propagating further up the tree. Works alongside `ErrorBoundary` for programmatic error handling. See also: ErrorBoundary."
|
|
12914
12922
|
},
|
|
12915
12923
|
"core/createContext": {
|
|
12916
12924
|
signature: "createContext<T>(defaultValue: T): Context<T>",
|
|
12917
|
-
example: `const
|
|
12925
|
+
example: `const ThemeCtx = createContext<"light" | "dark">("light")
|
|
12918
12926
|
|
|
12919
12927
|
// Provide:
|
|
12920
12928
|
const App = () => {
|
|
12921
|
-
provide(
|
|
12929
|
+
provide(ThemeCtx, "dark")
|
|
12922
12930
|
return <Child />
|
|
12923
12931
|
}
|
|
12924
12932
|
|
|
12925
12933
|
// Consume:
|
|
12926
12934
|
const Child = () => {
|
|
12927
|
-
const theme = useContext(
|
|
12935
|
+
const theme = useContext(ThemeCtx) // "dark" — safe to destructure
|
|
12928
12936
|
return <div class={theme}>...</div>
|
|
12929
|
-
}
|
|
12937
|
+
}`,
|
|
12938
|
+
notes: "Create a static context. `useContext()` returns the value directly (`T`), so it is safe to destructure. Use this for values that do not change after being provided (theme name, locale string, config object). For values that change reactively (mode signal, locale signal), use `createReactiveContext` instead — otherwise consumers capture a stale snapshot at setup time. See also: createReactiveContext, provide, useContext.",
|
|
12939
|
+
mistakes: `- \`provide(ThemeCtx, () => modeSignal())\` with a static context — the consumer receives the function itself, not the signal value. Use \`createReactiveContext\` for dynamic values
|
|
12940
|
+
- Destructuring a reactive context value: \`const { mode } = useContext(reactiveCtx)\` captures once. Keep the object reference and access lazily
|
|
12941
|
+
- Calling \`useContext\` outside a component body — it reads from the component context stack, which only exists during setup`
|
|
12930
12942
|
},
|
|
12931
|
-
"core/
|
|
12932
|
-
signature: "
|
|
12933
|
-
example: `const
|
|
12943
|
+
"core/createReactiveContext": {
|
|
12944
|
+
signature: "createReactiveContext<T>(defaultValue: T): ReactiveContext<T>",
|
|
12945
|
+
example: `const ModeCtx = createReactiveContext<"light" | "dark">("light")
|
|
12946
|
+
|
|
12947
|
+
// Provide:
|
|
12948
|
+
const App = () => {
|
|
12949
|
+
const mode = signal<"light" | "dark">("dark")
|
|
12950
|
+
provide(ModeCtx, () => mode())
|
|
12951
|
+
return <Child />
|
|
12952
|
+
}
|
|
12953
|
+
|
|
12954
|
+
// Consume:
|
|
12955
|
+
const Child = () => {
|
|
12956
|
+
const getMode = useContext(ModeCtx) // () => "dark"
|
|
12957
|
+
return <div class={getMode()}>...</div>
|
|
12958
|
+
}`,
|
|
12959
|
+
notes: "Create a reactive context. `useContext()` returns `() => T` — an accessor that must be called to read the current value. Use this for values that change over time (mode, locale, user). The accessor subscribes to updates when read inside reactive scopes (`effect()`, JSX thunks, `computed()`). See also: createContext, provide, useContext."
|
|
12934
12960
|
},
|
|
12935
12961
|
"core/provide": {
|
|
12936
|
-
signature: "provide<T>(ctx: Context<T>, value: T): void",
|
|
12962
|
+
signature: "provide<T>(ctx: Context<T> | ReactiveContext<T>, value: T): void",
|
|
12937
12963
|
example: `const ThemeCtx = createContext<"light" | "dark">("light")
|
|
12938
12964
|
|
|
12939
12965
|
function App() {
|
|
12940
12966
|
provide(ThemeCtx, "dark")
|
|
12941
12967
|
return <Child />
|
|
12942
12968
|
}`,
|
|
12943
|
-
notes: "
|
|
12969
|
+
notes: "Push a context value for all descendant components. Auto-cleans up on unmount. Must be called during component setup (synchronous function body). Preferred over manual `pushContext`/`popContext`. For reactive values, provide a getter function to a `ReactiveContext`: `provide(ModeCtx, () => modeSignal())`. See also: createContext, createReactiveContext, useContext.",
|
|
12970
|
+
mistakes: `- \`provide(ctx, "static")\` for a value that changes — use \`createReactiveContext\` + \`provide(ctx, () => signal())\`
|
|
12971
|
+
- Calling \`provide\` inside \`onMount\` or \`effect\` — it must run during synchronous component setup
|
|
12972
|
+
- Providing the same context twice in one component — the second \`provide\` shadows the first for that subtree`
|
|
12944
12973
|
},
|
|
12945
|
-
"core/
|
|
12946
|
-
signature: "
|
|
12947
|
-
example: `const
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
// { name: string }`,
|
|
12951
|
-
notes: "Extracts the props type from a ComponentFn. Passes through unchanged if T is not a ComponentFn."
|
|
12974
|
+
"core/useContext": {
|
|
12975
|
+
signature: "useContext<T>(ctx: Context<T>): T",
|
|
12976
|
+
example: `const theme = useContext(ThemeContext) // static: returns T
|
|
12977
|
+
const getMode = useContext(ModeCtx) // reactive: returns () => T`,
|
|
12978
|
+
notes: "Read the nearest provided value for a context. For static `Context<T>`, returns `T` directly. For `ReactiveContext<T>`, returns `() => T` — must call the accessor to read. Falls back to the default value if no ancestor provides the context. See also: provide, createContext, createReactiveContext."
|
|
12952
12979
|
},
|
|
12953
|
-
"core/
|
|
12954
|
-
signature: "
|
|
12955
|
-
example:
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
}
|
|
12961
|
-
|
|
12980
|
+
"core/Show": {
|
|
12981
|
+
signature: "<Show when={condition} fallback={alternative}>{children}</Show>",
|
|
12982
|
+
example: `<Show when={isLoggedIn()} fallback={<LoginForm />}>
|
|
12983
|
+
<Dashboard />
|
|
12984
|
+
</Show>`,
|
|
12985
|
+
notes: "Reactive conditional rendering. Mounts children when `when` is truthy, unmounts and shows `fallback` when falsy. More efficient than ternary for signal-driven conditions because it avoids re-evaluating the entire branch expression on every signal change — `Show` only transitions between mounted/unmounted when the boolean flips. See also: Switch, Match, For.",
|
|
12986
|
+
mistakes: `- \`{cond() ? <A /> : <B />}\` — works but less efficient than \`<Show>\` for signal-driven conditions
|
|
12987
|
+
- \`<Show when={items().length}>\` — works (truthy check), but be explicit: \`<Show when={items().length > 0}>\`
|
|
12988
|
+
- \`<Show when={user}>\` without calling the signal — must call: \`<Show when={user()}>\``
|
|
12989
|
+
},
|
|
12990
|
+
"core/Switch": {
|
|
12991
|
+
signature: "<Switch fallback={default}>{Match children}</Switch>",
|
|
12992
|
+
example: `<Switch fallback={<p>Unknown status</p>}>
|
|
12993
|
+
<Match when={status() === "loading"}>
|
|
12994
|
+
<Spinner />
|
|
12995
|
+
</Match>
|
|
12996
|
+
<Match when={status() === "error"}>
|
|
12997
|
+
<ErrorDisplay />
|
|
12998
|
+
</Match>
|
|
12999
|
+
<Match when={status() === "success"}>
|
|
13000
|
+
<Results />
|
|
13001
|
+
</Match>
|
|
13002
|
+
</Switch>`,
|
|
13003
|
+
notes: "Multi-branch conditional rendering. Renders the first `<Match>` child whose `when` prop is truthy. If no match, renders the `fallback`. More readable than nested `<Show>` for multi-way conditions. See also: Match, Show."
|
|
13004
|
+
},
|
|
13005
|
+
"core/Match": {
|
|
13006
|
+
signature: "<Match when={condition}>{children}</Match>",
|
|
13007
|
+
example: `<Switch>
|
|
13008
|
+
<Match when={tab() === "home"}><Home /></Match>
|
|
13009
|
+
<Match when={tab() === "settings"}><Settings /></Match>
|
|
13010
|
+
</Switch>`,
|
|
13011
|
+
notes: "A branch inside a `<Switch>`. Renders its children when `when` is truthy and it is the first truthy `<Match>` in the parent `<Switch>`. Must be a direct child of `<Switch>`. See also: Switch, Show."
|
|
12962
13012
|
},
|
|
12963
13013
|
"core/For": {
|
|
12964
13014
|
signature: "<For each={items} by={keyFn}>{renderFn}</For>",
|
|
@@ -12968,19 +13018,13 @@ type Props = ExtractProps<typeof Greet>
|
|
|
12968
13018
|
])
|
|
12969
13019
|
|
|
12970
13020
|
<For each={items()} by={item => item.id}>
|
|
12971
|
-
{item => <li>{item.name}</li>}
|
|
13021
|
+
{(item, index) => <li>{item.name}</li>}
|
|
12972
13022
|
</For>`,
|
|
12973
|
-
notes: "Uses
|
|
12974
|
-
mistakes: `- \`<For each={items}>\`
|
|
12975
|
-
- \`<For each={items()} key={...}>\`
|
|
12976
|
-
- \`{items().map(...)}\`
|
|
12977
|
-
|
|
12978
|
-
"core/Show": {
|
|
12979
|
-
signature: "<Show when={condition} fallback={alternative}>{children}</Show>",
|
|
12980
|
-
example: `<Show when={isLoggedIn()} fallback={<LoginForm />}>
|
|
12981
|
-
<Dashboard />
|
|
12982
|
-
</Show>`,
|
|
12983
|
-
notes: "More efficient than ternary for signal-driven conditions. Only mounts/unmounts when condition changes."
|
|
13023
|
+
notes: "Keyed reactive list rendering. Uses the `by` prop (not `key`) for the key function because JSX extracts `key` as a special VNode reconciliation prop. The render function receives each item and its index. Internally uses an LIS-based reconciler for minimal DOM mutations when the list changes. See also: Show, mapArray.",
|
|
13024
|
+
mistakes: `- \`<For each={items}>\` — must call the signal: \`<For each={items()}>\`
|
|
13025
|
+
- \`<For each={items()} key={...}>\` — use \`by\` not \`key\` (JSX reserves \`key\` for VNode reconciliation)
|
|
13026
|
+
- \`{items().map(...)}\` — use \`<For>\` for reactive list rendering; \`.map()\` re-creates all DOM nodes on every change
|
|
13027
|
+
- \`<For each={items()} by={index}>\` — using array index as key defeats the reconciler; use a stable identity like \`item.id\``
|
|
12984
13028
|
},
|
|
12985
13029
|
"core/Suspense": {
|
|
12986
13030
|
signature: "<Suspense fallback={loadingUI}>{children}</Suspense>",
|
|
@@ -12988,7 +13032,18 @@ type Props = ExtractProps<typeof Greet>
|
|
|
12988
13032
|
|
|
12989
13033
|
<Suspense fallback={<div>Loading...</div>}>
|
|
12990
13034
|
<LazyPage />
|
|
12991
|
-
</Suspense
|
|
13035
|
+
</Suspense>`,
|
|
13036
|
+
notes: "Async boundary that shows `fallback` while any `lazy()` component or async child inside is loading. SSR mode streams the fallback immediately and swaps in the resolved content when ready (30s timeout). Nested Suspense boundaries are independent — an inner boundary resolving does not affect the outer. See also: lazy, ErrorBoundary."
|
|
13037
|
+
},
|
|
13038
|
+
"core/ErrorBoundary": {
|
|
13039
|
+
signature: "<ErrorBoundary onCatch={handler} fallback={errorUI}>{children}</ErrorBoundary>",
|
|
13040
|
+
example: `<ErrorBoundary
|
|
13041
|
+
onCatch={(err) => console.error(err)}
|
|
13042
|
+
fallback={(err) => <div>Error: {err.message}</div>}
|
|
13043
|
+
>
|
|
13044
|
+
<App />
|
|
13045
|
+
</ErrorBoundary>`,
|
|
13046
|
+
notes: "Catches render errors thrown by descendant components. The `fallback` receives the error object for display. `onCatch` fires with the error for logging/telemetry. Without an ErrorBoundary, uncaught errors propagate to the nearest `registerErrorHandler` or crash the app. See also: Suspense, onErrorCaptured."
|
|
12992
13047
|
},
|
|
12993
13048
|
"core/lazy": {
|
|
12994
13049
|
signature: "lazy(loader: () => Promise<{ default: ComponentFn }>, options?: LazyOptions): LazyComponent",
|
|
@@ -12997,29 +13052,20 @@ type Props = ExtractProps<typeof Greet>
|
|
|
12997
13052
|
// Use in JSX (wrap with Suspense):
|
|
12998
13053
|
<Suspense fallback={<Spinner />}>
|
|
12999
13054
|
<Settings />
|
|
13000
|
-
</Suspense
|
|
13055
|
+
</Suspense>`,
|
|
13056
|
+
notes: "Wrap a dynamic import for code splitting. Returns a component that integrates with `Suspense` — the parent Suspense boundary shows its fallback until the import resolves. The loaded component is cached after first resolution. See also: Suspense, Dynamic."
|
|
13001
13057
|
},
|
|
13002
13058
|
"core/Dynamic": {
|
|
13003
13059
|
signature: "<Dynamic component={comp} {...props} />",
|
|
13004
13060
|
example: `const components = { home: HomePage, about: AboutPage }
|
|
13005
13061
|
const current = signal("home")
|
|
13006
13062
|
|
|
13007
|
-
<Dynamic component={components[current()]}
|
|
13008
|
-
|
|
13009
|
-
"core/ErrorBoundary": {
|
|
13010
|
-
signature: "<ErrorBoundary onCatch={handler} fallback={errorUI}>{children}</ErrorBoundary>",
|
|
13011
|
-
example: `<ErrorBoundary
|
|
13012
|
-
onCatch={(err) => console.error(err)}
|
|
13013
|
-
fallback={(err) => <div>Error: {err.message}</div>}
|
|
13014
|
-
>
|
|
13015
|
-
<App />
|
|
13016
|
-
</ErrorBoundary>`
|
|
13063
|
+
<Dynamic component={components[current()]} />`,
|
|
13064
|
+
notes: "Renders a component by reference or string tag name. Useful when the component to render is determined at runtime (tab panels, plugin systems, polymorphic containers). When `component` changes, the previous component unmounts and the new one mounts. See also: lazy, h."
|
|
13017
13065
|
},
|
|
13018
13066
|
"core/cx": {
|
|
13019
13067
|
signature: "cx(...values: ClassValue[]): string",
|
|
13020
|
-
example: `
|
|
13021
|
-
|
|
13022
|
-
cx("foo", "bar") // "foo bar"
|
|
13068
|
+
example: `cx("foo", "bar") // "foo bar"
|
|
13023
13069
|
cx("base", isActive && "active") // conditional
|
|
13024
13070
|
cx({ base: true, active: isActive() }) // object syntax
|
|
13025
13071
|
cx(["a", ["b", { c: true }]]) // nested arrays
|
|
@@ -13027,37 +13073,32 @@ cx(["a", ["b", { c: true }]]) // nested arrays
|
|
|
13027
13073
|
// class prop accepts ClassValue directly:
|
|
13028
13074
|
<div class={["base", cond && "active"]} />
|
|
13029
13075
|
<div class={{ base: true, active: isActive() }} />`,
|
|
13030
|
-
notes: "
|
|
13031
|
-
mistakes: `- \`class={cx(...)}\` works but is redundant — class prop already accepts ClassValue
|
|
13032
|
-
- \`class={condition ? "a" : undefined}\` → Use \`class={[condition && "a"]}\` or \`class={{ a: condition }}\``
|
|
13076
|
+
notes: "Combine class values into a single string. Accepts strings, booleans (falsy values ignored), objects (`{ active: true }`), and arrays (nested). The `class` prop on JSX elements already accepts `ClassValue` directly, so explicit `cx()` is only needed when building class strings outside JSX or when composing values from multiple sources. See also: splitProps, mergeProps."
|
|
13033
13077
|
},
|
|
13034
13078
|
"core/splitProps": {
|
|
13035
13079
|
signature: "splitProps<T, K extends keyof T>(props: T, keys: K[]): [Pick<T, K>, Omit<T, K>]",
|
|
13036
|
-
example: `
|
|
13037
|
-
|
|
13038
|
-
const Button = (props: { class?: string; onClick: () => void; children: VNodeChild }) => {
|
|
13080
|
+
example: `const Button = (props: { class?: string; onClick: () => void; children: VNodeChild }) => {
|
|
13039
13081
|
const [local, rest] = splitProps(props, ["class"])
|
|
13040
13082
|
return <button {...rest} class={cx("btn", local.class)} />
|
|
13041
13083
|
}`,
|
|
13042
|
-
notes: "
|
|
13043
|
-
mistakes: `-
|
|
13044
|
-
-
|
|
13084
|
+
notes: "Split a props object into two parts: the picked keys and the rest. Both halves preserve signal reactivity — reads through either half still track the original reactive prop getters. This is the Pyreon replacement for `const { x, ...rest } = props` destructuring, which captures values once and loses reactivity. See also: mergeProps, cx.",
|
|
13085
|
+
mistakes: `- \`const { class: cls, ...rest } = props\` — destructuring captures once, loses reactivity. Use \`splitProps(props, ["class"])\`
|
|
13086
|
+
- Passing a non-props object — \`splitProps\` relies on reactive getter descriptors that the compiler creates on props objects
|
|
13087
|
+
- Forgetting that symbol-keyed props are preserved — \`splitProps\` uses \`Reflect.ownKeys\` so symbols (like \`REACTIVE_PROP\`) survive`
|
|
13045
13088
|
},
|
|
13046
13089
|
"core/mergeProps": {
|
|
13047
13090
|
signature: "mergeProps<T extends object[]>(...sources: T): MergedProps<T>",
|
|
13048
|
-
example: `
|
|
13049
|
-
|
|
13050
|
-
const Button = (props: { size?: string; variant?: string }) => {
|
|
13091
|
+
example: `const Button = (props: { size?: string; variant?: string }) => {
|
|
13051
13092
|
const merged = mergeProps({ size: "md", variant: "primary" }, props)
|
|
13052
13093
|
return <button class={\`btn-\${merged.size} btn-\${merged.variant}\`} />
|
|
13053
13094
|
}`,
|
|
13054
|
-
notes: "
|
|
13095
|
+
notes: "Merge multiple props objects with last-source-wins semantics. Reads are lazy — the merged object delegates to the source objects via getters, so signal reactivity is preserved. Commonly used to inject default props: `mergeProps({ size: \"md\" }, props)`. Forces `configurable: true` on copied descriptors to prevent \"Cannot redefine property\" errors. See also: splitProps, cx.",
|
|
13096
|
+
mistakes: `- \`Object.assign({}, defaults, props)\` — loses reactivity. Use \`mergeProps(defaults, props)\` instead
|
|
13097
|
+
- \`mergeProps(props, defaults)\` — wrong order. Defaults go FIRST, actual props last (last source wins)`
|
|
13055
13098
|
},
|
|
13056
13099
|
"core/createUniqueId": {
|
|
13057
13100
|
signature: "createUniqueId(): string",
|
|
13058
|
-
example: `
|
|
13059
|
-
|
|
13060
|
-
const LabeledInput = (props: { label: string }) => {
|
|
13101
|
+
example: `const LabeledInput = (props: { label: string }) => {
|
|
13061
13102
|
const id = createUniqueId()
|
|
13062
13103
|
return (
|
|
13063
13104
|
<>
|
|
@@ -13066,7 +13107,54 @@ const LabeledInput = (props: { label: string }) => {
|
|
|
13066
13107
|
</>
|
|
13067
13108
|
)
|
|
13068
13109
|
}`,
|
|
13069
|
-
notes: "
|
|
13110
|
+
notes: "Generate a unique string ID (\"pyreon-1\", \"pyreon-2\", ...) that is consistent between server and client when called in the same order. SSR-safe — the counter resets per request context. Use for `id`/`for`/`aria-*` attribute pairing in components. See also: splitProps."
|
|
13111
|
+
},
|
|
13112
|
+
"core/Portal": {
|
|
13113
|
+
signature: "<Portal target={element}>{children}</Portal>",
|
|
13114
|
+
example: `<Portal target={document.body}>
|
|
13115
|
+
<div class="modal-overlay">
|
|
13116
|
+
<div class="modal">Content</div>
|
|
13117
|
+
</div>
|
|
13118
|
+
</Portal>`,
|
|
13119
|
+
notes: "Render children into a DOM element outside the component tree (typically `document.body`). Useful for modals, tooltips, and overlays that need to escape parent overflow/z-index stacking contexts. Context values from the Portal source tree are preserved. See also: Dynamic."
|
|
13120
|
+
},
|
|
13121
|
+
"core/mapArray": {
|
|
13122
|
+
signature: "mapArray<T, U>(list: () => T[], mapFn: (item: T, index: () => number) => U): () => U[]",
|
|
13123
|
+
example: `const items = signal([1, 2, 3])
|
|
13124
|
+
const doubled = mapArray(() => items(), (item) => item * 2)
|
|
13125
|
+
// doubled() → [2, 4, 6] — updates reactively`,
|
|
13126
|
+
notes: "Low-level reactive array mapping used internally by `<For>`. Maps a reactive array signal through a transform function, caching results per item identity. Prefer `<For>` in JSX — use `mapArray` only when you need a reactive derived array outside of rendering. See also: For."
|
|
13127
|
+
},
|
|
13128
|
+
"core/createRef": {
|
|
13129
|
+
signature: "createRef<T>(): Ref<T>",
|
|
13130
|
+
example: `const inputRef = createRef<HTMLInputElement>()
|
|
13131
|
+
onMount(() => inputRef.current?.focus())
|
|
13132
|
+
return <input ref={inputRef} />`,
|
|
13133
|
+
notes: "Create a mutable ref object (`{ current: T | null }`) for holding DOM element references. Pass as the `ref` prop on JSX elements — the runtime sets `.current` after mount and clears it on unmount. Callback refs (`(el: T | null) => void`) are also supported via `RefProp<T>`. See also: onMount."
|
|
13134
|
+
},
|
|
13135
|
+
"core/untrack": {
|
|
13136
|
+
signature: "(fn: () => T) => T",
|
|
13137
|
+
example: `effect(() => {
|
|
13138
|
+
const current = count() // tracked
|
|
13139
|
+
const other = untrack(() => otherSignal()) // NOT tracked
|
|
13140
|
+
})`,
|
|
13141
|
+
notes: "Execute a function reading signals WITHOUT subscribing to them. Alias for `runUntracked` from `@pyreon/reactivity`. Use inside effects when you need a one-shot snapshot of a signal value without the effect re-running when that signal changes. See also: @pyreon/reactivity."
|
|
13142
|
+
},
|
|
13143
|
+
"core/ExtractProps": {
|
|
13144
|
+
signature: "type ExtractProps<T> = T extends ComponentFn<infer P> ? P : T",
|
|
13145
|
+
example: `const Greet: ComponentFn<{ name: string }> = ({ name }) => <h1>{name}</h1>
|
|
13146
|
+
type Props = ExtractProps<typeof Greet> // { name: string }`,
|
|
13147
|
+
notes: `Extracts the props type from a \`ComponentFn\`. Passes through unchanged if \`T\` is not a \`ComponentFn\`. Useful for HOC patterns and typed wrappers that need to infer the wrapped component's prop interface. See also: HigherOrderComponent.`
|
|
13148
|
+
},
|
|
13149
|
+
"core/HigherOrderComponent": {
|
|
13150
|
+
signature: "type HigherOrderComponent<HOP, P> = ComponentFn<HOP & P>",
|
|
13151
|
+
example: `function withLogger<P>(Wrapped: ComponentFn<P>): HigherOrderComponent<{ logLevel?: string }, P> {
|
|
13152
|
+
return (props) => {
|
|
13153
|
+
console.log(\`[\${props.logLevel ?? "info"}] Rendering\`)
|
|
13154
|
+
return <Wrapped {...props} />
|
|
13155
|
+
}
|
|
13156
|
+
}`,
|
|
13157
|
+
notes: `Typed HOC pattern where \`HOP\` is the props the HOC adds and \`P\` is the wrapped component's own props. The resulting component accepts both sets of props. See also: ExtractProps.`
|
|
13070
13158
|
},
|
|
13071
13159
|
"router/createRouter": {
|
|
13072
13160
|
signature: "createRouter(options: RouterOptions | RouteRecord[]): Router",
|
|
@@ -13076,7 +13164,12 @@ const LabeledInput = (props: { label: string }) => {
|
|
|
13076
13164
|
{ path: "/admin", component: Admin, beforeEnter: requireAuth, children: [
|
|
13077
13165
|
{ path: "settings", component: Settings },
|
|
13078
13166
|
]},
|
|
13079
|
-
])
|
|
13167
|
+
])`,
|
|
13168
|
+
notes: "Create a router instance with route records, guards, middleware, and mode configuration. Accepts either an array of route records (shorthand) or a full `RouterOptions` object with `routes`, `mode` (`\"history\"` | `\"hash\"`), `scrollBehavior`, `beforeEach`, `afterEach`, and `middleware`. The returned `Router` is generic over route names for typed programmatic navigation. See also: RouterProvider, useRouter, useRoute.",
|
|
13169
|
+
mistakes: `- \`createRouter({ routes: [...], mode: "hash" })\` and using \`window.location.hash\` elsewhere — hash mode uses \`history.pushState\`, not \`location.hash\`. Reading \`location.hash\` directly will not reflect router state
|
|
13170
|
+
- Defining route paths without leading \`/\` in root routes — all root-level paths must start with \`/\`
|
|
13171
|
+
- Using \`redirect: "/target"\` with a guard on the same route — redirects bypass guards. Use \`beforeEnter\` to conditionally redirect instead
|
|
13172
|
+
- Forgetting the catch-all route — \`{ path: "(.*)", component: NotFound }\` should be the last route to handle 404s`
|
|
13080
13173
|
},
|
|
13081
13174
|
"router/RouterProvider": {
|
|
13082
13175
|
signature: "<RouterProvider router={router}>{children}</RouterProvider>",
|
|
@@ -13085,7 +13178,8 @@ const LabeledInput = (props: { label: string }) => {
|
|
|
13085
13178
|
<nav><RouterLink to="/">Home</RouterLink></nav>
|
|
13086
13179
|
<RouterView />
|
|
13087
13180
|
</RouterProvider>
|
|
13088
|
-
)
|
|
13181
|
+
)`,
|
|
13182
|
+
notes: "Provide the router instance to the component tree via `RouterContext`. Must wrap the entire app (or the routed section). Sets up the context stack so `useRouter()`, `useRoute()`, and other hooks can access the router. See also: createRouter, RouterView, RouterLink."
|
|
13089
13183
|
},
|
|
13090
13184
|
"router/RouterView": {
|
|
13091
13185
|
signature: "<RouterView />",
|
|
@@ -13098,12 +13192,17 @@ const Admin = () => (
|
|
|
13098
13192
|
<h1>Admin</h1>
|
|
13099
13193
|
<RouterView /> {/* renders Settings, Users, etc. */}
|
|
13100
13194
|
</div>
|
|
13101
|
-
)
|
|
13195
|
+
)`,
|
|
13196
|
+
notes: `Render the matched route's component. For nested routes, the parent route component includes a \`<RouterView />\` that renders the matched child. Each \`<RouterView>\` renders one level of the route tree. See also: RouterProvider, createRouter.`
|
|
13102
13197
|
},
|
|
13103
13198
|
"router/RouterLink": {
|
|
13104
|
-
signature: "<RouterLink to={path} activeClass={cls} exactActiveClass={cls}
|
|
13199
|
+
signature: "<RouterLink to={path} activeClass={cls} exactActiveClass={cls}>{children}</RouterLink>",
|
|
13105
13200
|
example: `<RouterLink to="/" activeClass="nav-active">Home</RouterLink>
|
|
13106
|
-
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink
|
|
13201
|
+
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>`,
|
|
13202
|
+
notes: "Declarative navigation link that renders an `<a>` element. Supports string paths or named route objects (`{ name, params }`). Applies `activeClass` when the current route matches the link path (prefix), and `exactActiveClass` for exact matches. Click handler calls `router.push()` and prevents default. See also: useRouter, useIsActive.",
|
|
13203
|
+
mistakes: `- \`<a href="/about" onClick={() => router.push("/about")}>\` — use \`<RouterLink to="/about">\` instead; it handles the anchor element, active class, and click interception
|
|
13204
|
+
- \`<RouterLink to="/about" target="_blank">\` — external navigation bypasses the router; use a plain \`<a>\` for external links
|
|
13205
|
+
- \`<RouterLink to={dynamicPath}>\` without calling the signal — must call: \`<RouterLink to={dynamicPath()}>\` (or let the compiler handle it via \`_rp()\`)`
|
|
13107
13206
|
},
|
|
13108
13207
|
"router/useRouter": {
|
|
13109
13208
|
signature: "useRouter(): Router",
|
|
@@ -13114,7 +13213,11 @@ router.push({ name: "user", params: { id: "42" } })
|
|
|
13114
13213
|
router.replace("/login")
|
|
13115
13214
|
router.back()
|
|
13116
13215
|
router.forward()
|
|
13117
|
-
router.go(-2)
|
|
13216
|
+
router.go(-2)`,
|
|
13217
|
+
notes: "Access the router instance for programmatic navigation. Returns the `Router` object with `push()`, `replace()`, `back()`, `forward()`, `go()`. `await router.push()` resolves after the View Transition `updateCallbackDone` (DOM commit is complete, new route state is live), NOT after the animation finishes. See also: useRoute, RouterLink, createRouter.",
|
|
13218
|
+
mistakes: `- \`router.push("/path")\` at the top level of a component body — this is synchronous imperative navigation during render, causing an infinite loop. Wrap in \`onMount\`, event handler, or \`effect\`
|
|
13219
|
+
- \`await router.push("/path")\` expecting animation completion — \`push\` resolves after DOM commit (\`updateCallbackDone\`), not after View Transition animation finishes. Use the returned transition object's \`.finished\` if you need to wait for animation
|
|
13220
|
+
- Calling \`useRouter()\` outside a \`<RouterProvider>\` — throws because no router context exists`
|
|
13118
13221
|
},
|
|
13119
13222
|
"router/useRoute": {
|
|
13120
13223
|
signature: "useRoute<TPath extends string>(): () => ResolvedRoute<ExtractParams<TPath>>",
|
|
@@ -13124,17 +13227,50 @@ const userId = route().params.id // string
|
|
|
13124
13227
|
|
|
13125
13228
|
// Access query, meta, etc:
|
|
13126
13229
|
route().query
|
|
13127
|
-
route().meta
|
|
13230
|
+
route().meta`,
|
|
13231
|
+
notes: "Access the current resolved route as a reactive accessor. Generic over the path string for typed params — `useRoute<\"/user/:id\">()` yields `route().params.id: string`. Returns a function (accessor) that must be called to read the current route — reads inside reactive scopes track route changes. See also: useRouter, useSearchParams, useLoaderData."
|
|
13128
13232
|
},
|
|
13129
|
-
"router/
|
|
13130
|
-
signature: "
|
|
13131
|
-
example: `const
|
|
13233
|
+
"router/useIsActive": {
|
|
13234
|
+
signature: "useIsActive(path: string, exact?: boolean): () => boolean",
|
|
13235
|
+
example: `const isHome = useIsActive("/")
|
|
13236
|
+
const isAdmin = useIsActive("/admin") // prefix match
|
|
13237
|
+
const isExactAdmin = useIsActive("/admin", true) // exact only
|
|
13132
13238
|
|
|
13133
|
-
//
|
|
13134
|
-
|
|
13239
|
+
// Reactive — updates when route changes:
|
|
13240
|
+
<a class={{ active: isAdmin() }} href="/admin">Admin</a>`,
|
|
13241
|
+
notes: "Returns a reactive boolean for whether a path matches the current route. Segment-aware prefix matching: `/admin` matches `/admin/users` but NOT `/admin-panel`. Pass `exact=true` for exact-only matching. Updates reactively when the route changes. See also: useRoute, RouterLink.",
|
|
13242
|
+
mistakes: `- \`useIsActive("/admin")\` matching \`/admin-panel\` — this does NOT happen. Matching is segment-aware: \`/admin\` only matches paths starting with \`/admin/\` or exactly \`/admin\`
|
|
13243
|
+
- \`if (useIsActive("/settings")())\` at component top level — the outer call returns an accessor; make sure to read it inside a reactive scope for updates
|
|
13244
|
+
- Using \`useIsActive\` for complex route matching — it only does path prefix/exact matching. For query-param-aware or meta-aware checks, use \`useRoute()\` directly`
|
|
13245
|
+
},
|
|
13246
|
+
"router/useTypedSearchParams": {
|
|
13247
|
+
signature: "useTypedSearchParams<T>(schema: T): TypedSearchParams<T>",
|
|
13248
|
+
example: `const params = useTypedSearchParams({ page: "number", q: "string", active: "boolean" })
|
|
13249
|
+
params.page() // number (auto-coerced)
|
|
13250
|
+
params.q() // string
|
|
13251
|
+
params.set({ page: 2 }) // updates URL`,
|
|
13252
|
+
notes: "Type-safe search params with auto-coercion from URL strings. Schema keys define parameter names, values define types (`\"string\"`, `\"number\"`, `\"boolean\"`). Returns an object where each key is a reactive accessor and `.set()` updates the URL. See also: useSearchParams, useRoute."
|
|
13253
|
+
},
|
|
13254
|
+
"router/useTransition": {
|
|
13255
|
+
signature: "useTransition(): { isTransitioning: () => boolean }",
|
|
13256
|
+
example: `const { isTransitioning } = useTransition()
|
|
13135
13257
|
|
|
13136
|
-
|
|
13137
|
-
|
|
13258
|
+
<Show when={isTransitioning()}>
|
|
13259
|
+
<ProgressBar />
|
|
13260
|
+
</Show>`,
|
|
13261
|
+
notes: "Reactive signal for route transition state. `isTransitioning()` is true during navigation (while guards run + loaders resolve), false when the new route is mounted. Useful for progress bars and global loading indicators. See also: useRouter, useRoute."
|
|
13262
|
+
},
|
|
13263
|
+
"router/useMiddlewareData": {
|
|
13264
|
+
signature: "useMiddlewareData<T>(): T",
|
|
13265
|
+
example: `// Middleware:
|
|
13266
|
+
const authMiddleware: RouteMiddleware = async (ctx) => {
|
|
13267
|
+
ctx.data.user = await getUser(ctx.to)
|
|
13268
|
+
}
|
|
13269
|
+
|
|
13270
|
+
// Component:
|
|
13271
|
+
const data = useMiddlewareData<{ user: User }>()
|
|
13272
|
+
// data.user is available`,
|
|
13273
|
+
notes: "Read data set by `RouteMiddleware` in the middleware chain. Middleware functions receive `ctx` with a mutable `ctx.data` object — properties set there are available to all downstream components via this hook. See also: createRouter, useLoaderData."
|
|
13138
13274
|
},
|
|
13139
13275
|
"router/useLoaderData": {
|
|
13140
13276
|
signature: "useLoaderData<T>(): T",
|
|
@@ -13143,7 +13279,47 @@ setSearch({ page: "2" })`
|
|
|
13143
13279
|
const User = () => {
|
|
13144
13280
|
const data = useLoaderData<UserData>()
|
|
13145
13281
|
return <div>{data.name}</div>
|
|
13146
|
-
}
|
|
13282
|
+
}`,
|
|
13283
|
+
notes: `Access the data returned by the current route's \`loader\` function. The loader runs before the route component mounts; its return value is cached and available synchronously via this hook. Generic over the loader return type. See also: useMiddlewareData, useRoute.`
|
|
13284
|
+
},
|
|
13285
|
+
"router/useSearchParams": {
|
|
13286
|
+
signature: "useSearchParams<T>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>]",
|
|
13287
|
+
example: `const [search, setSearch] = useSearchParams({ page: "1", sort: "name" })
|
|
13288
|
+
|
|
13289
|
+
// Read:
|
|
13290
|
+
search().page // "1"
|
|
13291
|
+
|
|
13292
|
+
// Write:
|
|
13293
|
+
setSearch({ page: "2" })`,
|
|
13294
|
+
notes: "Access and update URL search params as a reactive tuple. Returns `[get, set]` where `get()` reads the current params and `set()` updates them via `replaceState`. For typed params with auto-coercion, prefer `useTypedSearchParams`. See also: useTypedSearchParams, useRoute."
|
|
13295
|
+
},
|
|
13296
|
+
"router/useBlocker": {
|
|
13297
|
+
signature: "useBlocker(shouldBlock: () => boolean): Blocker",
|
|
13298
|
+
example: `const blocker = useBlocker(() => form.isDirty())
|
|
13299
|
+
|
|
13300
|
+
<Show when={blocker.isBlocked()}>
|
|
13301
|
+
<Dialog>
|
|
13302
|
+
<p>Unsaved changes. Leave anyway?</p>
|
|
13303
|
+
<button onClick={blocker.proceed}>Leave</button>
|
|
13304
|
+
<button onClick={blocker.reset}>Stay</button>
|
|
13305
|
+
</Dialog>
|
|
13306
|
+
</Show>`,
|
|
13307
|
+
notes: `Block navigation when a condition is true (e.g., unsaved form changes). Returns a \`Blocker\` object with \`proceed()\` and \`reset()\` methods. Also hooks into the browser's \`beforeunload\` event to warn on tab close. Uses a shared ref-counted listener for \`beforeunload\` — N blockers share one event handler. See also: useRouter.`
|
|
13308
|
+
},
|
|
13309
|
+
"router/onBeforeRouteLeave": {
|
|
13310
|
+
signature: "onBeforeRouteLeave(guard: NavigationGuard): void",
|
|
13311
|
+
example: `onBeforeRouteLeave((to, from) => {
|
|
13312
|
+
if (hasUnsavedChanges()) return false // cancel navigation
|
|
13313
|
+
})`,
|
|
13314
|
+
notes: "Register a per-component navigation guard that fires when leaving the current route. Return `false` to cancel, a string path to redirect, or `undefined` to allow. Must be called during component setup. See also: onBeforeRouteUpdate, useBlocker."
|
|
13315
|
+
},
|
|
13316
|
+
"router/onBeforeRouteUpdate": {
|
|
13317
|
+
signature: "onBeforeRouteUpdate(guard: NavigationGuard): void",
|
|
13318
|
+
example: `onBeforeRouteUpdate((to, from) => {
|
|
13319
|
+
if (to.params.id === from.params.id) return // no change
|
|
13320
|
+
// reload data for new ID...
|
|
13321
|
+
})`,
|
|
13322
|
+
notes: "Register a per-component navigation guard that fires when the route updates but the same component stays mounted (e.g., param change `/user/1` to `/user/2`). Same return semantics as `onBeforeRouteLeave`. See also: onBeforeRouteLeave, useRoute."
|
|
13147
13323
|
},
|
|
13148
13324
|
"head/useHead": {
|
|
13149
13325
|
signature: "useHead(input: UseHeadInput | (() => UseHeadInput)): void",
|
|
@@ -13202,18 +13378,28 @@ const dispose = mount(<App />, document.getElementById("app")!)
|
|
|
13202
13378
|
|
|
13203
13379
|
// To unmount:
|
|
13204
13380
|
dispose()`,
|
|
13205
|
-
|
|
13206
|
-
|
|
13381
|
+
notes: "Mount a VNode tree into a container element. Clears the container first, sets up event delegation, then mounts the given child. Returns an `unmount` function that removes everything and disposes all effects. In dev mode, throws if `container` is null/undefined with an actionable error message. See also: hydrateRoot, render.",
|
|
13382
|
+
mistakes: `- \`createRoot(container).render(<App />)\` — Pyreon uses a single function call: \`mount(<App />, container)\`
|
|
13383
|
+
- \`mount(<App />, document.getElementById("app"))\` without \`!\` — getElementById returns \`Element | null\`. The runtime throws in dev if null, but TypeScript needs the assertion
|
|
13384
|
+
- \`mount(<App />, document.body)\` — mounting directly to body is discouraged; use a dedicated container element
|
|
13385
|
+
- Forgetting to call the returned unmount function — leaks event listeners and effects. Store and call it on cleanup`
|
|
13386
|
+
},
|
|
13387
|
+
"runtime-dom/render": {
|
|
13388
|
+
signature: "render(root: VNodeChild, container: Element): () => void",
|
|
13389
|
+
example: `import { render } from "@pyreon/runtime-dom"
|
|
13390
|
+
render(<App />, document.getElementById("app")!)`,
|
|
13391
|
+
notes: "Alias for `mount`. Provided for API familiarity — both names point to the same function. See also: mount."
|
|
13207
13392
|
},
|
|
13208
13393
|
"runtime-dom/hydrateRoot": {
|
|
13209
13394
|
signature: "hydrateRoot(root: VNodeChild, container: Element): () => void",
|
|
13210
13395
|
example: `import { hydrateRoot } from "@pyreon/runtime-dom"
|
|
13211
13396
|
|
|
13212
|
-
// Hydrate
|
|
13213
|
-
hydrateRoot(<App />, document.getElementById("app")!)
|
|
13397
|
+
// Hydrate SSR-rendered HTML:
|
|
13398
|
+
hydrateRoot(<App />, document.getElementById("app")!)`,
|
|
13399
|
+
notes: "Hydrate server-rendered HTML. Walks the existing DOM and attaches reactive bindings without recreating elements. Expects the DOM to match the VNode tree structure — mismatches emit dev-mode warnings. Returns an unmount function. See also: mount, @pyreon/runtime-server."
|
|
13214
13400
|
},
|
|
13215
13401
|
"runtime-dom/Transition": {
|
|
13216
|
-
signature: "<Transition name={name} mode={mode}>{children}</Transition>",
|
|
13402
|
+
signature: "<Transition name={name} mode={mode} onEnter={fn} onLeave={fn}>{children}</Transition>",
|
|
13217
13403
|
example: `<Transition name="fade" mode="out-in">
|
|
13218
13404
|
<Show when={visible()}>
|
|
13219
13405
|
<div>Content</div>
|
|
@@ -13223,50 +13409,602 @@ hydrateRoot(<App />, document.getElementById("app")!)`
|
|
|
13223
13409
|
/* CSS:
|
|
13224
13410
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s }
|
|
13225
13411
|
.fade-enter-from, .fade-leave-to { opacity: 0 }
|
|
13226
|
-
|
|
13412
|
+
*/`,
|
|
13413
|
+
notes: "CSS-based enter/leave animation wrapper. Applies `{name}-enter-from`, `{name}-enter-active`, `{name}-enter-to` classes on enter and the corresponding `-leave-*` classes on leave. `mode` controls sequencing: `\"out-in\"` waits for leave to complete before entering, `\"in-out\"` enters first. Has a 5-second safety timeout — if `transitionend`/`animationend` never fires, the transition completes automatically. See also: TransitionGroup, @pyreon/kinetic.",
|
|
13414
|
+
mistakes: `- Missing CSS classes — \`<Transition name="fade">\` does nothing without \`.fade-enter-active\` / \`.fade-leave-active\` CSS
|
|
13415
|
+
- Wrapping multiple root elements — Transition expects a single child (or null). Multiple children cause undefined behavior
|
|
13416
|
+
- Using \`mode="in-out"\` when you want sequential — \`"out-in"\` is almost always what you want (old leaves, then new enters)`
|
|
13417
|
+
},
|
|
13418
|
+
"runtime-dom/TransitionGroup": {
|
|
13419
|
+
signature: "<TransitionGroup name={name} tag={tag}>{children}</TransitionGroup>",
|
|
13420
|
+
example: `<TransitionGroup name="list" tag="ul">
|
|
13421
|
+
<For each={items()} by={i => i.id}>
|
|
13422
|
+
{item => <li>{item.name}</li>}
|
|
13423
|
+
</For>
|
|
13424
|
+
</TransitionGroup>
|
|
13425
|
+
|
|
13426
|
+
/* CSS:
|
|
13427
|
+
.list-enter-active, .list-leave-active { transition: all 0.3s }
|
|
13428
|
+
.list-enter-from, .list-leave-to { opacity: 0; transform: translateY(10px) }
|
|
13429
|
+
.list-move { transition: transform 0.3s }
|
|
13430
|
+
*/`,
|
|
13431
|
+
notes: "Animate list item additions and removals with CSS transitions. Each item gets enter/leave classes on mount/unmount. The `tag` prop controls the wrapper element (defaults to a fragment). Works with `<For>` for reactive lists. Also applies `-move` classes for FLIP-animated reordering. See also: Transition, For."
|
|
13432
|
+
},
|
|
13433
|
+
"runtime-dom/KeepAlive": {
|
|
13434
|
+
signature: "<KeepAlive include={pattern} exclude={pattern} max={number}>{children}</KeepAlive>",
|
|
13435
|
+
example: `const tab = signal<"a" | "b">("a")
|
|
13436
|
+
|
|
13437
|
+
<KeepAlive>
|
|
13438
|
+
<Show when={tab() === "a"}><ExpensiveFormA /></Show>
|
|
13439
|
+
<Show when={tab() === "b"}><ExpensiveFormB /></Show>
|
|
13440
|
+
</KeepAlive>`,
|
|
13441
|
+
notes: "Cache component instances across mount/unmount cycles so their state (signals, scroll position, form inputs) is preserved when they are toggled out and back in. `include`/`exclude` filter by component name. `max` limits cache size (LRU eviction). Useful for tab panels and multi-step forms. See also: Transition, Show."
|
|
13442
|
+
},
|
|
13443
|
+
"runtime-dom/_tpl": {
|
|
13444
|
+
signature: "_tpl(html: string): () => DocumentFragment",
|
|
13445
|
+
example: `// Compiler output (not hand-written):
|
|
13446
|
+
const _$t0 = _tpl("<div class=\"container\"><span></span></div>")`,
|
|
13447
|
+
notes: "Compiler-internal: create a template factory from an HTML string. First call parses the HTML into a `<template>` element; subsequent calls use `cloneNode(true)` for zero-parse instantiation. Not intended for direct use — the JSX compiler emits `_tpl()` calls automatically. See also: _bindText, _bindDirect."
|
|
13448
|
+
},
|
|
13449
|
+
"runtime-dom/_bindText": {
|
|
13450
|
+
signature: "_bindText(fn: () => string, node: Text): void",
|
|
13451
|
+
example: `// Compiler output for <div>{count()}</div>:
|
|
13452
|
+
const _$t = _tpl("<div> </div>")
|
|
13453
|
+
const _$n = _$t()
|
|
13454
|
+
_bindText(() => count(), _$n.firstChild)`,
|
|
13455
|
+
notes: "Compiler-internal: bind a reactive expression to a text node via `TextNode.data` assignment. Creates a `renderEffect` that re-runs when tracked signals change. Each text node gets its own independent binding for fine-grained reactivity. See also: _tpl, _bindDirect."
|
|
13456
|
+
},
|
|
13457
|
+
"runtime-dom/sanitizeHtml": {
|
|
13458
|
+
signature: "sanitizeHtml(html: string): string",
|
|
13459
|
+
example: `import { setSanitizer, sanitizeHtml } from "@pyreon/runtime-dom"
|
|
13460
|
+
setSanitizer(DOMPurify.sanitize)
|
|
13461
|
+
const clean = sanitizeHtml(userInput)`,
|
|
13462
|
+
notes: "Sanitize an HTML string using the registered sanitizer (set via `setSanitizer()`). Falls back to the identity function if no sanitizer is registered. Used by the runtime when setting `innerHTML` on elements. See also: setSanitizer."
|
|
13227
13463
|
},
|
|
13228
13464
|
"store/defineStore": {
|
|
13229
|
-
signature: "
|
|
13465
|
+
signature: "<T extends Record<string, unknown>>(id: string, setup: () => T) => () => StoreApi<T>",
|
|
13230
13466
|
example: `const useCounter = defineStore('counter', () => {
|
|
13231
13467
|
const count = signal(0)
|
|
13468
|
+
const double = computed(() => count() * 2)
|
|
13232
13469
|
const increment = () => count.update(n => n + 1)
|
|
13233
|
-
return { count, increment }
|
|
13470
|
+
return { count, double, increment }
|
|
13471
|
+
})
|
|
13472
|
+
|
|
13473
|
+
const { store, patch, subscribe, reset } = useCounter()
|
|
13474
|
+
store.count() // 0
|
|
13475
|
+
store.increment() // reactive update
|
|
13476
|
+
patch({ count: 42 })`,
|
|
13477
|
+
notes: "Define a composition-style store. The setup function runs once per store ID, returning an object whose signals become tracked state and whose functions become interceptable actions. Returns a hook function that produces a StoreApi with `.store` (user state/actions), `.patch()`, `.subscribe()`, `.onAction()`, `.reset()`, and `.dispose()`. Stores are singletons — calling the hook twice with the same ID returns the same instance. See also: StoreApi, addStorePlugin, resetStore.",
|
|
13478
|
+
mistakes: `- Calling \`useCounter()\` expecting a new instance — stores are singletons by ID, the setup function only runs once
|
|
13479
|
+
- Reading \`store.count\` without calling it — signals are functions, use \`store.count()\` to read the value
|
|
13480
|
+
- Calling \`store.count.set()\` instead of using \`patch()\` when updating multiple signals — \`patch()\` batches updates into a single notification
|
|
13481
|
+
- Forgetting \`dispose()\` in tests — store persists in the registry across test cases, leaking state. Use \`resetStore(id)\` or \`resetAllStores()\` in test cleanup`
|
|
13482
|
+
},
|
|
13483
|
+
"store/addStorePlugin": {
|
|
13484
|
+
signature: "(plugin: StorePlugin) => void",
|
|
13485
|
+
example: `addStorePlugin((api) => {
|
|
13486
|
+
api.subscribe((mutation, state) => {
|
|
13487
|
+
console.log(\`[\${api.id}] \${mutation.type}:\`, mutation.events)
|
|
13488
|
+
})
|
|
13489
|
+
})`,
|
|
13490
|
+
notes: "Register a global store plugin that runs when any store is first created. Plugin receives the full StoreApi, enabling cross-cutting concerns like logging, persistence, or devtools integration. Plugin errors are caught and logged in dev mode without breaking store creation. See also: defineStore, StoreApi."
|
|
13491
|
+
},
|
|
13492
|
+
"store/setStoreRegistryProvider": {
|
|
13493
|
+
signature: "(provider: () => Map<string, StoreApi<any>>) => void",
|
|
13494
|
+
example: `import { setStoreRegistryProvider } from '@pyreon/store'
|
|
13495
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
13496
|
+
|
|
13497
|
+
const als = new AsyncLocalStorage<Map<string, any>>()
|
|
13498
|
+
setStoreRegistryProvider(() => als.getStore() ?? new Map())`,
|
|
13499
|
+
notes: "Replace the default global store registry with a provider function. Essential for concurrent SSR — pass an AsyncLocalStorage-backed provider so each request gets isolated store state instead of sharing a single global map across concurrent requests. See also: defineStore.",
|
|
13500
|
+
mistakes: "- Forgetting to call this on the SSR server — all concurrent requests share the same store instances, causing cross-request state leaks"
|
|
13501
|
+
},
|
|
13502
|
+
"store/resetStore": {
|
|
13503
|
+
signature: "(id: string) => void",
|
|
13504
|
+
example: `resetStore('counter') // next useCounter() call creates a fresh store`,
|
|
13505
|
+
notes: "Remove a store from the registry by ID. The next call to the store hook re-runs the setup function from scratch. Useful for testing isolation and HMR. See also: resetAllStores, defineStore."
|
|
13506
|
+
},
|
|
13507
|
+
"store/resetAllStores": {
|
|
13508
|
+
signature: "() => void",
|
|
13509
|
+
example: "afterEach(() => resetAllStores())",
|
|
13510
|
+
notes: "Clear the entire store registry. All subsequent store hook calls create fresh instances. Primary use case is test cleanup and SSR request isolation. See also: resetStore."
|
|
13511
|
+
},
|
|
13512
|
+
"state-tree/model": {
|
|
13513
|
+
signature: "(definition: { state: StateShape, views?: (self: ModelSelf) => Record<string, () => any>, actions?: (self: ModelSelf) => Record<string, (...args: any[]) => any> }) => ModelDefinition",
|
|
13514
|
+
example: `const Counter = model({
|
|
13515
|
+
state: { count: 0 },
|
|
13516
|
+
views: (self) => ({
|
|
13517
|
+
doubled: () => self.count() * 2,
|
|
13518
|
+
}),
|
|
13519
|
+
actions: (self) => ({
|
|
13520
|
+
increment: () => self.count.update(n => n + 1),
|
|
13521
|
+
}),
|
|
13234
13522
|
})
|
|
13235
13523
|
|
|
13236
|
-
const {
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13524
|
+
const counter = Counter.create({ count: 10 })
|
|
13525
|
+
counter.count() // 10
|
|
13526
|
+
counter.increment()
|
|
13527
|
+
counter.doubled() // 22`,
|
|
13528
|
+
notes: "Define a structured reactive model. `state` declares signal-backed fields with their initial values. `views` are computed derivations. `actions` are the only way to mutate state — enabling middleware interception and patch recording. Returns a `ModelDefinition` with `.create(initial?)` for instances and `.asHook(id)` for singleton access. See also: getSnapshot, applySnapshot, onPatch, addMiddleware.",
|
|
13529
|
+
mistakes: `- Mutating state outside of actions — bypasses middleware and patch recording, breaks the structured contract
|
|
13530
|
+
- Forgetting that \`self.count\` is a signal — read with \`self.count()\`, write with \`self.count.set(v)\` or \`.update(fn)\` inside actions
|
|
13531
|
+
- Nesting plain objects in state instead of child models — plain objects are not signal-backed, changes to their properties are not reactive`
|
|
13532
|
+
},
|
|
13533
|
+
"state-tree/getSnapshot": {
|
|
13534
|
+
signature: "(instance: ModelInstance) => Snapshot",
|
|
13535
|
+
example: "const snap = getSnapshot(counter) // { count: 10 }",
|
|
13536
|
+
notes: "Recursively serialize a model instance into a plain JSON-safe snapshot. Reads all signal values via `.peek()` to avoid tracking subscriptions. Nested models are recursively serialized. See also: applySnapshot, model."
|
|
13537
|
+
},
|
|
13538
|
+
"state-tree/applySnapshot": {
|
|
13539
|
+
signature: "(instance: ModelInstance, snapshot: Snapshot) => void",
|
|
13540
|
+
example: "applySnapshot(counter, { count: 0 }) // reset to zero",
|
|
13541
|
+
notes: `Replace a model instance's state wholesale from a snapshot. Recursively applies to nested models. Triggers patch listeners with replace operations. See also: getSnapshot, model.`
|
|
13542
|
+
},
|
|
13543
|
+
"state-tree/onPatch": {
|
|
13544
|
+
signature: "(instance: ModelInstance, listener: PatchListener) => () => void",
|
|
13545
|
+
example: `const dispose = onPatch(counter, (patch) => {
|
|
13546
|
+
console.log(patch) // { op: 'replace', path: '/count', value: 11 }
|
|
13547
|
+
})`,
|
|
13548
|
+
notes: "Subscribe to JSON patches emitted by actions on a model instance. Each patch records the path, operation (add/replace/remove), and value. Returns an unsubscribe function. Pairs with `applyPatch` for undo/redo and state synchronization. See also: applyPatch, model."
|
|
13549
|
+
},
|
|
13550
|
+
"state-tree/applyPatch": {
|
|
13551
|
+
signature: "(instance: ModelInstance, patch: Patch | Patch[]) => void",
|
|
13552
|
+
example: `applyPatch(counter, { op: 'replace', path: '/count', value: 0 })`,
|
|
13553
|
+
notes: "Apply one or more JSON patches to a model instance. Accepts a single patch or an array for batch replay. Used with `onPatch` for undo/redo and state synchronization. See also: onPatch, model."
|
|
13554
|
+
},
|
|
13555
|
+
"state-tree/addMiddleware": {
|
|
13556
|
+
signature: "(instance: ModelInstance, middleware: MiddlewareFn) => () => void",
|
|
13557
|
+
example: `addMiddleware(counter, (call, next) => {
|
|
13558
|
+
console.log(\`\${call.name}(\${call.args.join(', ')})\`)
|
|
13559
|
+
return next(call)
|
|
13560
|
+
})`,
|
|
13561
|
+
notes: "Add an action interception middleware to a model instance. The middleware receives the action call context and a `next` function — call `next(call)` to proceed or return early to block the action. Returns an unsubscribe function. See also: model."
|
|
13562
|
+
},
|
|
13563
|
+
"validation/zodSchema": {
|
|
13564
|
+
signature: "<T>(schema: ZodType<T>) => SchemaAdapter<T>",
|
|
13565
|
+
example: `const schema = z.object({ email: z.string().email(), age: z.number().min(18) })
|
|
13566
|
+
const form = useForm({
|
|
13567
|
+
initialValues: { email: '', age: 0 },
|
|
13568
|
+
schema: zodSchema(schema),
|
|
13569
|
+
onSubmit: (values) => save(values),
|
|
13570
|
+
})`,
|
|
13571
|
+
notes: "Create a whole-form schema adapter from a Zod schema. Duck-typed against the `.safeParse()` method so it works with both Zod v3 and v4 without version checks. Pass the result to `useForm({ schema })` for automatic full-form validation on submit or blur. See also: zodField, valibotSchema, arktypeSchema.",
|
|
13572
|
+
mistakes: `- Passing zodSchema AND per-field validators for the same field — both run and errors may conflict
|
|
13573
|
+
- Using zodSchema with a non-object schema (z.string()) — form schemas must validate an object shape matching initialValues`
|
|
13574
|
+
},
|
|
13575
|
+
"validation/zodField": {
|
|
13576
|
+
signature: "<T>(schema: ZodType<T>) => ValidateFn<T>",
|
|
13577
|
+
example: `const form = useForm({
|
|
13578
|
+
initialValues: { username: '' },
|
|
13579
|
+
validators: { username: zodField(z.string().min(3).max(20)) },
|
|
13580
|
+
onSubmit: (values) => save(values),
|
|
13581
|
+
})`,
|
|
13582
|
+
notes: `Create a per-field validator from a Zod schema. Returns a function compatible with \`useForm({ validators: { fieldName: zodField(z.string().email()) } })\`. Use when individual fields have independent validation rules that don't need cross-field context. See also: zodSchema, valibotField.`
|
|
13583
|
+
},
|
|
13584
|
+
"validation/valibotSchema": {
|
|
13585
|
+
signature: "<T>(schema: ValibotSchema<T>, safeParse: SafeParseFn) => SchemaAdapter<T>",
|
|
13586
|
+
example: `import * as v from 'valibot'
|
|
13587
|
+
const schema = v.object({ email: v.pipe(v.string(), v.email()) })
|
|
13588
|
+
const form = useForm({
|
|
13589
|
+
initialValues: { email: '' },
|
|
13590
|
+
schema: valibotSchema(schema, v.safeParse),
|
|
13591
|
+
onSubmit: (values) => save(values),
|
|
13592
|
+
})`,
|
|
13593
|
+
notes: `Create a whole-form schema adapter from a Valibot schema. Requires passing the \`safeParse\` function explicitly (Valibot uses standalone functions, not methods). This keeps the adapter independent of Valibot's internal module structure across versions. See also: valibotField, zodSchema.`,
|
|
13594
|
+
mistakes: "- Forgetting to pass v.safeParse as the second argument — the adapter cannot call safeParse without it since Valibot uses standalone functions"
|
|
13595
|
+
},
|
|
13596
|
+
"validation/valibotField": {
|
|
13597
|
+
signature: "<T>(schema: ValibotSchema<T>, safeParse: SafeParseFn) => ValidateFn<T>",
|
|
13598
|
+
example: "validators: { email: valibotField(v.pipe(v.string(), v.email()), v.safeParse) }",
|
|
13599
|
+
notes: "Create a per-field validator from a Valibot schema. Same standalone-function-style as valibotSchema — pass `v.safeParse` explicitly. See also: valibotSchema, zodField."
|
|
13600
|
+
},
|
|
13601
|
+
"validation/arktypeSchema": {
|
|
13602
|
+
signature: "<T>(schema: ArkTypeSchema<T>) => SchemaAdapter<T>",
|
|
13603
|
+
example: `import { type } from 'arktype'
|
|
13604
|
+
const schema = type({ email: 'email', age: 'number > 18' })
|
|
13605
|
+
const form = useForm({
|
|
13606
|
+
initialValues: { email: '', age: 0 },
|
|
13607
|
+
schema: arktypeSchema(schema),
|
|
13608
|
+
onSubmit: (values) => save(values),
|
|
13609
|
+
})`,
|
|
13610
|
+
notes: "Create a whole-form schema adapter from an ArkType type. ArkType validation is synchronous only — async validators are not supported through this adapter. Returns errors via the ArkType `problems` array. See also: arktypeField, zodSchema."
|
|
13611
|
+
},
|
|
13612
|
+
"validation/arktypeField": {
|
|
13613
|
+
signature: "<T>(schema: ArkTypeSchema<T>) => ValidateFn<T>",
|
|
13614
|
+
example: `validators: { age: arktypeField(type('number > 18')) }`,
|
|
13615
|
+
notes: "Create a per-field validator from an ArkType type. Synchronous only, same as arktypeSchema. See also: arktypeSchema, zodField."
|
|
13240
13616
|
},
|
|
13241
13617
|
"form/useForm": {
|
|
13242
|
-
signature: "
|
|
13618
|
+
signature: "<TValues extends Record<string, unknown>>(options: UseFormOptions<TValues>) => FormState<TValues>",
|
|
13243
13619
|
example: `const form = useForm({
|
|
13244
|
-
initialValues: {
|
|
13245
|
-
|
|
13246
|
-
|
|
13620
|
+
initialValues: { email: '', password: '' },
|
|
13621
|
+
validators: {
|
|
13622
|
+
email: (v) => (!v ? 'Required' : undefined),
|
|
13623
|
+
password: (v, all) => (v.length < 8 ? 'Too short' : undefined),
|
|
13624
|
+
},
|
|
13625
|
+
onSubmit: async (values) => { await login(values) },
|
|
13247
13626
|
})
|
|
13248
13627
|
|
|
13249
|
-
|
|
13250
|
-
form.
|
|
13251
|
-
|
|
13628
|
+
// Bind inputs with register():
|
|
13629
|
+
// h('input', form.register('email'))
|
|
13630
|
+
// h('input', { type: 'checkbox', ...form.register('remember', { type: 'checkbox' }) })`,
|
|
13631
|
+
notes: `Create a signal-based form. \`initialValues\` drives field keys and types end-to-end — TValues is inferred from it, so all downstream typings (\`useField\` field name, \`useWatch\` keys, validator signatures) are fully typed without annotation. Returns \`FormState<TValues>\` with per-field signals, form-level signals (\`isSubmitting\`, \`isValidating\`, \`isValid\`, \`isDirty\`, \`submitCount\`, \`submitError\`), and handlers (\`handleSubmit\`, \`reset\`, \`validate\`). \`validateOn\` defaults to \`"blur"\` (not \`"change"\`) so users aren't scolded mid-keystroke; optional \`schema\` integrates with \`@pyreon/validation\` adapters (\`zodSchema\`, \`valibotSchema\`, \`arktypeSchema\`) for whole-form validation after per-field validators run. See also: useField, FormProvider, useFormState.`,
|
|
13632
|
+
mistakes: `- Mutating \`initialValues\` after creation — it is read once at setup; use \`setFieldValue\` for programmatic updates
|
|
13633
|
+
- Reading \`form.fields[name].value\` as a plain value — it is \`Signal<T>\`, call it: \`form.fields.email.value()\`
|
|
13634
|
+
- Passing \`validateOn: "change"\` without \`debounceMs\` on async validators — fires a network request on every keystroke
|
|
13635
|
+
- Calling \`form.handleSubmit()\` without attaching it as a form \`onSubmit\` handler — it calls \`preventDefault()\` so it must receive the form event, or be called with no argument for programmatic submit
|
|
13636
|
+
- Forgetting that \`schema\` runs AFTER per-field \`validators\` — errors from both sources merge; if a field validator already set an error, the schema can override it`
|
|
13252
13637
|
},
|
|
13253
13638
|
"form/useField": {
|
|
13254
|
-
signature: "
|
|
13255
|
-
example: `
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13639
|
+
signature: "<TValues, K extends keyof TValues & string>(form: FormState<TValues>, name: K) => UseFieldResult<TValues[K]>",
|
|
13640
|
+
example: `function EmailField({ form }: { form: FormState<{ email: string }> }) {
|
|
13641
|
+
const field = useField(form, 'email')
|
|
13642
|
+
return (
|
|
13643
|
+
<>
|
|
13644
|
+
<input {...field.register()} />
|
|
13645
|
+
{() => field.showError() && <span>{field.error()}</span>}
|
|
13646
|
+
</>
|
|
13647
|
+
)
|
|
13648
|
+
}`,
|
|
13649
|
+
notes: `Extract a single field's state and helpers from a form instance — avoids passing the entire \`FormState\` to leaf components. Returns all \`FieldState\` signals (\`value\`, \`error\`, \`touched\`, \`dirty\`) plus two convenience computeds: \`hasError\` (true when an error string exists) and \`showError\` (true when touched AND errored — the typical UI condition for gating error display). Also exposes \`register(opts?)\` to bind an \`<input>\` element with a single spread. See also: useForm, useWatch.`,
|
|
13650
|
+
mistakes: `- Destructuring \`const { value } = useField(form, "email")\` and calling \`value()\` — works, but the getter evaluates to the Signal itself; storing \`value()\` at setup captures the initial value and defeats reactivity
|
|
13651
|
+
- Forgetting \`showError\` and reimplementing \`touched() && hasError()\` in every template — \`showError\` is a \`Computed<boolean>\`, use it directly`
|
|
13652
|
+
},
|
|
13653
|
+
"form/useFieldArray": {
|
|
13654
|
+
signature: "<T>(initial?: T[]) => UseFieldArrayResult<T>",
|
|
13655
|
+
example: `const tags = useFieldArray<string>([])
|
|
13656
|
+
tags.append('typescript')
|
|
13657
|
+
tags.prepend('signals')
|
|
13658
|
+
tags.insert(1, 'reactive')
|
|
13659
|
+
tags.move(0, 2)
|
|
13660
|
+
tags.remove(0)
|
|
13661
|
+
|
|
13662
|
+
// Keyed rendering — never drop the \`by={i => i.key}\`
|
|
13663
|
+
<For each={tags.items()} by={(i) => i.key}>
|
|
13664
|
+
{(item) => <input value={item.value()} onInput={(e) => item.value.set(e.currentTarget.value)} />}
|
|
13665
|
+
</For>`,
|
|
13666
|
+
notes: "Manage a dynamic array of form fields with stable keys. Each item is `{ key: number, value: Signal<T> }` — use `item.key` inside `<For by={i => i.key}>` so reordering / inserts do not remount child components. Full mutation surface: `append`, `prepend`, `insert`, `remove`, `update`, `move`, `swap`, `replace`. See also: useForm.",
|
|
13667
|
+
mistakes: `- Rendering with <For by={(_, i) => i}> — index-based keys lose identity on reorder, defeating the stable-key design
|
|
13668
|
+
- Calling tags.items() inside setup and storing the array — it is a Signal, read inside reactive scopes`
|
|
13669
|
+
},
|
|
13670
|
+
"form/useWatch": {
|
|
13671
|
+
signature: "(form, name) => Signal<TValues[K]> | (form, names[]) => Signal<T>[] | (form) => Computed<TValues>",
|
|
13672
|
+
example: `const email = useWatch(form, 'email') // Signal<string>
|
|
13673
|
+
const [first, last] = useWatch(form, ['firstName', 'lastName'])
|
|
13674
|
+
const everything = useWatch(form) // Computed<TValues>
|
|
13675
|
+
|
|
13676
|
+
// Derive and sync: preview displays the email as the user types.
|
|
13677
|
+
effect(() => { preview.set(\`Hello \${email()}\`) })`,
|
|
13678
|
+
notes: "Typed overloads for reactively watching form field values. Single-field form returns `Signal<T>` (fast path — same signal, no wrapper), multi-field returns a tuple of signals, no-args returns a `Computed<TValues>` over the whole values object. Prefer the narrowest form — watching everything re-runs your effect when ANY field changes. See also: useFormState, useField.",
|
|
13679
|
+
mistakes: "- Using the all-fields overload (`useWatch(form)`) to derive a single computed — re-runs when any field changes, not just the one you care about. Use `useWatch(form, \"email\")` for single-field precision"
|
|
13680
|
+
},
|
|
13681
|
+
"form/useFormState": {
|
|
13682
|
+
signature: "<TValues, T>(form: FormState<TValues>, selector?: (s: FormStateSummary) => T) => Computed<T>",
|
|
13683
|
+
example: `const canSubmit = useFormState(form, (s) => s.isValid && !s.isSubmitting && s.isDirty)
|
|
13684
|
+
<button disabled={() => !canSubmit()}>Save</button>`,
|
|
13685
|
+
notes: "Computed summary of form-level state (`isValid`, `isDirty`, `isSubmitting`, `isValidating`, `submitCount`, `errors`). Passing a selector restricts the tracked subset — a button driven by `canSubmit` should not re-render just because `submitCount` changed. Without a selector, the computed re-derives on ANY form-level state change. See also: useForm, useWatch.",
|
|
13686
|
+
mistakes: "- Omitting the selector and reading `useFormState(form)` as a whole — triggers on every field change, every validation, every submit count bump. Always pass a selector for UI-bound computeds"
|
|
13687
|
+
},
|
|
13688
|
+
"form/FormProvider": {
|
|
13689
|
+
signature: "<TValues>(props: { form: FormState<TValues>; children: VNodeChild }) => VNode",
|
|
13690
|
+
example: `<FormProvider form={form}>
|
|
13691
|
+
<PersonalInfoSection />
|
|
13692
|
+
<AddressSection />
|
|
13693
|
+
<SubmitButton />
|
|
13694
|
+
</FormProvider>
|
|
13695
|
+
|
|
13696
|
+
// Inside any descendant:
|
|
13697
|
+
const form = useFormContext<typeof values>()`,
|
|
13698
|
+
notes: "Provide a form via context so nested components can read it with `useFormContext<TValues>()` without prop-drilling. Every call to `useFormContext` inside the provider tree returns the same `FormState` instance. Nest inside `PyreonUI` or any other provider — the form context is independent. See also: useFormContext, useForm.",
|
|
13699
|
+
mistakes: "- Nesting `FormProvider` within itself expecting scoped forms — the inner provider shadows the outer; for multi-form pages, use separate providers at sibling level, not nested"
|
|
13700
|
+
},
|
|
13701
|
+
"form/useFormContext": {
|
|
13702
|
+
signature: "<TValues>() => FormState<TValues>",
|
|
13703
|
+
example: `const form = useFormContext<{ email: string; password: string }>()
|
|
13704
|
+
const field = useField(form, 'email')`,
|
|
13705
|
+
notes: "Read the nearest `FormProvider` form from context. Throws at dev time if no provider is mounted above the call site. Pass the expected `TValues` generic so downstream typings (`useField` field names, `useWatch` keys) stay end-to-end typed. Returns the same `FormState<TValues>` instance that was passed to `FormProvider`. See also: FormProvider, useForm.",
|
|
13706
|
+
mistakes: `- Calling at module scope — hooks require an active component setup context; call inside a component body
|
|
13707
|
+
- Omitting the \`<TValues>\` generic — TypeScript infers \`FormState<Record<string, unknown>>\` and \`useField\` field names lose type narrowing`
|
|
13708
|
+
},
|
|
13709
|
+
"query/QueryClientProvider": {
|
|
13710
|
+
signature: "(props: { client: QueryClient; children: VNodeChild }) => VNode",
|
|
13711
|
+
example: `const client = new QueryClient()
|
|
13712
|
+
<QueryClientProvider client={client}>
|
|
13713
|
+
<App />
|
|
13714
|
+
</QueryClientProvider>`,
|
|
13715
|
+
notes: "Mounts a `QueryClient` at the root of the component tree via context so every descendant hook (`useQuery`, `useMutation`, `useSubscription`, `useSSE`, etc.) can reach it via `useQueryClient()`. Must wrap the app — omitting it causes a runtime throw on the first hook call. One provider per app; nested providers are not supported (the deepest one wins, silently shadowing the outer). See also: useQueryClient, QueryClient.",
|
|
13716
|
+
mistakes: `- Forgetting to wrap the app — every query/mutation hook throws "No QueryClient set" at runtime
|
|
13717
|
+
- Creating the \`QueryClient\` inside a component body — it re-creates on every render. Hoist to module scope or use \`useMemo\`-equivalent (\`const client = useMemo(() => new QueryClient())\`)
|
|
13718
|
+
- Nesting providers expecting scoped caches — only one provider is supported; the deepest one wins silently`
|
|
13259
13719
|
},
|
|
13260
13720
|
"query/useQuery": {
|
|
13261
|
-
signature: "
|
|
13262
|
-
example: `const
|
|
13263
|
-
|
|
13264
|
-
|
|
13721
|
+
signature: "<TData, TError, TKey>(options: () => QueryObserverOptions<...>) => UseQueryResult<TData, TError>",
|
|
13722
|
+
example: `const userId = signal(1)
|
|
13723
|
+
const user = useQuery(() => ({
|
|
13724
|
+
queryKey: ['user', userId()],
|
|
13725
|
+
queryFn: () => fetch(\`/api/users/\${userId()}\`).then((r) => r.json()),
|
|
13726
|
+
}))
|
|
13727
|
+
// user.data(), user.error(), user.isFetching() — each its own signal`,
|
|
13728
|
+
notes: `Subscribe to a query with fine-grained reactive signals. \`options\` is a FUNCTION (not an object) so it can read Pyreon signals — when a tracked signal inside changes (e.g. a reactive queryKey), the observer re-evaluates options and refetches automatically. Returns one independent \`Signal<T>\` per observer field (\`data\`, \`error\`, \`status\`, \`isPending\`, \`isLoading\`, \`isFetching\`, \`isError\`, \`isSuccess\`) so templates only re-run for the exact fields they read. Internally wraps TanStack's \`QueryObserver\` and subscribes via \`onUnmount\`-guarded effect — the observer unsubscribes when the component unmounts. See also: useQueryClient, useMutation, useSuspenseQuery.`,
|
|
13729
|
+
mistakes: `- Passing the options object directly instead of a function — loses reactive queryKey support; the observer never re-evaluates when signals change
|
|
13730
|
+
- Reading \`.data\` / \`.error\` / \`.isFetching\` as plain values — they are \`Signal<T>\`, call them: \`user.data()\`, \`user.isFetching()\`
|
|
13731
|
+
- Destructuring \`const { data } = useQuery(...)\` at setup and reading \`data\` later — captures the Signal reference once, which is fine, but storing \`data()\` at setup captures the initial VALUE and defeats reactivity
|
|
13732
|
+
- Returning \`user.data()\` at the top of a component body instead of inside a reactive accessor — components run once; read signals inside \`() => user.data()?.name\` or effects
|
|
13733
|
+
- Expecting refetch on \`queryFn\` closure changes alone — only signals read inside the options function trigger re-evaluation; a closure capture of a \`let\` variable does not`
|
|
13734
|
+
},
|
|
13735
|
+
"query/useMutation": {
|
|
13736
|
+
signature: "<TData, TError, TVars, TCtx>(options: MutationObserverOptions<...>) => UseMutationResult<TData, TError, TVars, TCtx>",
|
|
13737
|
+
example: `const create = useMutation({
|
|
13738
|
+
mutationFn: (input) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(input) }).then(r => r.json()),
|
|
13739
|
+
onSuccess: () => client.invalidateQueries({ queryKey: ['posts'] }),
|
|
13740
|
+
})
|
|
13741
|
+
// <button onClick={() => create.mutate({ title: 'New' })}>Create</button>`,
|
|
13742
|
+
notes: "Run a mutation (create / update / delete). Returns reactive `pending` / `success` / `error` signals plus two firing modes: `mutate(vars)` (fire-and-forget — errors go to the `error` signal) and `mutateAsync(vars)` (returns a promise for try/catch). `reset()` returns state to idle. Unlike `useQuery`, options is a plain OBJECT (not a function) because mutations are imperative — there are no reactive queryKeys to re-evaluate, so the function-wrapper overhead would add no value. `onSuccess` / `onError` / `onSettled` callbacks fire synchronously after the mutation resolves, useful for cache invalidation (`client.invalidateQueries`). See also: useQuery, useIsMutating.",
|
|
13743
|
+
mistakes: `- \`mutate()\` swallows errors into the \`error\` signal — use \`mutateAsync()\` with try/catch if you need programmatic error handling
|
|
13744
|
+
- Calling \`mutate()\` inside a \`useQuery\` \`queryFn\` — mutations are imperative user actions, not data-fetching side effects; this causes infinite loops if the mutation invalidates the query that spawned it
|
|
13745
|
+
- Reading \`mutation.data()\` outside a reactive scope — same rule as \`useQuery\`: read inside \`() => mutation.data()\` or effects`
|
|
13746
|
+
},
|
|
13747
|
+
"query/useInfiniteQuery": {
|
|
13748
|
+
signature: "<TQueryFnData, TError>(options: () => InfiniteQueryObserverOptions<...>) => UseInfiniteQueryResult<TQueryFnData, TError>",
|
|
13749
|
+
example: `const feed = useInfiniteQuery(() => ({
|
|
13750
|
+
queryKey: ['feed'],
|
|
13751
|
+
queryFn: ({ pageParam }) => fetchPage(pageParam),
|
|
13752
|
+
initialPageParam: 0,
|
|
13753
|
+
getNextPageParam: (last) => last.nextCursor,
|
|
13754
|
+
}))`,
|
|
13755
|
+
notes: "Paginated / cursor-based query. Returns reactive `data` (wrapping `InfiniteData<T>` with `.pages` + `.pageParams`), `hasNextPage` / `hasPreviousPage` booleans, and `fetchNextPage` / `fetchPreviousPage` trigger functions. Options is a function (same reactive-tracking contract as `useQuery`). `getNextPageParam` / `getPreviousPageParam` drive cursor progression — return `undefined` to signal the end. See also: useQuery, useSuspenseInfiniteQuery.",
|
|
13756
|
+
mistakes: `- Forgetting \`initialPageParam\` — required by TanStack v5; omitting it throws at the first \`queryFn\` call
|
|
13757
|
+
- Using \`data().pages\` without flattening — \`pages\` is an array of page results; most UIs want \`data().pages.flat()\` or \`data().pages.flatMap(p => p.items)\``
|
|
13758
|
+
},
|
|
13759
|
+
"query/useQueries": {
|
|
13760
|
+
signature: "(queries: () => UseQueriesOptions[]) => Signal<QueryObserverResult[]>",
|
|
13761
|
+
example: `const results = useQueries(() =>
|
|
13762
|
+
userIds().map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id) })),
|
|
13763
|
+
)
|
|
13764
|
+
// results() is QueryObserverResult[] — one entry per input query`,
|
|
13765
|
+
notes: "Subscribe to multiple queries in parallel. Returns a `Signal<QueryObserverResult[]>` — one entry per input query. Options is a function so the query list can depend on signals (e.g. derive one query per item in a reactive array). Each inner query independently tracks its own `data` / `error` / `isFetching` — the outer signal fires when ANY inner query updates. See also: useQuery.",
|
|
13766
|
+
mistakes: `- Expecting per-query fine-grained signals — \`useQueries\` returns a single combined signal, not individual \`UseQueryResult\` objects. For independent per-query tracking, call \`useQuery\` N times
|
|
13767
|
+
- Passing a static array instead of a function — loses reactive query-list tracking; if the list of IDs changes (e.g. \`userIds()\` is a signal), the queries won't re-evaluate. Always wrap: \`useQueries(() => ids().map(...))\``
|
|
13768
|
+
},
|
|
13769
|
+
"query/useSubscription": {
|
|
13770
|
+
signature: "(options: UseSubscriptionOptions) => UseSubscriptionResult",
|
|
13771
|
+
example: `const sub = useSubscription({
|
|
13772
|
+
url: 'wss://api.example.com/feed',
|
|
13773
|
+
onMessage: (event, client) => {
|
|
13774
|
+
if (JSON.parse(event.data).type === 'post-created') {
|
|
13775
|
+
client.invalidateQueries({ queryKey: ['posts'] })
|
|
13776
|
+
}
|
|
13777
|
+
},
|
|
13778
|
+
})
|
|
13779
|
+
// sub.status() — 'connecting' | 'connected' | 'disconnected' | 'error'
|
|
13780
|
+
// sub.send(data), sub.close(), sub.reconnect()`,
|
|
13781
|
+
notes: "Reactive WebSocket with auto-reconnect and QueryClient cache integration. `onMessage` receives the active `QueryClient` so push updates can invalidate or directly patch cached queries in a single line. Exponential backoff on reconnect (default 1s doubling, max 10 attempts — configurable via `reconnectDelay` / `maxReconnectAttempts`). `url` and `enabled` may be signals for reactive connection management — changing the URL closes the old socket and opens a new one. Returns `status` (signal), `send(data)`, `close()`, `reconnect()`. See also: useSSE, useQuery.",
|
|
13782
|
+
mistakes: `- \`onMessage\` runs on every frame the socket receives — debounce cache invalidations for high-frequency streams or you'll trigger N refetches per second
|
|
13783
|
+
- Storing data in a parallel signal instead of using \`queryClient.setQueryData\` inside \`onMessage\` — defeats the QueryClient cache; use \`setQueryData\` to push updates into the same cache that \`useQuery\` reads
|
|
13784
|
+
- Forgetting \`enabled: false\` on unmount-sensitive connections — the WebSocket stays open unless \`enabled\` is a signal that tracks component lifecycle or a reactive condition`
|
|
13785
|
+
},
|
|
13786
|
+
"query/useSSE": {
|
|
13787
|
+
signature: "<T>(options: UseSSEOptions<T>) => UseSSEResult<T>",
|
|
13788
|
+
example: `const sse = useSSE({
|
|
13789
|
+
url: '/api/events',
|
|
13790
|
+
parse: JSON.parse,
|
|
13791
|
+
onMessage: (data, queryClient) => {
|
|
13792
|
+
if (data.type === 'order-updated') {
|
|
13793
|
+
queryClient.invalidateQueries({ queryKey: ['orders'] })
|
|
13794
|
+
}
|
|
13795
|
+
},
|
|
13796
|
+
})
|
|
13797
|
+
// sse.data() — last parsed message
|
|
13798
|
+
// sse.status() — 'connecting' | 'connected' | 'disconnected' | 'error'
|
|
13799
|
+
// sse.lastEventId(), sse.readyState(), sse.close(), sse.reconnect()`,
|
|
13800
|
+
notes: "Reactive Server-Sent Events hook with QueryClient cache integration. Same pattern as `useSubscription` but read-only (no `send`). `parse` deserializes raw event data per message (e.g. `JSON.parse`); `events` filters named SSE event types (defaults to generic `message` events). Honours the SSE spec `id` field via `lastEventId()` so the browser includes `Last-Event-ID` on reconnect and the server can resume from the right offset. `onMessage` receives the `QueryClient` for cache invalidation. See also: useSubscription.",
|
|
13801
|
+
mistakes: `- Passing \`queryKey\` (TanStack v4 pattern) instead of using \`onMessage\` for cache integration — Pyreon's \`useSSE\` does NOT auto-update query cache; use \`queryClient.setQueryData\` or \`invalidateQueries\` inside \`onMessage\`
|
|
13802
|
+
- Omitting \`parse\` and expecting typed data — without \`parse\`, \`data()\` is \`string\` (raw event payload); pass \`parse: JSON.parse\` for auto-deserialization`
|
|
13803
|
+
},
|
|
13804
|
+
"query/useSuspenseQuery": {
|
|
13805
|
+
signature: "<TData, TError>(options: () => QueryObserverOptions<...>) => UseSuspenseQueryResult<TData, TError>",
|
|
13806
|
+
example: `const user = useSuspenseQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))
|
|
13807
|
+
|
|
13808
|
+
<QuerySuspense query={user} fallback={<Spinner />}>
|
|
13809
|
+
{() => <UserCard name={user.data().name} />}
|
|
13810
|
+
</QuerySuspense>`,
|
|
13811
|
+
notes: "Like `useQuery` but `data` is narrowed to `Signal<TData>` (never undefined). Designed for use inside a `QuerySuspense` boundary that guarantees children only render after the query succeeds — read `user.data().name` unconditionally, no `undefined` guard needed. The Suspense-mode observer fires a background refetch but never transitions `data` back to `undefined` (the previous data is retained as placeholder). `useSuspenseInfiniteQuery` is the equivalent for paginated queries. See also: QuerySuspense, useSuspenseInfiniteQuery, useQuery.",
|
|
13812
|
+
mistakes: `- Using \`useSuspenseQuery\` without a \`QuerySuspense\` wrapper — the narrowed type assumes a boundary guarantees data; without it, \`data()\` CAN be the initial value during the first render cycle
|
|
13813
|
+
- Mixing \`useSuspenseQuery\` and \`useQuery\` for the same \`queryKey\` — the Suspense observer and the regular observer can race; use one or the other per key`
|
|
13814
|
+
},
|
|
13815
|
+
"query/useSuspenseInfiniteQuery": {
|
|
13816
|
+
signature: "<TQueryFnData, TError>(options: () => InfiniteQueryObserverOptions<...>) => UseSuspenseInfiniteQueryResult<TQueryFnData, TError>",
|
|
13817
|
+
example: `const feed = useSuspenseInfiniteQuery(() => ({
|
|
13818
|
+
queryKey: ['feed'],
|
|
13819
|
+
queryFn: ({ pageParam }) => fetchPage(pageParam),
|
|
13820
|
+
initialPageParam: 0,
|
|
13821
|
+
getNextPageParam: (last) => last.nextCursor,
|
|
13822
|
+
}))
|
|
13823
|
+
|
|
13824
|
+
<QuerySuspense query={feed} fallback={<Spinner />}>
|
|
13825
|
+
{() => <Feed pages={feed.data().pages} onMore={feed.fetchNextPage} />}
|
|
13826
|
+
</QuerySuspense>`,
|
|
13827
|
+
notes: "Like `useInfiniteQuery` but `data` is narrowed to `Signal<InfiniteData<TQueryFnData>>` (never undefined) — for use inside a `QuerySuspense` boundary. Returns the same `fetchNextPage` / `fetchPreviousPage` / `hasNextPage` / `hasPreviousPage` surface as `useInfiniteQuery`. Same caveats as `useSuspenseQuery` regarding Suspense boundary requirement. See also: useSuspenseQuery, useInfiniteQuery, QuerySuspense.",
|
|
13828
|
+
mistakes: `- Using without a \`QuerySuspense\` wrapper — same boundary-requirement as \`useSuspenseQuery\`; the narrowed type assumes success, but \`data()\` CAN be the initial value during the first render cycle without a boundary
|
|
13829
|
+
- Mixing \`useSuspenseInfiniteQuery\` and \`useInfiniteQuery\` for the same \`queryKey\` — the Suspense observer and the regular observer can race; use one or the other per key`
|
|
13830
|
+
},
|
|
13831
|
+
"query/QuerySuspense": {
|
|
13832
|
+
signature: "(props: QuerySuspenseProps) => VNodeChild",
|
|
13833
|
+
example: `<QuerySuspense
|
|
13834
|
+
query={[userQuery, postsQuery]}
|
|
13835
|
+
fallback={<Spinner />}
|
|
13836
|
+
error={(err) => <ErrorCard message={String(err)} />}
|
|
13837
|
+
>
|
|
13838
|
+
{() => <Dashboard user={userQuery.data()} posts={postsQuery.data()} />}
|
|
13839
|
+
</QuerySuspense>`,
|
|
13840
|
+
notes: "Pyreon-native Suspense boundary for queries — replaces `<Suspense>` for the query use case with explicit error handling. Shows `fallback` while any query is `isPending`. On error, renders the `error` callback or re-throws to the nearest `ErrorBoundary`. Accepts a single query or an array — pass an array to gate on multiple queries in parallel. Children are a function (`{() => <UI />}`) so they only execute after all queries succeed. See also: useSuspenseQuery, useSuspenseInfiniteQuery.",
|
|
13841
|
+
mistakes: `- Passing children as plain JSX (\`<QuerySuspense query={q}><Data /></QuerySuspense>\`) instead of a function — plain children evaluate eagerly, defeating the Suspense gate. Always wrap: \`{() => <Data />}\`
|
|
13842
|
+
- Omitting the \`error\` callback — errors re-throw to the nearest \`ErrorBoundary\`, which may not exist or may be too far up the tree. Provide an explicit \`error\` fallback for precise error handling`
|
|
13843
|
+
},
|
|
13844
|
+
"query/useIsFetching": {
|
|
13845
|
+
signature: "(filters?: QueryFilters) => Signal<number>",
|
|
13846
|
+
example: `const fetching = useIsFetching()
|
|
13847
|
+
// <TopSpinner visible={() => fetching() > 0} />`,
|
|
13848
|
+
notes: "Global reactive count of currently-fetching queries. Pass `QueryFilters` to narrow by `queryKey` prefix, `stale` status, or `fetchStatus`. Pair with `useIsMutating` to drive a top-of-page progress bar that aggregates ALL in-flight data fetching without tracking individual queries. Returns `Signal<number>` — zero when idle. See also: useIsMutating."
|
|
13849
|
+
},
|
|
13850
|
+
"query/useIsMutating": {
|
|
13851
|
+
signature: "(filters?: MutationFilters) => Signal<number>",
|
|
13852
|
+
example: `const mutating = useIsMutating()
|
|
13853
|
+
// <Banner visible={() => mutating() > 0}>Saving…</Banner>`,
|
|
13854
|
+
notes: "Global reactive count of currently-running mutations (optionally filtered by `MutationFilters`). Same pattern as `useIsFetching` but for the mutation pipeline. Returns `Signal<number>` — zero when no mutations are in flight. See also: useIsFetching."
|
|
13855
|
+
},
|
|
13856
|
+
"query/QueryErrorResetBoundary": {
|
|
13857
|
+
signature: "(props: QueryErrorResetBoundaryProps) => VNodeChild",
|
|
13858
|
+
example: `<QueryErrorResetBoundary>
|
|
13859
|
+
{(reset) => (
|
|
13860
|
+
<ErrorBoundary fallback={(err, retry) => <button onClick={() => { reset(); retry() }}>Retry</button>}>
|
|
13861
|
+
<QuerySuspense query={q}>{() => <Data />}</QuerySuspense>
|
|
13862
|
+
</ErrorBoundary>
|
|
13863
|
+
)}
|
|
13864
|
+
</QueryErrorResetBoundary>`,
|
|
13865
|
+
notes: "Resets errored queries inside its subtree when a sibling `ErrorBoundary` recovers. Wrap around a `QuerySuspense` + `ErrorBoundary` pair to get clean retry semantics — without this, a recovered `ErrorBoundary` re-renders children but the queries still hold their error state, so the boundary immediately catches the same error again (infinite error loop). Accepts a render function child `{(reset) => ...}` so the reset action can be wired to a retry button. See also: QuerySuspense."
|
|
13866
|
+
},
|
|
13867
|
+
"query/useQueryErrorResetBoundary": {
|
|
13868
|
+
signature: "() => { reset: () => void }",
|
|
13869
|
+
example: `const { reset } = useQueryErrorResetBoundary()
|
|
13870
|
+
// Inside an ErrorBoundary fallback:
|
|
13871
|
+
<button onClick={() => { reset(); retry() }}>Try again</button>`,
|
|
13872
|
+
notes: `Imperative access to the nearest \`QueryErrorResetBoundary\`. Returns \`{ reset }\` — call \`reset()\` to clear errored queries in the subtree. Useful when an error fallback has its own retry button outside the render-prop form of \`QueryErrorResetBoundary\`, e.g. inside a standalone \`ErrorBoundary\` fallback component that isn't a direct child of the boundary. See also: QueryErrorResetBoundary.`
|
|
13873
|
+
},
|
|
13874
|
+
"query/useQueryClient": {
|
|
13875
|
+
signature: "() => QueryClient",
|
|
13876
|
+
example: `const client = useQueryClient()
|
|
13877
|
+
client.invalidateQueries({ queryKey: ['posts'] })
|
|
13878
|
+
await client.prefetchQuery({ queryKey: ['user', 1], queryFn: fetchUser })`,
|
|
13879
|
+
notes: "Access the nearest `QueryClient` from context. Used to invalidate queries (`client.invalidateQueries`), prefetch data (`client.prefetchQuery`), read/write cache (`getQueryData` / `setQueryData`), or cancel queries. Throws \"[Pyreon] No QueryClient set\" if no `QueryClientProvider` is mounted above the call site. Returns the same `QueryClient` instance that TanStack core exposes — all TanStack methods work. See also: QueryClientProvider.",
|
|
13880
|
+
mistakes: "- Calling `useQueryClient()` at module scope — hooks require an active component setup context; hoist into the component body or pass the client as a function parameter"
|
|
13881
|
+
},
|
|
13882
|
+
"query/TanStack core re-exports": {
|
|
13883
|
+
signature: `import { QueryClient, QueryCache, MutationCache, dehydrate, hydrate, keepPreviousData, hashKey, isCancelledError, CancelledError, defaultShouldDehydrateQuery, defaultShouldDehydrateMutation } from '@pyreon/query'`,
|
|
13884
|
+
example: `// SSR dehydration round-trip:
|
|
13885
|
+
import { QueryClient, dehydrate, hydrate } from '@pyreon/query'
|
|
13886
|
+
|
|
13887
|
+
const server = new QueryClient()
|
|
13888
|
+
await server.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers })
|
|
13889
|
+
const snapshot = dehydrate(server)
|
|
13890
|
+
|
|
13891
|
+
const client = new QueryClient()
|
|
13892
|
+
hydrate(client, snapshot)`,
|
|
13893
|
+
notes: "`@pyreon/query` re-exports the framework-agnostic TanStack surface so consumers import every primitive from one entry: `QueryClient` / `QueryCache` / `MutationCache` (instance classes), `dehydrate` / `hydrate` (SSR serialization), `keepPreviousData` (placeholder helper), `hashKey` / `isCancelledError` / `CancelledError`, and the `defaultShouldDehydrate*` predicates. Types (`QueryKey`, `QueryFilters`, `MutationFilters`, `DehydratedState`, `FetchQueryOptions`, `InvalidateQueryFilters`, `InvalidateOptions`, `RefetchQueryFilters`, `RefetchOptions`, `QueryClientConfig`) re-export alongside the runtime values. See also: QueryClientProvider, useQueryClient."
|
|
13894
|
+
},
|
|
13895
|
+
"hooks/useControllableState": {
|
|
13896
|
+
signature: "<T>(opts: { value?: () => T | undefined; defaultValue: () => T; onChange?: (v: T) => void }) => [Signal<T>, (v: T) => void]",
|
|
13897
|
+
example: `function MyToggle(props: { checked?: boolean; defaultChecked?: boolean; onChange?: (v: boolean) => void }) {
|
|
13898
|
+
const [checked, setChecked] = useControllableState({
|
|
13899
|
+
value: () => props.checked,
|
|
13900
|
+
defaultValue: () => props.defaultChecked ?? false,
|
|
13901
|
+
onChange: props.onChange,
|
|
13902
|
+
})
|
|
13903
|
+
return <button onClick={() => setChecked(!checked())}>{() => checked() ? 'on' : 'off'}</button>
|
|
13904
|
+
}`,
|
|
13905
|
+
notes: "Canonical controlled/uncontrolled state pattern. Returns a `[value, setValue]` tuple where the setter respects controlled mode (calls `onChange` only if controlled, mutates internal signal if uncontrolled). Used by every primitive in `@pyreon/ui-primitives`. Never reimplement the `isControlled + signal + getter` shape by hand. `value` and `defaultValue` are FUNCTIONS so signal reads track reactively — passing a plain value loses controlled/uncontrolled detection on prop changes. See also: useToggle, usePrevious.",
|
|
13906
|
+
mistakes: `- Passing \`value: props.checked\` (not a function) — loses reactivity on prop changes
|
|
13907
|
+
- Mutating the returned signal directly with \`.set()\` instead of using the returned setter — bypasses the controlled-mode check`
|
|
13908
|
+
},
|
|
13909
|
+
"hooks/useEventListener": {
|
|
13910
|
+
signature: "(target: EventTarget | (() => EventTarget | null), event: string, handler: EventListener, options?: AddEventListenerOptions) => void",
|
|
13911
|
+
example: `useEventListener(window, 'resize', () => layoutSig.set(measure()))
|
|
13912
|
+
useEventListener(() => panelRef(), 'keydown', (e) => {
|
|
13913
|
+
if (e.key === 'Escape') setOpen(false)
|
|
13265
13914
|
})`,
|
|
13266
|
-
notes:
|
|
13915
|
+
notes: `Register a DOM event listener with automatic cleanup on unmount. Use this instead of raw \`addEventListener\` in primitives — never \`addEventListener\` / \`removeEventListener\` directly in component code (the cleanup is the hook's whole job). \`target\` may be a getter so reactive refs (\`() => buttonRef()\`) re-bind when the underlying element changes. See also: useClickOutside, useKeyboard.`,
|
|
13916
|
+
mistakes: `- Using raw \`addEventListener\` instead of \`useEventListener\` — you lose automatic \`onUnmount\` cleanup
|
|
13917
|
+
- Passing a static \`window\` / \`document\` when the target might not exist on SSR — \`useEventListener\` handles SSR-safe registration internally, but the target must be resolvable at \`onMount\` time`
|
|
13918
|
+
},
|
|
13919
|
+
"hooks/useClickOutside": {
|
|
13920
|
+
signature: "(ref: () => HTMLElement | null, handler: (e: MouseEvent) => void) => void",
|
|
13921
|
+
example: "useClickOutside(() => panelRef(), () => setOpen(false))",
|
|
13922
|
+
notes: "Fire a callback when the user clicks outside the referenced element. Foundation for click-to-dismiss popovers, dropdowns, modals. Pair with `useFocusTrap` + `useScrollLock` for the full modal package. See also: useFocusTrap, useScrollLock, useDialog.",
|
|
13923
|
+
mistakes: "- Attaching to a ref that encompasses the entire viewport — every click anywhere except the ref itself triggers the handler; use a more specific ref (the popover panel, not the whole page)"
|
|
13924
|
+
},
|
|
13925
|
+
"hooks/useElementSize": {
|
|
13926
|
+
signature: "(ref: () => HTMLElement | null) => Signal<{ width: number; height: number }>",
|
|
13927
|
+
example: `const size = useElementSize(() => boxRef())
|
|
13928
|
+
effect(() => console.log('Box is', size().width, 'x', size().height))`,
|
|
13929
|
+
notes: "Reactive element size via `ResizeObserver`. Returns `Signal<{ width, height }>` that updates whenever the observed element resizes. SSR-safe (returns `{ width: 0, height: 0 }` until mount). See also: useWindowResize, useRootSize."
|
|
13930
|
+
},
|
|
13931
|
+
"hooks/useFocusTrap": {
|
|
13932
|
+
signature: "(ref: () => HTMLElement | null, active: () => boolean) => void",
|
|
13933
|
+
example: `const isOpen = signal(false)
|
|
13934
|
+
useFocusTrap(() => modalRef(), () => isOpen())
|
|
13935
|
+
useScrollLock(() => isOpen())`,
|
|
13936
|
+
notes: "Trap Tab/Shift+Tab focus inside the referenced element while `active()` is true. Required for modals / drawers / fullscreen overlays to be keyboard-accessible. Returns focus to the previously-focused element on deactivation. See also: useScrollLock, useDialog, useClickOutside.",
|
|
13937
|
+
mistakes: `- Forgetting the second argument \`active\` — always pass a reactive boolean (\`() => isOpen()\`) so the trap deactivates when the modal closes; a static \`true\` traps focus forever
|
|
13938
|
+
- Using on an element that isn't rendered yet — the ref getter must return the element at the time \`active\` becomes true; pair with a \`<Show>\` or reactive accessor that mounts the element first`
|
|
13939
|
+
},
|
|
13940
|
+
"hooks/useBreakpoint": {
|
|
13941
|
+
signature: "() => Signal<{ xs: boolean; sm: boolean; md: boolean; lg: boolean; xl: boolean }>",
|
|
13942
|
+
example: `const bp = useBreakpoint()
|
|
13943
|
+
{() => bp().md ? <DesktopNav /> : <MobileNav />}`,
|
|
13944
|
+
notes: "Reactive breakpoint flags driven by the **theme**, not raw media queries — reads `theme.breakpoints` so swapping themes (or unit systems) Just Works. Use `useMediaQuery` for one-off arbitrary queries. See also: useMediaQuery, useThemeValue.",
|
|
13945
|
+
mistakes: "- Using `useBreakpoint` for a one-off media query like `(prefers-contrast: more)` — `useBreakpoint` reads theme breakpoints only; use `useMediaQuery` for arbitrary media queries"
|
|
13946
|
+
},
|
|
13947
|
+
"hooks/useDebouncedValue": {
|
|
13948
|
+
signature: "<T>(source: Signal<T> | (() => T), delayMs: number) => Signal<T>",
|
|
13949
|
+
example: `const search = signal('')
|
|
13950
|
+
const debouncedSearch = useDebouncedValue(search, 300)
|
|
13951
|
+
effect(() => fetchResults(debouncedSearch()))`,
|
|
13952
|
+
notes: `Returns a debounced signal that only updates after \`delayMs\` of source-signal idle. Use for search-as-you-type, filter inputs, anywhere downstream effects shouldn't fire on every keystroke. The PAIR — \`useDebouncedCallback\` — debounces a function call instead of a value. See also: useDebouncedCallback, useThrottledCallback.`,
|
|
13953
|
+
mistakes: "- Reading the debounced signal immediately after setting the source — it still holds the OLD value during the debounce window; effects downstream of the debounced signal are correct, but imperative reads in the same tick are stale"
|
|
13954
|
+
},
|
|
13955
|
+
"hooks/useClipboard": {
|
|
13956
|
+
signature: "(timeoutMs?: number) => { copy: (text: string) => Promise<void>; copied: Signal<boolean> }",
|
|
13957
|
+
example: `const { copy, copied } = useClipboard()
|
|
13958
|
+
<button onClick={() => copy(token)}>{() => copied() ? 'Copied!' : 'Copy'}</button>`,
|
|
13959
|
+
notes: "`navigator.clipboard.writeText` wrapped with a reactive `copied` flag that auto-resets after `timeoutMs` (default 2000). Use the `copied` signal to flash a \"Copied!\" UI cue without manual timer management. See also: useDialog, useOnline."
|
|
13960
|
+
},
|
|
13961
|
+
"hooks/useDialog": {
|
|
13962
|
+
signature: "() => { ref: (el: HTMLDialogElement | null) => void; open: () => void; close: (returnValue?: string) => void; isOpen: Signal<boolean>; returnValue: Signal<string> }",
|
|
13963
|
+
example: `const dialog = useDialog()
|
|
13964
|
+
<dialog ref={dialog.ref}>...</dialog>
|
|
13965
|
+
<button onClick={dialog.open}>Open</button>`,
|
|
13966
|
+
notes: `Native \`<dialog>\` element wrapper with reactive \`isOpen\` / \`returnValue\` signals. Handles \`showModal()\` / \`close()\` plumbing and the \`cancel\`/\`close\` event wiring so consumers don't reimplement the boilerplate. See also: useFocusTrap, useScrollLock.`,
|
|
13967
|
+
mistakes: "- Calling `dialog.open()` before the ref callback has fired — Pyreon components run once, so the `<dialog>` must be in the initial render (not behind a conditional `<Show>`); the ref callback fires synchronously during mount, and `dialog.open()` before that point has no element to call `showModal()` on"
|
|
13968
|
+
},
|
|
13969
|
+
"hooks/useTimeAgo": {
|
|
13970
|
+
signature: "(date: Date | (() => Date), opts?: UseTimeAgoOptions) => Signal<string>",
|
|
13971
|
+
example: `const sent = useTimeAgo(message.sentAt)
|
|
13972
|
+
<span>{sent}</span>`,
|
|
13973
|
+
notes: "Reactive \"5 minutes ago\" / \"in 2 hours\" relative-time string. Auto-updates on a sensible interval (every minute under an hour, every hour under a day, etc.) so the UI stays accurate without manual scheduling. Cleans up the interval on unmount. See also: useInterval, useDebouncedValue."
|
|
13974
|
+
},
|
|
13975
|
+
"hooks/useInfiniteScroll": {
|
|
13976
|
+
signature: "(onLoadMore: () => void | Promise<void>, opts?: { rootMargin?: string; threshold?: number; enabled?: () => boolean }) => { sentinelRef: (el: HTMLElement | null) => void; isLoading: Signal<boolean> }",
|
|
13977
|
+
example: `const { sentinelRef, isLoading } = useInfiniteScroll(loadNextPage, { rootMargin: '200px', enabled: () => hasMore() })
|
|
13978
|
+
<For each={items()} by={(i) => i.id}>{(item) => <Row data={item} />}</For>
|
|
13979
|
+
<div ref={sentinelRef}>{() => isLoading() && 'Loading…'}</div>`,
|
|
13980
|
+
notes: `\`IntersectionObserver\`-based infinite loading. Attach the returned \`sentinelRef\` to a node at the bottom of the list — when it scrolls into view, \`onLoadMore\` fires. \`isLoading\` blocks re-fires until the promise resolves. \`enabled\` accessor lets you stop observing once you've loaded the last page. See also: useIntersection.`,
|
|
13981
|
+
mistakes: `- Placing the sentinel inside a container with \`overflow: hidden\` and no scroll — IntersectionObserver never fires because the sentinel is always clipped; the sentinel must be inside the scrollable container
|
|
13982
|
+
- Forgetting to pass \`enabled: () => hasMore()\` — the hook keeps calling \`onLoadMore\` even after the last page`
|
|
13983
|
+
},
|
|
13984
|
+
"hooks/useMergedRef": {
|
|
13985
|
+
signature: "<T>(...refs: (Ref<T> | RefCallback<T> | null | undefined)[]) => RefCallback<T>",
|
|
13986
|
+
example: `const localRef = ref<HTMLDivElement>()
|
|
13987
|
+
const merged = useMergedRef(localRef, props.ref)
|
|
13988
|
+
<div ref={merged}>...</div>`,
|
|
13989
|
+
notes: "Combine multiple refs into a single callback ref — used when forwarding `props.ref` while also keeping a local ref to the same element. Each provided ref (callback or object) receives the element on mount and `null` on unmount. See also: useEventListener."
|
|
13990
|
+
},
|
|
13991
|
+
"hooks/useUpdateEffect": {
|
|
13992
|
+
signature: "(fn: () => void | (() => void), deps: Signal<unknown>[]) => void",
|
|
13993
|
+
example: `useUpdateEffect(() => api.save(value()), [value])
|
|
13994
|
+
// Doesn't fire on initial mount — only on subsequent value changes`,
|
|
13995
|
+
notes: "Like `effect` but skips the initial run — only fires when one of the tracked signals updates *after* mount. Use for \"save on change but not on first render\" patterns where the initial value is already persisted. See also: useIsomorphicLayoutEffect."
|
|
13996
|
+
},
|
|
13997
|
+
"hooks/useIsomorphicLayoutEffect": {
|
|
13998
|
+
signature: "(fn: () => void | (() => void)) => void",
|
|
13999
|
+
example: `const ref = signal<HTMLDivElement | null>(null)
|
|
14000
|
+
useIsomorphicLayoutEffect(() => {
|
|
14001
|
+
const el = ref()
|
|
14002
|
+
if (el) widthSig.set(el.getBoundingClientRect().width)
|
|
14003
|
+
})`,
|
|
14004
|
+
notes: "Runs a layout-phase effect on the client (synchronous, before paint) and a no-op on the server. Use when you need to read DOM measurements before the next paint without triggering an SSR mismatch warning. See also: useUpdateEffect, useElementSize."
|
|
13267
14005
|
},
|
|
13268
14006
|
"permissions/createPermissions": {
|
|
13269
|
-
signature: "
|
|
14007
|
+
signature: "<T extends PermissionMap>(initial?: T) => Permissions",
|
|
13270
14008
|
example: `const can = createPermissions({
|
|
13271
14009
|
'posts.read': true,
|
|
13272
14010
|
'posts.delete': (post) => post.authorId === userId,
|
|
@@ -13277,11 +14015,29 @@ can('posts.read') // true (reactive)
|
|
|
13277
14015
|
can('posts.delete', post) // evaluates predicate
|
|
13278
14016
|
can.not('admin.dashboard')
|
|
13279
14017
|
can.all('posts.read', 'posts.create')
|
|
13280
|
-
can.any('admin.users', 'posts.read')
|
|
13281
|
-
|
|
14018
|
+
can.any('admin.users', 'posts.read')
|
|
14019
|
+
can.set({ 'admin.*': true }) // replace all
|
|
14020
|
+
can.patch({ 'posts.delete': true }) // merge`,
|
|
14021
|
+
notes: "Create a reactive permissions instance. Returns a callable object — `can(key, context?)` checks a permission reactively (reads as a signal in effects and JSX). Permissions can be booleans or predicate functions `(context?) => boolean`. Supports wildcard keys (`admin.*`). The instance exposes `.not()`, `.all()`, `.any()` for multi-checks, and `.set()` / `.patch()` for runtime updates. See also: PermissionsProvider, usePermissions.",
|
|
14022
|
+
mistakes: `- Reading \`can("key")\` outside a reactive scope and expecting updates — the check is a signal read, it only re-evaluates inside \`effect()\`, \`computed()\`, or JSX expression thunks
|
|
14023
|
+
- Using a static object instead of a predicate for context-dependent checks — \`'posts.update': true\` always passes, use \`(post) => post.authorId === userId()\` for ABAC
|
|
14024
|
+
- Forgetting that wildcard \`admin.*\` only matches one level — \`admin.users.list\` is NOT matched by \`admin.*\`, only \`admin.users\` is`
|
|
14025
|
+
},
|
|
14026
|
+
"permissions/PermissionsProvider": {
|
|
14027
|
+
signature: "(props: { value: Permissions; children: VNodeChild }) => VNodeChild",
|
|
14028
|
+
example: `<PermissionsProvider value={can}>
|
|
14029
|
+
<App />
|
|
14030
|
+
</PermissionsProvider>`,
|
|
14031
|
+
notes: "Context provider that makes a permissions instance available to descendant components via `usePermissions()`. Enables SSR isolation (per-request permissions) and testing (override permissions per test). See also: usePermissions, createPermissions."
|
|
14032
|
+
},
|
|
14033
|
+
"permissions/usePermissions": {
|
|
14034
|
+
signature: "() => Permissions",
|
|
14035
|
+
example: `const can = usePermissions()
|
|
14036
|
+
return (() => can('admin.dashboard') ? <Dashboard /> : <AccessDenied />)`,
|
|
14037
|
+
notes: "Consume the nearest `PermissionsProvider` value. Returns the same callable `Permissions` instance. Throws if no provider is mounted. See also: PermissionsProvider, createPermissions."
|
|
13282
14038
|
},
|
|
13283
14039
|
"machine/createMachine": {
|
|
13284
|
-
signature: "
|
|
14040
|
+
signature: "<S extends string, E extends string>(config: MachineConfig<S, E>) => Machine<S, E>",
|
|
13285
14041
|
example: `const traffic = createMachine({
|
|
13286
14042
|
initial: 'red',
|
|
13287
14043
|
states: {
|
|
@@ -13295,54 +14051,131 @@ traffic() // 'red' (reactive)
|
|
|
13295
14051
|
traffic.send('NEXT') // 'green'
|
|
13296
14052
|
traffic.matches('green') // true
|
|
13297
14053
|
traffic.can('NEXT') // true`,
|
|
13298
|
-
notes: "
|
|
14054
|
+
notes: "Create a reactive state machine. The returned machine reads like a signal (`machine()` returns the current state string) and transitions via `machine.send(event, payload?)`. States and events are type-safe — TypeScript infers the union from the config object. Guards enable conditional transitions with typed payloads. No built-in context or effects — use Pyreon signals and `effect()` alongside the machine for data and side effects. See also: Machine, MachineConfig.",
|
|
14055
|
+
mistakes: `- Expecting \`machine.send()\` to return the new state — it returns void; read the state with \`machine()\` after sending
|
|
14056
|
+
- Calling \`machine.set()\` — machines are constrained signals, they do not expose \`.set()\`. State changes only happen through \`machine.send(event)\`
|
|
14057
|
+
- Using a machine for data storage — machines only hold the current state string. Use regular signals alongside the machine for associated data
|
|
14058
|
+
- Forgetting guard payloads — \`machine.send("LOGIN")\` without the required payload silently fails the guard`
|
|
13299
14059
|
},
|
|
13300
14060
|
"storage/useStorage": {
|
|
13301
|
-
signature: "
|
|
14061
|
+
signature: "<T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T>",
|
|
13302
14062
|
example: `const theme = useStorage('theme', 'light')
|
|
13303
14063
|
theme() // 'light'
|
|
13304
14064
|
theme.set('dark') // persists + cross-tab sync
|
|
13305
|
-
theme.remove() // delete from storage`,
|
|
13306
|
-
notes: "
|
|
14065
|
+
theme.remove() // delete from storage, reset to default`,
|
|
14066
|
+
notes: "Create a reactive signal backed by localStorage. Reads the stored value on creation (falling back to `defaultValue` if absent or on SSR), writes on every `.set()`, and syncs across browser tabs via `storage` events. Returns `StorageSignal<T>` which extends `Signal<T>` with `.remove()` to delete the key and reset to default. Serialization defaults to JSON; provide custom `serialize`/`deserialize` in options for non-JSON types. See also: useSessionStorage, useCookie, useIndexedDB, createStorage.",
|
|
14067
|
+
mistakes: `- Expecting cross-tab sync with \`useSessionStorage\` — only \`useStorage\` (localStorage) fires storage events across tabs
|
|
14068
|
+
- Storing non-serializable values (functions, class instances) without custom \`serialize\`/\`deserialize\` — JSON.stringify drops them silently
|
|
14069
|
+
- Reading \`.remove()\` return value — it returns void, not the removed value`
|
|
14070
|
+
},
|
|
14071
|
+
"storage/useCookie": {
|
|
14072
|
+
signature: "<T>(key: string, defaultValue: T, options?: CookieOptions) => StorageSignal<T>",
|
|
14073
|
+
example: `const locale = useCookie('locale', 'en', { maxAge: 365 * 86400, path: '/' })
|
|
14074
|
+
locale.set('fr')`,
|
|
14075
|
+
notes: "Reactive signal backed by browser cookies. SSR-readable — on the server, reads from the request cookie header via `setCookieSource()`. Options include `maxAge`, `path`, `domain`, `sameSite`, `secure`. Same `StorageSignal<T>` return type as other hooks. See also: useStorage, setCookieSource."
|
|
14076
|
+
},
|
|
14077
|
+
"storage/useIndexedDB": {
|
|
14078
|
+
signature: "<T>(key: string, defaultValue: T, options?: IndexedDBOptions) => StorageSignal<T>",
|
|
14079
|
+
example: `const draft = useIndexedDB('article-draft', { title: '', body: '' })
|
|
14080
|
+
draft.set({ title: 'New Article', body: 'Content...' })`,
|
|
14081
|
+
notes: "Reactive signal backed by IndexedDB for large data. Writes are debounced to avoid excessive I/O. The signal initializes with `defaultValue` synchronously and hydrates from IndexedDB asynchronously — the value updates reactively once the read completes. Silent init error logging in dev mode. See also: useStorage, useMemoryStorage."
|
|
14082
|
+
},
|
|
14083
|
+
"storage/createStorage": {
|
|
14084
|
+
signature: "(backend: StorageBackend | AsyncStorageBackend) => <T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T>",
|
|
14085
|
+
example: `const useEncrypted = createStorage({
|
|
14086
|
+
getItem: (key) => decrypt(localStorage.getItem(key)),
|
|
14087
|
+
setItem: (key, value) => localStorage.setItem(key, encrypt(value)),
|
|
14088
|
+
removeItem: (key) => localStorage.removeItem(key),
|
|
14089
|
+
})
|
|
14090
|
+
const secret = useEncrypted('api-key', '')`,
|
|
14091
|
+
notes: "Factory for custom storage backends. Pass an object with `getItem`, `setItem`, `removeItem` methods (sync or async) and receive a hook function with the same signature as `useStorage`. Use for encrypted storage, remote backends, or any custom persistence layer. See also: useStorage."
|
|
13307
14092
|
},
|
|
13308
14093
|
"i18n/createI18n": {
|
|
13309
|
-
signature: "
|
|
13310
|
-
example:
|
|
13311
|
-
import { createI18n, useI18n } from '@pyreon/i18n'
|
|
13312
|
-
|
|
13313
|
-
const i18n = createI18n({
|
|
14094
|
+
signature: "(options: I18nOptions) => I18nInstance",
|
|
14095
|
+
example: `const i18n = createI18n({
|
|
13314
14096
|
locale: 'en',
|
|
13315
14097
|
messages: { en: { greeting: 'Hello, {{name}}!' } },
|
|
13316
14098
|
loader: (locale, ns) => import(\`./locales/\${locale}/\${ns}.json\`),
|
|
14099
|
+
fallbackLocale: 'en',
|
|
13317
14100
|
})
|
|
13318
14101
|
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
locale
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
14102
|
+
i18n.t('greeting', { name: 'World' }) // "Hello, World!"
|
|
14103
|
+
i18n.locale.set('fr') // switch reactively`,
|
|
14104
|
+
notes: "Create a reactive i18n instance. Returns `{ t, locale, addMessages, loadNamespace }`. The `t(key, values?)` function resolves translations reactively — changing `locale` via `.set()` re-evaluates all `t()` reads in reactive scopes. Supports `{{name}}` interpolation, `_one`/`_other` plural suffixes, namespace lazy loading with deduplication, fallback locale, and custom plural rules. Available from both `@pyreon/i18n` and `@pyreon/i18n/core`. See also: I18nProvider, useI18n, Trans, interpolate.",
|
|
14105
|
+
mistakes: `- Reading \`t(key)\` outside a reactive scope and expecting updates on locale change — \`t()\` is a reactive signal read, wrap in JSX thunk or \`effect()\`
|
|
14106
|
+
- Using \`@pyreon/i18n\` on the backend — use \`@pyreon/i18n/core\` instead, it has zero JSX/core dependencies
|
|
14107
|
+
- Forgetting \`fallbackLocale\` — missing keys in the current locale return the key string instead of falling back to another language`
|
|
14108
|
+
},
|
|
14109
|
+
"i18n/I18nProvider": {
|
|
14110
|
+
signature: "(props: I18nProviderProps) => VNodeChild",
|
|
14111
|
+
example: `<I18nProvider value={i18n}>
|
|
14112
|
+
<App />
|
|
14113
|
+
</I18nProvider>`,
|
|
14114
|
+
notes: "Context provider that makes an i18n instance available to descendant components via `useI18n()`. Only available from the full `@pyreon/i18n` entry, not from `/core`. See also: useI18n, createI18n."
|
|
14115
|
+
},
|
|
14116
|
+
"i18n/useI18n": {
|
|
14117
|
+
signature: "() => I18nInstance",
|
|
14118
|
+
example: `const { t, locale } = useI18n()
|
|
14119
|
+
return <div>{() => t('greeting', { name: 'User' })}</div>`,
|
|
14120
|
+
notes: "Consume the nearest `I18nProvider` value. Returns the same `I18nInstance` with `t`, `locale`, `addMessages`, etc. Only available from the full `@pyreon/i18n` entry. See also: I18nProvider, createI18n."
|
|
14121
|
+
},
|
|
14122
|
+
"i18n/Trans": {
|
|
14123
|
+
signature: "(props: TransProps) => VNodeChild",
|
|
14124
|
+
example: `// Message: "Please <link>click here</link> to continue"
|
|
14125
|
+
<Trans key="action" components={{ link: <a href="/next" /> }}>
|
|
14126
|
+
Please <link>click here</link> to continue
|
|
14127
|
+
</Trans>`,
|
|
14128
|
+
notes: "Rich text interpolation component. Translates a key and replaces named placeholders with JSX components. Use for translations that contain markup (bold, links, etc.) that cannot be expressed as plain string interpolation. See also: createI18n, useI18n."
|
|
14129
|
+
},
|
|
14130
|
+
"i18n/interpolate": {
|
|
14131
|
+
signature: "(template: string, values?: InterpolationValues) => string",
|
|
14132
|
+
example: `interpolate('Hello, {{name}}!', { name: 'World' }) // 'Hello, World!'`,
|
|
14133
|
+
notes: "Pure string interpolation — replaces `{{name}}` placeholders with values from the map. Available from both entries. Use directly when you need interpolation without the full i18n instance (e.g. server-side email templates). See also: createI18n."
|
|
14134
|
+
},
|
|
14135
|
+
"document/render": {
|
|
14136
|
+
signature: "(node: DocNode, format: OutputFormat, options?: RenderOptions) => Promise<RenderResult>",
|
|
14137
|
+
example: `const pdf = await render(doc, 'pdf') // Uint8Array
|
|
14138
|
+
const html = await render(doc, 'html') // string
|
|
14139
|
+
const email = await render(doc, 'email') // Outlook-safe HTML
|
|
14140
|
+
const md = await render(doc, 'md') // Markdown string
|
|
14141
|
+
const slack = await render(doc, 'slack') // Slack Block Kit JSON`,
|
|
14142
|
+
notes: "Render a document node tree to any supported format. Returns a string (HTML, Markdown, text, CSV, email, Slack, Teams, etc.) or Uint8Array (PDF, DOCX, XLSX, PPTX) depending on the format. Heavy format renderers are lazy-loaded on first use. Supports 14+ built-in formats plus custom renderers registered via `registerRenderer()`. See also: createDocument, Document, download, registerRenderer.",
|
|
14143
|
+
mistakes: `- Not awaiting the render call — render() is always async due to lazy-loaded format renderers
|
|
14144
|
+
- Expecting render("pdf") to return a string — PDF, DOCX, XLSX, PPTX return Uint8Array
|
|
14145
|
+
- Passing a VNode instead of a DocNode — render() expects the output of JSX primitives (Document, Page, etc.) or createDocument(), not arbitrary Pyreon VNodes`
|
|
13333
14146
|
},
|
|
13334
14147
|
"document/createDocument": {
|
|
13335
|
-
signature: "
|
|
14148
|
+
signature: "(props?: DocumentProps) => DocumentBuilder",
|
|
13336
14149
|
example: `const doc = createDocument({ title: 'Report' })
|
|
13337
14150
|
.heading('Sales Report')
|
|
14151
|
+
.text('Q4 2026 summary.')
|
|
13338
14152
|
.table({ columns: ['Region', 'Revenue'], rows: [['US', '$1M']] })
|
|
13339
14153
|
|
|
13340
|
-
await doc.toPdf() // PDF
|
|
14154
|
+
await doc.toPdf() // PDF Uint8Array
|
|
13341
14155
|
await doc.toEmail() // Outlook-safe HTML
|
|
13342
|
-
await doc.toDocx() // Word document
|
|
13343
|
-
|
|
13344
|
-
await
|
|
13345
|
-
|
|
14156
|
+
await doc.toDocx() // Word document`,
|
|
14157
|
+
notes: "Fluent builder API for constructing documents without JSX. Chain `.heading()`, `.text()`, `.table()`, `.image()`, `.list()`, `.code()`, `.divider()`, `.page()` calls. Terminal methods: `.toPdf()`, `.toDocx()`, `.toEmail()`, `.toSlack()`, `.toNotion()`, `.toHtml()`, `.toMarkdown()`, etc. Each terminal method calls `render()` internally. See also: render, Document.",
|
|
14158
|
+
mistakes: `- Forgetting to await terminal methods — toPdf(), toDocx(), etc. are async
|
|
14159
|
+
- Calling builder methods after a terminal method — the builder is consumed; create a new one`
|
|
14160
|
+
},
|
|
14161
|
+
"document/Document": {
|
|
14162
|
+
signature: "(props: DocumentProps) => DocNode",
|
|
14163
|
+
example: `const doc = (
|
|
14164
|
+
<Document title="Report" author="Team">
|
|
14165
|
+
<Page>
|
|
14166
|
+
<Heading>Title</Heading>
|
|
14167
|
+
<Text>Content</Text>
|
|
14168
|
+
</Page>
|
|
14169
|
+
</Document>
|
|
14170
|
+
)
|
|
14171
|
+
await render(doc, 'pdf')`,
|
|
14172
|
+
notes: "Root JSX primitive for document trees. Accepts `title`, `author`, `subject` as metadata props. Children should be `Page` elements (or other block-level primitives for single-page documents). The returned DocNode is passed to `render()` for output. See also: render, Page, createDocument."
|
|
14173
|
+
},
|
|
14174
|
+
"document/download": {
|
|
14175
|
+
signature: "(data: Uint8Array | string, filename: string) => void",
|
|
14176
|
+
example: `const pdf = await render(doc, 'pdf')
|
|
14177
|
+
download(pdf, 'report.pdf')`,
|
|
14178
|
+
notes: "Browser helper that triggers a file download from rendered document data. Creates a temporary Blob URL and clicks a hidden anchor element. Works with both Uint8Array (PDF, DOCX) and string (HTML, Markdown) outputs from `render()`. See also: render."
|
|
13346
14179
|
},
|
|
13347
14180
|
"flow/createFlow": {
|
|
13348
14181
|
signature: "<TData = Record<string, unknown>>(config: FlowConfig<TData>) => FlowInstance<TData>",
|
|
@@ -13472,113 +14305,237 @@ flow.addEdge({ source: '1', sourceHandle: 'out-primary', target: '2' })`,
|
|
|
13472
14305
|
</Flow>`,
|
|
13473
14306
|
notes: "Overlay panel positioned absolutely relative to the flow viewport. Use for toolbars, legend badges, or contextual action buttons. Pass any JSX as children — the panel is a plain positioned container, not a predefined chrome component. See also: Flow, Controls."
|
|
13474
14307
|
},
|
|
14308
|
+
"charts/useChart": {
|
|
14309
|
+
signature: "<TOption extends EChartsOption = EChartsOption>(optionsFn: () => TOption, config?: UseChartConfig) => UseChartResult",
|
|
14310
|
+
example: `const chart = useChart(() => ({
|
|
14311
|
+
xAxis: { type: 'category', data: months() },
|
|
14312
|
+
yAxis: { type: 'value' },
|
|
14313
|
+
series: [{ type: 'bar', data: revenue() }],
|
|
14314
|
+
}))
|
|
14315
|
+
|
|
14316
|
+
<div ref={chart.ref} style="height: 400px" />
|
|
14317
|
+
// chart.loading() — true until ECharts modules loaded + chart initialized
|
|
14318
|
+
// chart.instance() — raw ECharts instance for imperative API`,
|
|
14319
|
+
notes: "Create a reactive ECharts instance. Options are passed as a function — signal reads inside are tracked and the chart updates automatically when any tracked signal changes. Lazy-loads the required ECharts modules on first render (zero bytes until mount). Returns `ref` (bind to a container div), `instance` (Signal<ECharts | null>), `loading` (Signal<boolean>), `error` (Signal<Error | null>), and `resize()`. Auto-resizes via ResizeObserver and disposes on unmount. See also: Chart.",
|
|
14320
|
+
mistakes: `- Forgetting to set a height on the container div — ECharts requires explicit dimensions, it does not auto-size to content
|
|
14321
|
+
- Passing options as a plain object instead of a function — signal reads are not tracked and the chart never updates
|
|
14322
|
+
- Reading chart.instance() immediately after useChart — the instance is null until the async module load completes; check chart.loading() first
|
|
14323
|
+
- Calling chart.resize() during SSR — useChart is browser-only; the hook no-ops safely on the server but resize is meaningless`
|
|
14324
|
+
},
|
|
14325
|
+
"charts/Chart": {
|
|
14326
|
+
signature: "(props: ChartProps) => VNodeChild",
|
|
14327
|
+
example: `<Chart
|
|
14328
|
+
options={() => ({
|
|
14329
|
+
series: [{ type: 'pie', data: [{ value: 60, name: 'A' }, { value: 40, name: 'B' }] }],
|
|
14330
|
+
})}
|
|
14331
|
+
style="height: 300px"
|
|
14332
|
+
onClick={(params) => alert(params.name)}
|
|
14333
|
+
/>`,
|
|
14334
|
+
notes: "Declarative chart component that wraps `useChart` internally. Accepts `options` (reactive function), `style`/`class` for the container, and event handlers (`onClick`, `onMouseover`, etc.) that bind to the ECharts instance. Renders a div with the chart — auto-resizes and cleans up on unmount. Simpler than useChart for most use cases. See also: useChart.",
|
|
14335
|
+
mistakes: `- Missing style height on the Chart component — same as useChart, ECharts requires explicit container dimensions
|
|
14336
|
+
- Passing a static options object — wrap in \`() => ({...})\` so signal reads inside are tracked reactively`
|
|
14337
|
+
},
|
|
13475
14338
|
"code/createEditor": {
|
|
13476
|
-
signature: "
|
|
14339
|
+
signature: "(config: EditorConfig) => EditorInstance",
|
|
13477
14340
|
example: `const editor = createEditor({
|
|
13478
14341
|
value: '// hello',
|
|
13479
14342
|
language: 'typescript',
|
|
13480
14343
|
theme: 'dark',
|
|
13481
14344
|
minimap: true,
|
|
13482
|
-
onChange: (next) => console.log('
|
|
14345
|
+
onChange: (next) => console.log('edit:', next),
|
|
13483
14346
|
})
|
|
13484
14347
|
|
|
13485
|
-
editor.value() // reactive
|
|
13486
|
-
editor.value.set('new') // write
|
|
13487
|
-
editor.cursor() //
|
|
14348
|
+
editor.value() // reactive read
|
|
14349
|
+
editor.value.set('new') // write into CodeMirror
|
|
14350
|
+
editor.cursor() // { line, col }
|
|
13488
14351
|
editor.lineCount() // computed
|
|
13489
14352
|
editor.goToLine(42)
|
|
13490
|
-
editor.insert('
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
<CodeEditor instance={editor}
|
|
13494
|
-
<
|
|
13495
|
-
|
|
13496
|
-
mistakes: `- Forgetting to declare @pyreon/runtime-dom in consumer app deps — <CodeEditor> JSX emits _tpl() which needs runtime-dom imports
|
|
13497
|
-
- Hand-rolling the applyingFromExternal/applyingFromEditor flag pattern for two-way binding — use the bindEditorToSignal helper instead, it handles the loop prevention correctly and is tested
|
|
14353
|
+
editor.insert('code')
|
|
14354
|
+
|
|
14355
|
+
<CodeEditor instance={editor} style="height: 400px" />`,
|
|
14356
|
+
notes: "Create a reactive editor instance. `editor.value` is a writable Signal<string> — `editor.value()` reads reactively, `editor.value.set(next)` writes back into CodeMirror. `editor.cursor` and `editor.lineCount` are computed signals. Config accepts value, language, theme, minimap, lineNumbers, foldGutter, onChange, and more. The instance is framework-independent — mount it via `<CodeEditor instance={editor} />`. See also: CodeEditor, bindEditorToSignal, loadLanguage.",
|
|
14357
|
+
mistakes: `- Forgetting to declare @pyreon/runtime-dom in consumer app deps — <CodeEditor> JSX emits _tpl() which needs runtime-dom
|
|
14358
|
+
- Hand-rolling the applyingFromExternal/applyingFromEditor flag pattern — use bindEditorToSignal instead
|
|
13498
14359
|
- Calling editor methods before mount — they no-op safely but changes don't persist
|
|
13499
14360
|
- Setting both vim: true and emacs: true — emacs wins`
|
|
13500
14361
|
},
|
|
13501
14362
|
"code/bindEditorToSignal": {
|
|
13502
|
-
signature: "
|
|
13503
|
-
example: `
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
interface Doc { name: string; count: number }
|
|
13507
|
-
const data = signal<Doc>({ name: 'Alice', count: 1 })
|
|
13508
|
-
|
|
13509
|
-
const editor = createEditor({
|
|
13510
|
-
value: JSON.stringify(data(), null, 2),
|
|
13511
|
-
language: 'json',
|
|
13512
|
-
})
|
|
14363
|
+
signature: "<T>(options: BindEditorToSignalOptions<T>) => EditorBinding",
|
|
14364
|
+
example: `const data = signal<Doc>({ name: 'Alice', count: 1 })
|
|
14365
|
+
const editor = createEditor({ value: JSON.stringify(data(), null, 2), language: 'json' })
|
|
13513
14366
|
|
|
13514
14367
|
const binding = bindEditorToSignal({
|
|
13515
14368
|
editor,
|
|
13516
|
-
signal: data,
|
|
14369
|
+
signal: data,
|
|
13517
14370
|
serialize: (val) => JSON.stringify(val, null, 2),
|
|
13518
|
-
parse: (text) => {
|
|
13519
|
-
try { return JSON.parse(text) } catch { return null }
|
|
13520
|
-
},
|
|
14371
|
+
parse: (text) => { try { return JSON.parse(text) } catch { return null } },
|
|
13521
14372
|
onParseError: (err) => console.warn(err.message),
|
|
13522
14373
|
})
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
binding.dispose()
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
14374
|
+
// binding.dispose() on unmount`,
|
|
14375
|
+
notes: "Two-way binding between an editor instance and an external Signal<T> (or SignalLike<T>). Replaces the recurring loop-prevention flag-pair boilerplate. Round-trips through user-supplied `serialize`/`parse` functions. Internal flags break the format-on-input race; parse failures call `onParseError` and leave the external state at its last valid value. Returns `{ dispose }` for cleanup. See also: createEditor.",
|
|
14376
|
+
mistakes: `- Forgetting to call binding.dispose() on unmount — leaks both effects
|
|
14377
|
+
- Non-deterministic serialize() — if serialize(parse(text)) varies on each call, the helper dispatches redundant writes that fight the user's typing
|
|
14378
|
+
- Returning a non-null value from parse() for malformed input — return null on failure, or throw
|
|
14379
|
+
- Using bindEditorToSignal AND a manual editor.value.set() loop — defeats loop prevention`
|
|
14380
|
+
},
|
|
14381
|
+
"code/CodeEditor": {
|
|
14382
|
+
signature: "(props: CodeEditorProps) => VNodeChild",
|
|
14383
|
+
example: "<CodeEditor instance={editor} style=\"height: 400px\" class=\"my-editor\" />",
|
|
14384
|
+
notes: "Mount component for a `createEditor` instance. Accepts `instance`, `style`, `class`, and passes through to a container div. Auto-mounts the CodeMirror view on render and cleans up on unmount. See also: createEditor, DiffEditor, TabbedEditor."
|
|
14385
|
+
},
|
|
14386
|
+
"code/DiffEditor": {
|
|
14387
|
+
signature: "(props: DiffEditorProps) => VNodeChild",
|
|
14388
|
+
example: "<DiffEditor original=\"old code\" modified=\"new code\" language=\"typescript\" />",
|
|
14389
|
+
notes: "Side-by-side diff editor. Accepts `original` and `modified` strings plus optional `language` and `theme`. Renders two CodeMirror instances with unified diff highlighting via @codemirror/merge. See also: CodeEditor, TabbedEditor."
|
|
14390
|
+
},
|
|
14391
|
+
"code/loadLanguage": {
|
|
14392
|
+
signature: "(lang: EditorLanguage) => Promise<void>",
|
|
14393
|
+
example: `await loadLanguage('python')
|
|
14394
|
+
// Now 'python' is available in createEditor({ language: 'python' })`,
|
|
14395
|
+
notes: "Lazy-load a language grammar. Supports 19 languages: json, typescript, javascript, python, css, html, markdown, rust, go, java, cpp, sql, xml, yaml, php, and more. Grammars are declared as optional dependencies and loaded on demand. See also: createEditor, getAvailableLanguages."
|
|
14396
|
+
},
|
|
14397
|
+
"code/minimapExtension": {
|
|
14398
|
+
signature: "() => Extension",
|
|
14399
|
+
example: `const editor = createEditor({ value: longCode, minimap: true })
|
|
14400
|
+
// or: import { minimapExtension } from '@pyreon/code'`,
|
|
14401
|
+
notes: "CodeMirror extension that renders a canvas-based code overview minimap. Enable via `createEditor({ minimap: true })` or add the extension manually to a CodeMirror state. See also: createEditor."
|
|
13532
14402
|
},
|
|
13533
14403
|
"hotkeys/useHotkey": {
|
|
13534
|
-
signature: "
|
|
14404
|
+
signature: "(shortcut: string, handler: (e: KeyboardEvent) => void, options?: HotkeyOptions) => void",
|
|
13535
14405
|
example: `useHotkey('mod+s', (e) => {
|
|
13536
14406
|
e.preventDefault()
|
|
13537
14407
|
save()
|
|
13538
|
-
})
|
|
13539
|
-
|
|
13540
|
-
useHotkey('
|
|
13541
|
-
|
|
13542
|
-
notes:
|
|
14408
|
+
}, { description: 'Save' })
|
|
14409
|
+
|
|
14410
|
+
useHotkey('ctrl+z', () => undo(), { scope: 'editor' })
|
|
14411
|
+
useHotkey('escape', () => close(), { enableOnFormElements: true })`,
|
|
14412
|
+
notes: `Register a keyboard shortcut that auto-unregisters when the component unmounts. Shortcut format: \`mod+s\`, \`ctrl+shift+p\`, \`escape\`, etc. \`mod\` is Command on Mac, Ctrl elsewhere. By default, shortcuts don't fire when focused on form elements (input, textarea, select) — override with \`enableOnFormElements: true\`. Supports \`scope\` option for context-aware activation and \`description\` for introspection. See also: useHotkeyScope, registerHotkey.`,
|
|
14413
|
+
mistakes: `- Forgetting e.preventDefault() for browser-reserved shortcuts (mod+s, mod+p) — the browser dialog fires alongside your handler
|
|
14414
|
+
- Registering the same shortcut in overlapping scopes without priority — both handlers fire; use scope isolation to prevent conflicts
|
|
14415
|
+
- Using useHotkey outside a component body — the onUnmount cleanup requires an active component setup context
|
|
14416
|
+
- Not activating the scope — useHotkey with a scope option does nothing unless useHotkeyScope(scope) is called or enableScope(scope) is invoked`
|
|
14417
|
+
},
|
|
14418
|
+
"hotkeys/useHotkeyScope": {
|
|
14419
|
+
signature: "(scope: string) => void",
|
|
14420
|
+
example: `// In an editor component:
|
|
14421
|
+
useHotkeyScope('editor')
|
|
14422
|
+
useHotkey('ctrl+z', () => undo(), { scope: 'editor' })
|
|
14423
|
+
|
|
14424
|
+
// In a modal component:
|
|
14425
|
+
useHotkeyScope('modal')
|
|
14426
|
+
useHotkey('escape', () => close(), { scope: 'modal' })`,
|
|
14427
|
+
notes: "Activate a hotkey scope for the lifetime of the current component. When the component mounts, the scope is enabled; when it unmounts, the scope is disabled. Shortcuts registered with a matching `scope` option only fire when the scope is active. Multiple components can activate the same scope — it stays active until the last one unmounts. See also: useHotkey, enableScope, disableScope.",
|
|
14428
|
+
mistakes: `- Using useHotkeyScope outside a component body — the lifecycle hooks require an active setup context
|
|
14429
|
+
- Assuming scope deactivation is immediate on unmount — if another component also activated the scope, it stays active`
|
|
14430
|
+
},
|
|
14431
|
+
"hotkeys/registerHotkey": {
|
|
14432
|
+
signature: "(shortcut: string, handler: (e: KeyboardEvent) => void, options?: HotkeyOptions) => () => void",
|
|
14433
|
+
example: `const unregister = registerHotkey('ctrl+q', () => quit(), { scope: 'global' })
|
|
14434
|
+
// Later:
|
|
14435
|
+
unregister()`,
|
|
14436
|
+
notes: "Imperative hotkey registration for non-component contexts (stores, global setup). Returns an unregister function. Unlike useHotkey, this does NOT auto-cleanup on unmount — caller is responsible for calling the returned unregister function. See also: useHotkey."
|
|
13543
14437
|
},
|
|
13544
14438
|
"table/useTable": {
|
|
13545
|
-
signature: "
|
|
13546
|
-
example: `const table = useTable({
|
|
13547
|
-
data:
|
|
14439
|
+
signature: "<TData extends RowData>(options: () => TableOptions<TData>) => Computed<Table<TData>>",
|
|
14440
|
+
example: `const table = useTable(() => ({
|
|
14441
|
+
data: users(),
|
|
13548
14442
|
columns: [
|
|
13549
14443
|
{ accessorKey: 'name', header: 'Name' },
|
|
13550
14444
|
{ accessorKey: 'email', header: 'Email' },
|
|
13551
14445
|
],
|
|
13552
|
-
|
|
14446
|
+
getCoreRowModel: getCoreRowModel(),
|
|
14447
|
+
}))
|
|
13553
14448
|
|
|
13554
|
-
//
|
|
14449
|
+
// Read inside reactive scope:
|
|
14450
|
+
<For each={() => table().getRowModel().rows} by={(r) => r.id}>
|
|
14451
|
+
{(row) => <tr>...</tr>}
|
|
14452
|
+
</For>`,
|
|
14453
|
+
notes: "Create a reactive TanStack Table instance. Options are passed as a function so reactive signals (data, columns, sorting state) can be read inside and the table updates automatically when they change. Returns a Computed<Table<T>> — read it inside JSX expression thunks or effects to track state changes. Internal state management uses a version counter to force re-notification even when the table reference is the same object. See also: flexRender.",
|
|
14454
|
+
mistakes: `- Passing options as a plain object instead of a function — signal reads are not tracked and the table never updates when data changes
|
|
14455
|
+
- Reading \`table\` without calling it — \`table\` is a Computed, you must call \`table()\` to get the Table instance
|
|
14456
|
+
- Forgetting getCoreRowModel() — TanStack Table requires at least getCoreRowModel in options or it throws
|
|
14457
|
+
- Using \`.map()\` on rows instead of \`<For>\` — loses Pyreon's keyed reconciliation and fine-grained DOM updates`
|
|
14458
|
+
},
|
|
14459
|
+
"table/flexRender": {
|
|
14460
|
+
signature: "<TData extends RowData, TValue>(component: Renderable<TValue>, props: TValue) => unknown",
|
|
14461
|
+
example: `// Header:
|
|
14462
|
+
flexRender(header.column.columnDef.header, header.getContext())
|
|
14463
|
+
// Cell:
|
|
13555
14464
|
flexRender(cell.column.columnDef.cell, cell.getContext())`,
|
|
13556
|
-
notes: "TanStack Table
|
|
14465
|
+
notes: "Render a TanStack Table column definition template (header, cell, or footer). Handles strings, numbers, functions (component functions or render functions), and VNodes. Returns the rendered output or null for undefined/null inputs. Use in JSX to render column definitions provided by TanStack Table. See also: useTable.",
|
|
14466
|
+
mistakes: `- Wrapping flexRender output in an extra function accessor — the result is already renderable JSX content
|
|
14467
|
+
- Passing the column def directly instead of calling getContext() — TanStack Table requires the context object`
|
|
13557
14468
|
},
|
|
13558
14469
|
"virtual/useVirtualizer": {
|
|
13559
|
-
signature: "
|
|
13560
|
-
example: `const
|
|
13561
|
-
count:
|
|
13562
|
-
getScrollElement: () => scrollRef
|
|
14470
|
+
signature: "(options: UseVirtualizerOptions) => UseVirtualizerResult",
|
|
14471
|
+
example: `const virtualizer = useVirtualizer({
|
|
14472
|
+
count: () => items().length,
|
|
14473
|
+
getScrollElement: () => scrollRef,
|
|
13563
14474
|
estimateSize: () => 35,
|
|
13564
|
-
|
|
13565
|
-
|
|
14475
|
+
overscan: 5,
|
|
14476
|
+
})
|
|
14477
|
+
|
|
14478
|
+
// virtualItems() is reactive — re-evaluates as user scrolls
|
|
14479
|
+
<For each={() => virtualizer.virtualItems()} by={(item) => item.index}>
|
|
14480
|
+
{(item) => <div style={() => \`top: \${item.start}px\`}>{item.index}</div>}
|
|
14481
|
+
</For>`,
|
|
14482
|
+
notes: "Create an element-scoped virtualizer. Attach to a scrollable container via `getScrollElement`. Returns reactive `virtualItems()`, `totalSize()`, and `isScrolling()` signals plus `scrollToIndex()` and `scrollToOffset()` for programmatic control. Options that accept functions (`count`, `estimateSize`) track signal reads reactively. See also: useWindowVirtualizer.",
|
|
14483
|
+
mistakes: `- Forgetting to set a fixed height on the scroll container — without overflow:auto + a height, the virtualizer has no viewport to measure
|
|
14484
|
+
- Passing count as a plain number instead of a function when the list length is dynamic — the virtualizer won't update when items change
|
|
14485
|
+
- Reading virtualItems() outside a reactive scope — captures the initial window only, never updates on scroll
|
|
14486
|
+
- Using .map() instead of <For> on virtualItems — loses keyed reconciliation`
|
|
14487
|
+
},
|
|
14488
|
+
"virtual/useWindowVirtualizer": {
|
|
14489
|
+
signature: "(options: UseWindowVirtualizerOptions) => UseWindowVirtualizerResult",
|
|
14490
|
+
example: `const virtualizer = useWindowVirtualizer({
|
|
14491
|
+
count: () => items().length,
|
|
14492
|
+
estimateSize: () => 50,
|
|
14493
|
+
})
|
|
14494
|
+
|
|
14495
|
+
<div style={() => \`height: \${virtualizer.totalSize()}px; position: relative\`}>
|
|
14496
|
+
<For each={() => virtualizer.virtualItems()} by={(item) => item.index}>
|
|
14497
|
+
{(item) => <div style={() => \`position: absolute; top: \${item.start}px\`}>Row {item.index}</div>}
|
|
14498
|
+
</For>
|
|
14499
|
+
</div>`,
|
|
14500
|
+
notes: "Create a window-scoped virtualizer that uses the browser window as the scroll container. SSR-safe — checks for browser environment before attaching scroll listeners. Same return shape as `useVirtualizer` (virtualItems, totalSize, isScrolling, scrollToIndex). Use for long page-level lists where the entire page scrolls. See also: useVirtualizer.",
|
|
14501
|
+
mistakes: `- Using useWindowVirtualizer inside a scrollable container that is not the window — use useVirtualizer with getScrollElement instead
|
|
14502
|
+
- Forgetting to position items absolutely inside a relative container with the total height — items overlap or collapse`
|
|
13566
14503
|
},
|
|
13567
14504
|
"feature/defineFeature": {
|
|
13568
|
-
signature: "
|
|
14505
|
+
signature: "<T>(config: FeatureConfig<T>) => Feature<T>",
|
|
13569
14506
|
example: `const Posts = defineFeature({
|
|
13570
14507
|
name: 'posts',
|
|
13571
|
-
schema: {
|
|
14508
|
+
schema: {
|
|
14509
|
+
title: 'string',
|
|
14510
|
+
body: 'string',
|
|
14511
|
+
author: reference('users'),
|
|
14512
|
+
},
|
|
13572
14513
|
api: { baseUrl: '/api/posts' },
|
|
13573
14514
|
})
|
|
13574
14515
|
|
|
13575
|
-
|
|
13576
|
-
Posts.
|
|
13577
|
-
Posts.
|
|
13578
|
-
Posts.
|
|
13579
|
-
Posts.
|
|
13580
|
-
|
|
13581
|
-
|
|
14516
|
+
Posts.useList({ page: 1 })
|
|
14517
|
+
Posts.useById('123')
|
|
14518
|
+
Posts.useCreate()
|
|
14519
|
+
Posts.useForm('123')
|
|
14520
|
+
Posts.useTable({ columns: ['title', 'author'] })`,
|
|
14521
|
+
notes: "Define a schema-driven CRUD feature. Accepts a name, field schema, and API config. Returns a Feature object with auto-generated hooks: `useList`, `useById`, `useSearch`, `useCreate`, `useUpdate`, `useDelete`, `useForm`, `useTable`, `useStore`. Composes @pyreon/query (data fetching), @pyreon/form (form state), @pyreon/validation (schema validation), @pyreon/store (global state), and @pyreon/table (table configuration). Schema field types are inferred for TypeScript autocompletion across all generated hooks. See also: reference, extractFields, defaultInitialValues.",
|
|
14522
|
+
mistakes: `- Forgetting to install peer dependencies — defineFeature composes @pyreon/query, @pyreon/form, @pyreon/validation, @pyreon/store, @pyreon/table internally
|
|
14523
|
+
- Using defineFeature without a QueryClient provider — useList/useById/useSearch/useCreate/useUpdate/useDelete all depend on @pyreon/query which requires a QueryClient in context
|
|
14524
|
+
- Passing schema field types as TypeScript types instead of string literals — schema values must be runtime strings like \`"string"\`, \`"number"\`, \`"boolean"\`, or \`reference("otherFeature")\`
|
|
14525
|
+
- Calling useForm without an id for edit mode — pass an id to load existing data, omit it for create mode`
|
|
14526
|
+
},
|
|
14527
|
+
"feature/reference": {
|
|
14528
|
+
signature: "(featureName: string) => ReferenceSchema",
|
|
14529
|
+
example: `const Posts = defineFeature({
|
|
14530
|
+
name: 'posts',
|
|
14531
|
+
schema: {
|
|
14532
|
+
title: 'string',
|
|
14533
|
+
author: reference('users'), // FK to users feature
|
|
14534
|
+
category: reference('categories'),
|
|
14535
|
+
},
|
|
14536
|
+
api: { baseUrl: '/api/posts' },
|
|
14537
|
+
})`,
|
|
14538
|
+
notes: "Mark a schema field as a foreign key reference to another feature. Used inside defineFeature schema definitions to establish relationships between features. The generated form and table hooks understand reference fields and can render appropriate UI (select dropdowns, linked displays). See also: defineFeature."
|
|
13582
14539
|
},
|
|
13583
14540
|
"lint/lint": {
|
|
13584
14541
|
signature: "lint(options?: LintOptions): LintResult",
|
|
@@ -13702,154 +14659,97 @@ const theme = enrichTheme({
|
|
|
13702
14659
|
// Merges user overrides with default breakpoints, spacing, and units`,
|
|
13703
14660
|
notes: "Merges a partial theme with the full default theme (breakpoints, spacing, unit utilities). Always use when passing a theme to PyreonUI."
|
|
13704
14661
|
},
|
|
13705
|
-
"rx/
|
|
13706
|
-
signature: "
|
|
13707
|
-
example: `
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
const
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
// Plain input → plain output:
|
|
13715
|
-
const result = filter([1, 2, 3, 4, 5], n => n > 3) // [4, 5]`,
|
|
13716
|
-
notes: "Every @pyreon/rx function is overloaded: Signal<T[]> input produces Computed<T[]>, plain T[] input produces plain T[]. 24 functions total: filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, count, sum, min, max, average, distinct, scan, combine, debounce, throttle, search."
|
|
14662
|
+
"rx/rx": {
|
|
14663
|
+
signature: "Readonly<{ filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, count, sum, min, max, average, distinct, scan, combine, debounce, throttle, search, pipe }>",
|
|
14664
|
+
example: `const active = rx.filter(users, u => u.active) // Computed<User[]>
|
|
14665
|
+
const sorted = rx.sortBy(active, 'name') // Computed<User[]>
|
|
14666
|
+
const total = rx.sum(users, u => u.age) // Computed<number>
|
|
14667
|
+
const grouped = rx.groupBy(users, u => u.department) // Computed<Map<string, User[]>>`,
|
|
14668
|
+
notes: "Namespaced object exposing all 24 reactive transform functions plus `pipe`. Use `rx.filter(...)` for dot-notation style, or destructure individual functions for tree-shaking. Every function is overloaded: `Signal<T[]>` input produces `Computed<T[]>` that auto-tracks, plain `T[]` input produces a static result. See also: pipe, filter.",
|
|
14669
|
+
mistakes: `- Expecting \`rx.filter(signal, pred)\` to return a plain array — signal inputs always produce \`Computed\` outputs. Call the result to read: \`active()\`
|
|
14670
|
+
- Passing a signal accessor (\`() => items()\`) instead of the signal itself — pass \`items\` not \`() => items()\`; the function checks for \`.subscribe\` to detect signals`
|
|
13717
14671
|
},
|
|
13718
14672
|
"rx/pipe": {
|
|
13719
|
-
signature: "
|
|
13720
|
-
example: `
|
|
13721
|
-
|
|
13722
|
-
const users = signal([
|
|
13723
|
-
{ name: 'Charlie', age: 35 },
|
|
13724
|
-
{ name: 'Alice', age: 25 },
|
|
13725
|
-
{ name: 'Bob', age: 30 },
|
|
13726
|
-
])
|
|
13727
|
-
|
|
13728
|
-
// Compose transforms left-to-right:
|
|
13729
|
-
const result = pipe(
|
|
14673
|
+
signature: "<T>(source: Signal<T[]> | T[], ...operators: Operator[]) => Computed<T[]> | T[]",
|
|
14674
|
+
example: `const result = pipe(
|
|
13730
14675
|
users,
|
|
13731
|
-
filter(u => u.
|
|
14676
|
+
filter(u => u.active),
|
|
13732
14677
|
sortBy('name'),
|
|
13733
14678
|
map(u => u.name),
|
|
14679
|
+
take(10),
|
|
13734
14680
|
)
|
|
13735
|
-
// Computed<string[]>
|
|
13736
|
-
notes: "
|
|
14681
|
+
// Computed<string[]> when users is a signal`,
|
|
14682
|
+
notes: "Compose transforms left-to-right. Each operator receives the output of the previous one. Signal source produces a reactive `Computed` that re-derives when the source changes. Use curried forms of individual functions as operators: `filter(pred)`, `sortBy(key)`, `map(fn)`, etc. See also: rx.",
|
|
14683
|
+
mistakes: "- Calling the non-curried form inside pipe — `pipe(users, filter(users, pred))` is wrong; use the curried form: `pipe(users, filter(pred))`"
|
|
14684
|
+
},
|
|
14685
|
+
"rx/filter": {
|
|
14686
|
+
signature: "<T>(source: Signal<T[]> | T[], predicate: (item: T) => boolean) => Computed<T[]> | T[]",
|
|
14687
|
+
example: `const evens = filter(items, n => n % 2 === 0) // Computed<number[]>
|
|
14688
|
+
const result = filter([1, 2, 3, 4, 5], n => n > 3) // [4, 5]`,
|
|
14689
|
+
notes: "Filter items by predicate. Signal input produces a reactive `Computed<T[]>` that re-evaluates when the source signal changes. Also available in curried form `filter(pred)` for use with `pipe()`. See also: rx, pipe."
|
|
13737
14690
|
},
|
|
13738
14691
|
"toast/toast": {
|
|
13739
|
-
signature: "
|
|
13740
|
-
example:
|
|
13741
|
-
|
|
13742
|
-
// Basic:
|
|
14692
|
+
signature: "(message: string, options?: ToastOptions) => string",
|
|
14693
|
+
example: `// Basic:
|
|
13743
14694
|
toast('Hello!')
|
|
13744
|
-
toast.success('Saved!')
|
|
13745
|
-
toast.error('Failed!')
|
|
14695
|
+
const id = toast.success('Saved!')
|
|
13746
14696
|
|
|
13747
|
-
// Loading → success
|
|
13748
|
-
const
|
|
14697
|
+
// Loading → success:
|
|
14698
|
+
const loadId = toast.loading('Saving...')
|
|
13749
14699
|
await save()
|
|
13750
|
-
toast.update(
|
|
14700
|
+
toast.update(loadId, { type: 'success', message: 'Done!' })
|
|
13751
14701
|
|
|
13752
14702
|
// Promise helper:
|
|
13753
14703
|
toast.promise(fetchData(), {
|
|
13754
14704
|
loading: 'Loading...',
|
|
13755
14705
|
success: 'Loaded!',
|
|
13756
|
-
error: 'Failed
|
|
14706
|
+
error: 'Failed',
|
|
13757
14707
|
})
|
|
13758
14708
|
|
|
13759
14709
|
// Dismiss:
|
|
13760
14710
|
toast.dismiss(id) // one
|
|
13761
|
-
toast.dismiss() // all
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
14711
|
+
toast.dismiss() // all`,
|
|
14712
|
+
notes: "Create a toast notification imperatively. Returns the toast ID for later `update()` or `dismiss()`. Works from anywhere in the app — no context or provider needed. The function also exposes `.success()`, `.error()`, `.warning()`, `.info()`, `.loading()` preset methods, `.update(id, options)` for modifying existing toasts, `.dismiss(id?)` for removal, and `.promise(promise, messages)` for async operation tracking. See also: Toaster.",
|
|
14713
|
+
mistakes: `- Forgetting to render \`<Toaster />\` — toasts are created but have no visual container to render into
|
|
14714
|
+
- Calling \`toast.update()\` after the toast has been auto-dismissed — the ID is no longer valid, the update is silently ignored
|
|
14715
|
+
- Using \`toast.promise()\` with a function instead of a promise — pass the promise directly, not \`() => fetch(...)\``
|
|
14716
|
+
},
|
|
14717
|
+
"toast/Toaster": {
|
|
14718
|
+
signature: "(props?: ToasterProps) => VNodeChild",
|
|
14719
|
+
example: "<Toaster position=\"top-right\" duration={5000} />",
|
|
14720
|
+
notes: "Render container for toast notifications. Mount once at the app root. Renders via Portal with CSS transitions, auto-dismiss timer, and pause-on-hover behavior. Position configurable via `position` prop (`top-right`, `top-left`, `bottom-right`, `bottom-left`, `top-center`, `bottom-center`). Duration configurable via `duration` prop (default 4000ms). See also: toast.",
|
|
14721
|
+
mistakes: `- Mounting multiple \`<Toaster />\` instances — toasts render in all of them, causing duplicates
|
|
14722
|
+
- Conditional rendering of \`<Toaster />\` — if unmounted, toasts created via \`toast()\` are queued but invisible until the Toaster mounts`
|
|
13766
14723
|
},
|
|
13767
14724
|
"url-state/useUrlState": {
|
|
13768
|
-
signature: "
|
|
13769
|
-
example:
|
|
13770
|
-
|
|
13771
|
-
// Single param — synced to ?page=:
|
|
14725
|
+
signature: "<T>(key: string, defaultValue: T, options?: UrlStateOptions) => UrlStateSignal<T>",
|
|
14726
|
+
example: `// Single param:
|
|
13772
14727
|
const page = useUrlState('page', 1)
|
|
13773
|
-
page()
|
|
13774
|
-
page.set(2)
|
|
13775
|
-
|
|
13776
|
-
// Schema mode
|
|
13777
|
-
const
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
14728
|
+
page() // 1
|
|
14729
|
+
page.set(2) // URL → ?page=2
|
|
14730
|
+
|
|
14731
|
+
// Schema mode:
|
|
14732
|
+
const { q, sort } = useUrlState({ q: '', sort: 'name' })
|
|
14733
|
+
q.set('hello') // ?q=hello&sort=name
|
|
14734
|
+
|
|
14735
|
+
// Array with repeated keys:
|
|
14736
|
+
const tags = useUrlState('tags', [] as string[], { arrayFormat: 'repeat' })
|
|
14737
|
+
tags.set(['a', 'b']) // ?tags=a&tags=b`,
|
|
14738
|
+
notes: "Create a reactive signal synced to a URL search parameter. Type is inferred from the default value — numbers, booleans, strings, and arrays are auto-coerced. Uses `replaceState` by default (no history entries). Returns a `UrlStateSignal<T>` with `.set()`, `.reset()`, and `.remove()`. Schema mode overload: `useUrlState({ page: 1, sort: \"name\" })` creates multiple synced signals from a single call. SSR-safe — reads from the request URL on server. See also: setUrlRouter.",
|
|
14739
|
+
mistakes: `- Using pushState behavior (adds history entries per keystroke) — useUrlState defaults to replaceState; if you pass \`{ replaceState: false }\` on a high-frequency input, the browser back button breaks
|
|
14740
|
+
- Forgetting the default value — the type is inferred from it and determines the auto-coercion strategy (number default = coerce to number, boolean default = coerce to boolean)
|
|
14741
|
+
- Reading useUrlState in a non-reactive scope at component setup — the signal reads the URL once; wrap in a reactive scope to track URL changes
|
|
14742
|
+
- Calling setUrlRouter before the router is available — SSR renders may not have a router instance yet`
|
|
13782
14743
|
},
|
|
13783
|
-
"
|
|
13784
|
-
signature: "
|
|
13785
|
-
example: `import {
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
// data() reactively updates on each SSE message
|
|
13794
|
-
// Auto-reconnects on disconnect
|
|
13795
|
-
// Integrates with QueryClient for cache invalidation`,
|
|
13796
|
-
notes: "Server-Sent Events hook. Same pattern as useSubscription but read-only (no send). Integrates with QueryClient cache."
|
|
13797
|
-
},
|
|
13798
|
-
"router/useIsActive": {
|
|
13799
|
-
signature: "useIsActive(path: string, exact?: boolean): () => boolean",
|
|
13800
|
-
example: `import { useIsActive } from '@pyreon/router'
|
|
13801
|
-
|
|
13802
|
-
const isHome = useIsActive('/')
|
|
13803
|
-
const isAdmin = useIsActive('/admin') // prefix match
|
|
13804
|
-
const isExactAdmin = useIsActive('/admin', true) // exact only
|
|
13805
|
-
|
|
13806
|
-
// Reactive — updates when route changes:
|
|
13807
|
-
<a class={{ active: isAdmin() }} href="/admin">Admin</a>`,
|
|
13808
|
-
notes: "Returns a reactive boolean. Segment-aware prefix matching: /admin matches /admin/users but not /admin-panel. Pass exact=true for exact-only matching."
|
|
13809
|
-
},
|
|
13810
|
-
"router/useTypedSearchParams": {
|
|
13811
|
-
signature: "useTypedSearchParams<T>(schema: T): TypedSearchParams<T>",
|
|
13812
|
-
example: `import { useTypedSearchParams } from '@pyreon/router'
|
|
13813
|
-
|
|
13814
|
-
const params = useTypedSearchParams({ page: 'number', q: 'string', active: 'boolean' })
|
|
13815
|
-
params.page() // number (auto-coerced)
|
|
13816
|
-
params.q() // string
|
|
13817
|
-
params.set({ page: 2 }) // updates URL`,
|
|
13818
|
-
notes: "Type-safe search params with auto-coercion from URL strings. Supports \"string\", \"number\", and \"boolean\" types."
|
|
13819
|
-
},
|
|
13820
|
-
"router/useTransition": {
|
|
13821
|
-
signature: "useTransition(): { isTransitioning: () => boolean }",
|
|
13822
|
-
example: `import { useTransition } from '@pyreon/router'
|
|
13823
|
-
|
|
13824
|
-
const { isTransitioning } = useTransition()
|
|
13825
|
-
// true during navigation (guards + loaders), false when mounted`,
|
|
13826
|
-
notes: "Reactive signal for route transition state. Useful for progress bars and loading indicators."
|
|
13827
|
-
},
|
|
13828
|
-
"router/useMiddlewareData": {
|
|
13829
|
-
signature: "useMiddlewareData<T>(): T",
|
|
13830
|
-
example: `import { useMiddlewareData } from '@pyreon/router'
|
|
13831
|
-
|
|
13832
|
-
// After middleware sets ctx.data.user:
|
|
13833
|
-
const data = useMiddlewareData<{ user: User }>()
|
|
13834
|
-
// data.user is available in the component`,
|
|
13835
|
-
notes: "Reads data set by RouteMiddleware in the middleware chain. Middleware sets ctx.data properties, components read them."
|
|
13836
|
-
},
|
|
13837
|
-
"storybook/renderToCanvas": {
|
|
13838
|
-
signature: "renderToCanvas(context: StoryContext, canvasElement: HTMLElement): void",
|
|
13839
|
-
example: `// .storybook/main.ts:
|
|
13840
|
-
export default { framework: '@pyreon/storybook' }
|
|
13841
|
-
|
|
13842
|
-
// Story file:
|
|
13843
|
-
import type { Meta, StoryObj } from '@pyreon/storybook'
|
|
13844
|
-
import { Button } from './Button'
|
|
13845
|
-
|
|
13846
|
-
const meta: Meta<typeof Button> = { component: Button }
|
|
13847
|
-
export default meta
|
|
13848
|
-
|
|
13849
|
-
export const Primary: StoryObj<typeof meta> = {
|
|
13850
|
-
args: { variant: 'primary', label: 'Click me' },
|
|
13851
|
-
}`,
|
|
13852
|
-
notes: "Storybook renderer for Pyreon components. Re-exports h, Fragment, signal, computed, effect, mount for story convenience."
|
|
14744
|
+
"url-state/setUrlRouter": {
|
|
14745
|
+
signature: "(router: UrlRouter) => void",
|
|
14746
|
+
example: `import { useRouter } from '@pyreon/router'
|
|
14747
|
+
import { setUrlRouter } from '@pyreon/url-state'
|
|
14748
|
+
|
|
14749
|
+
const router = useRouter()
|
|
14750
|
+
setUrlRouter(router)
|
|
14751
|
+
// Now useUrlState uses router.replace() internally`,
|
|
14752
|
+
notes: `Configure useUrlState to use a @pyreon/router instance for URL updates instead of raw \`history.replaceState\`. When set, URL changes go through the router's navigation system, ensuring route guards, middleware, and scroll management integrate correctly. See also: useUrlState.`
|
|
13853
14753
|
},
|
|
13854
14754
|
"document-primitives/extractDocNode": {
|
|
13855
14755
|
signature: "extractDocNode(templateFn: () => VNode, options?: ExtractOptions): DocNode",
|