Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 2f012b2

Browse files
vitbokischclaude
andcommitted
docs: update for Pyreon v0.7.7
- Smart class with cx() (arrays, objects, nested mix) - Typed event targets (TargetedEvent<E>, no more casts) - New events: onBeforeInput, onInvalid, onResize, onToggle - splitProps, mergeProps, createUniqueId utilities - untrack alias for runUntracked - @pyreon/typescript package docs (new page) - Remove n-show/directive references - Fix onMount void return docs - Update all e.target casts to e.currentTarget - Signal-preserving HMR and auto naming docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0bdcf89 commit 2f012b2

10 files changed

Lines changed: 467 additions & 145 deletions

File tree

.mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"pyreon": {
4+
"command": "bunx",
5+
"args": ["@pyreon/mcp"]
6+
}
7+
}
8+
}

content/docs/core/index.mdx

Lines changed: 146 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ const SearchBox = defineComponent((props: { placeholder: string }) => {
6666
// Register lifecycle hooks
6767
onMount(() => {
6868
inputRef.current?.focus()
69-
return undefined
7069
})
7170

7271
// Set up effects
@@ -88,7 +87,7 @@ const SearchBox = defineComponent((props: { placeholder: string }) => {
8887
ref={inputRef}
8988
placeholder={props.placeholder}
9089
value={() => query()}
91-
onInput={(e) => query.set((e.target as HTMLInputElement).value)}
90+
onInput={(e) => query.set(e.currentTarget.value)}
9291
/>
9392
<ul>
9493
{() => results().map(r => <li>{r}</li>)}
@@ -473,7 +472,6 @@ function AutoFocusInput() {
473472

474473
onMount(() => {
475474
inputRef.current?.focus()
476-
return undefined
477475
})
478476

479477
return <input ref={inputRef} />
@@ -604,6 +602,46 @@ function ChatMessages(props: { messages: () => Message[] }) {
604602
}
605603
```
606604

605+
### onCleanup
606+
607+
Register a cleanup function that runs when the current reactive scope is disposed. Inside an `effect`, `onCleanup` runs before each re-execution and on final disposal. Inside a component, it runs when the component unmounts. This is the idiomatic way to clean up resources in effects.
608+
609+
```tsx
610+
import { onCleanup } from "@pyreon/core"
611+
import { signal, effect } from "@pyreon/reactivity"
612+
613+
function WebSocketComponent(props: { url: () => string }) {
614+
const messages = signal<string[]>([])
615+
616+
effect(() => {
617+
const ws = new WebSocket(props.url())
618+
ws.onmessage = (e) => messages.update(m => [...m, e.data])
619+
620+
// Runs before next effect re-execution and on unmount
621+
onCleanup(() => ws.close())
622+
})
623+
624+
return <ul>{() => messages().map(m => <li>{m}</li>)}</ul>
625+
}
626+
```
627+
628+
```tsx
629+
// Cleanup a timer inside an effect
630+
function Poller(props: { interval: () => number }) {
631+
const data = signal<string>("")
632+
633+
effect(() => {
634+
const id = setInterval(() => {
635+
fetch("/api/data").then(r => r.text()).then(t => data.set(t))
636+
}, props.interval())
637+
638+
onCleanup(() => clearInterval(id))
639+
})
640+
641+
return <p>{data()}</p>
642+
}
643+
```
644+
607645
### onErrorCaptured
608646

609647
Register an error handler for the component subtree. When an error is thrown during rendering or in a child component, the nearest `onErrorCaptured` handler is called. Return `true` to mark the error as handled and stop propagation.
@@ -840,7 +878,6 @@ function AutoFocusInput() {
840878

841879
onMount(() => {
842880
inputRef.current?.focus()
843-
return undefined
844881
})
845882

846883
return <input ref={inputRef} placeholder="Auto-focused" />
@@ -902,7 +939,6 @@ function Parent() {
902939

903940
onMount(() => {
904941
ref.current?.focus()
905-
return undefined
906942
})
907943

908944
return <FancyInput inputRef={ref} placeholder="Type here..." />
@@ -921,8 +957,6 @@ function DrawingCanvas() {
921957

922958
ctx.fillStyle = "#007bff"
923959
ctx.fillRect(10, 10, 100, 100)
924-
925-
return undefined
926960
})
927961

928962
return <canvas ref={canvasRef} width={400} height={300} />
@@ -1438,6 +1472,81 @@ function mapArray<T, U>(
14381472
): () => U[]
14391473
```
14401474

1475+
## Prop Utilities
1476+
1477+
### splitProps
1478+
1479+
Split a props object into two parts: one with the specified keys, and one with the rest. Both parts preserve reactivity -- accessing a property on either part reads the original prop.
1480+
1481+
```tsx
1482+
import { splitProps } from "@pyreon/core"
1483+
1484+
function Button(props: { label: string; icon?: string } & PyreonHTMLAttributes<HTMLButtonElement>) {
1485+
const [own, html] = splitProps(props, ["label", "icon"])
1486+
1487+
return (
1488+
<button {...html}>
1489+
<Show when={() => !!own.icon}>
1490+
<Icon name={own.icon!} />
1491+
</Show>
1492+
{own.label}
1493+
</button>
1494+
)
1495+
}
1496+
```
1497+
1498+
```ts
1499+
function splitProps<T extends object, K extends keyof T>(
1500+
props: T,
1501+
keys: K[],
1502+
): [Pick<T, K>, Omit<T, K>]
1503+
```
1504+
1505+
### mergeProps
1506+
1507+
Merge multiple props objects into one, with later sources overriding earlier ones. The merged object is lazy -- property reads go through the original sources, preserving reactivity.
1508+
1509+
```tsx
1510+
import { mergeProps } from "@pyreon/core"
1511+
1512+
function Button(props: { size?: "sm" | "md" | "lg"; variant?: string }) {
1513+
const merged = mergeProps({ size: "md", variant: "primary" }, props)
1514+
1515+
return (
1516+
<button class={() => `btn-${merged.size} btn-${merged.variant}`}>
1517+
{merged.children}
1518+
</button>
1519+
)
1520+
}
1521+
```
1522+
1523+
```ts
1524+
function mergeProps<T extends object[]>(...sources: T): MergedProps<T>
1525+
```
1526+
1527+
### createUniqueId
1528+
1529+
Generate a unique string ID that is stable across server and client renders. Use this for linking labels to inputs, ARIA attributes, and other cases where you need a deterministic unique ID.
1530+
1531+
```tsx
1532+
import { createUniqueId } from "@pyreon/core"
1533+
1534+
function LabeledInput(props: { label: string }) {
1535+
const id = createUniqueId()
1536+
1537+
return (
1538+
<div>
1539+
<label for={id}>{props.label}</label>
1540+
<input id={id} />
1541+
</div>
1542+
)
1543+
}
1544+
```
1545+
1546+
```ts
1547+
function createUniqueId(): string
1548+
```
1549+
14411550
## Telemetry
14421551

14431552
Register global error handlers for monitoring and reporting. This integrates with services like Sentry, Datadog, or custom error tracking.
@@ -1572,7 +1681,7 @@ const ContactForm = defineComponent(() => {
15721681
<label>Name</label>
15731682
<input
15741683
value={() => name().value}
1575-
onInput={(e) => updateField(name, "name", (e.target as HTMLInputElement).value)}
1684+
onInput={(e) => updateField(name, "name", e.currentTarget.value)}
15761685
/>
15771686
<Show when={() => name().touched && name().error}>
15781687
<span class="error">{name().error}</span>
@@ -1583,7 +1692,7 @@ const ContactForm = defineComponent(() => {
15831692
<input
15841693
type="email"
15851694
value={() => email().value}
1586-
onInput={(e) => updateField(email, "email", (e.target as HTMLInputElement).value)}
1695+
onInput={(e) => updateField(email, "email", e.currentTarget.value)}
15871696
/>
15881697
<Show when={() => email().touched && email().error}>
15891698
<span class="error">{email().error}</span>
@@ -1593,7 +1702,7 @@ const ContactForm = defineComponent(() => {
15931702
<label>Message</label>
15941703
<textarea
15951704
value={() => message().value}
1596-
onInput={(e) => updateField(message, "message", (e.target as HTMLTextAreaElement).value)}
1705+
onInput={(e) => updateField(message, "message", e.currentTarget.value)}
15971706
/>
15981707
<Show when={() => message().touched && message().error}>
15991708
<span class="error">{message().error}</span>
@@ -1780,4 +1889,31 @@ Dispatch an error to the nearest active `ErrorBoundary`. Returns `true` if the b
17801889

17811890
<APICard name="onUnmount" type="hook" signature="onUnmount(callback: () => void): void" description="Registers a callback to run when the component is removed from the DOM. Use for cleanup not covered by onMount's return value." />
17821891

1892+
<APICard name="onCleanup" type="hook" signature="onCleanup(fn: () => void): void" description="Registers a cleanup function for the current reactive scope. Inside effects, runs before each re-execution and on disposal. Inside components, runs on unmount." />
1893+
17831894
<APICard name="onUpdate" type="hook" signature="onUpdate(callback: () => void): void" description="Registers a callback to run after each reactive update within the component. Fires via microtask after effects settle, so the DOM is up-to-date." />
1895+
1896+
<APICard name="splitProps" type="function" signature="splitProps<T, K extends keyof T>(props: T, keys: K[]): [Pick<T, K>, Omit<T, K>]" description="Splits a props object into two parts preserving reactivity. First part has the specified keys, second has the rest." />
1897+
1898+
<APICard name="mergeProps" type="function" signature="mergeProps<T extends object[]>(...sources: T): MergedProps<T>" description="Merges multiple props objects with later sources overriding earlier ones. Preserves reactivity through lazy property access." />
1899+
1900+
<APICard name="createUniqueId" type="function" signature="createUniqueId(): string" description="Generates a unique string ID that is stable across server and client renders. Use for ARIA attributes and label-input linking." />
1901+
1902+
<APICard name="cx" type="function" signature="cx(...args: ClassValue[]): string" description="Utility for composing class names from strings, arrays, objects, or nested combinations. Used internally by the class prop." />
1903+
1904+
## Type Exports
1905+
1906+
| Type | Description |
1907+
|------|-------------|
1908+
| `ComponentFn<P>` | `(props: P) => VNodeChild` -- component function type |
1909+
| `VNode` | Virtual DOM node with type, props, children, and key |
1910+
| `VNodeChild` | Union type for all renderable values (VNode, string, number, null, boolean, function, array) |
1911+
| `Props` | Base props interface for elements and components |
1912+
| `Ref<T>` | Mutable ref container `{ current: T \| null }` |
1913+
| `ExtractProps<T>` | Extracts the props type from a `ComponentFn<P>`, or passes through if already a props object |
1914+
| `HigherOrderComponent<HOP, P>` | Typed higher-order component pattern `(component: ComponentFn<P>) => ComponentFn<P & HOP>` |
1915+
| `PyreonHTMLAttributes<E>` | HTML attribute types parameterized by element type (e.g., `PyreonHTMLAttributes<HTMLInputElement>`) |
1916+
| `CSSProperties` | Typed CSS property object for the `style` prop |
1917+
| `StyleValue` | Union type for style prop values: `string \| CSSProperties \| (() => string \| CSSProperties)` |
1918+
| `ClassValue` | Union type for the `class` prop: `string \| boolean \| null \| undefined \| ClassValue[] \| Record<string, boolean \| (() => boolean)>` |
1919+
| `TargetedEvent<E>` | Event type where `currentTarget` is typed as `E` (e.g., `TargetedEvent<HTMLInputElement>`) |

content/docs/hooks/index.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ const Modal = defineComponent<{ onClose: () => void }>((props) => {
643643
const { lock, unlock } = useScrollLock()
644644

645645
// Lock scroll when modal opens, unlock when it closes
646-
onMount(() => { lock(); return undefined })
646+
onMount(() => { lock() })
647647
onUnmount(unlock)
648648

649649
return () => (
@@ -1152,7 +1152,7 @@ Multiple calls to `lock()` from the same hook instance are idempotent -- calling
11521152
const FullScreenOverlay = defineComponent<{ onClose: () => void }>((props) => {
11531153
const { lock, unlock } = useScrollLock()
11541154
1155-
onMount(() => { lock(); return undefined })
1155+
onMount(() => { lock() })
11561156
onUnmount(unlock)
11571157
11581158
return () => (
@@ -1330,7 +1330,7 @@ const AccessibleModal = defineComponent<{
13301330
13311331
// Lock page scroll
13321332
const { lock, unlock } = useScrollLock()
1333-
onMount(() => { lock(); return undefined })
1333+
onMount(() => { lock() })
13341334
onUnmount(unlock)
13351335
13361336
// Respect reduced motion

content/docs/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"head",
1616
"server",
1717
"vite-plugin",
18+
"typescript",
1819
"mcp",
1920
"---Compatibility Layers---",
2021
"react-compat",

content/docs/query/index.mdx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,11 @@ const SearchResults = defineComponent(() => {
216216
type="text"
217217
placeholder="Search..."
218218
onInput={(e) => {
219-
searchTerm.set((e.target as HTMLInputElement).value)
219+
searchTerm.set(e.currentTarget.value)
220220
page.set(1) // Reset to page 1 on new search
221221
}}
222222
/>
223-
<select onChange={(e) => category.set((e.target as HTMLSelectElement).value)}>
223+
<select onChange={(e) => category.set(e.currentTarget.value)}>
224224
<option value="all">All</option>
225225
<option value="posts">Posts</option>
226226
<option value="users">Users</option>
@@ -443,7 +443,7 @@ const UpdateTodo = defineComponent((props: { todo: Todo }) => {
443443
checked={() => props.todo.completed}
444444
onChange={(e) => {
445445
mutation.mutate({
446-
completed: (e.target as HTMLInputElement).checked,
446+
completed: e.currentTarget.checked,
447447
})
448448
}}
449449
/>
@@ -619,11 +619,15 @@ const InfiniteScroll = defineComponent(() => {
619619
{/* Sentinel element -- triggers fetchNextPage when scrolled into view */}
620620
{() => query.hasNextPage() && (
621621
<div
622-
n-inView={createInViewDirective(() => {
623-
if (!query.isFetchingNextPage()) {
624-
query.fetchNextPage()
625-
}
626-
})}
622+
ref={(el: HTMLDivElement) => {
623+
const observer = new IntersectionObserver(([entry]) => {
624+
if (entry.isIntersecting && !query.isFetchingNextPage()) {
625+
query.fetchNextPage()
626+
}
627+
})
628+
observer.observe(el)
629+
onCleanup(() => observer.disconnect())
630+
}}
627631
class="loading-sentinel"
628632
>
629633
{() => query.isFetchingNextPage() && <Spinner />}
@@ -1068,12 +1072,11 @@ const GlobalLoadingBar = defineComponent(() => {
10681072
const mutating = useIsMutating()
10691073

10701074
return () => (
1071-
<div
1072-
class="global-loading-bar"
1073-
n-show={() => fetching() > 0 || mutating() > 0}
1074-
>
1075-
<div class="progress-bar" />
1076-
</div>
1075+
<Show when={() => fetching() > 0 || mutating() > 0}>
1076+
<div class="global-loading-bar">
1077+
<div class="progress-bar" />
1078+
</div>
1079+
</Show>
10771080
)
10781081
})
10791082
```

content/docs/reactivity/index.mdx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,23 @@ const outer = effect(() => {
344344

345345
### Effect Cleanup
346346

347-
Effects do not have a built-in cleanup mechanism like `watch`. For cleanup needs, use `watch` instead, or manage cleanup manually:
347+
Use `onCleanup` from `@pyreon/core` inside an effect to register a cleanup function. The cleanup runs before each re-execution and on final disposal:
348+
349+
```ts
350+
import { onCleanup } from "@pyreon/core"
351+
352+
effect(() => {
353+
const q = query()
354+
const controller = new AbortController()
355+
fetch(`/search?q=${q}`, { signal: controller.signal })
356+
357+
onCleanup(() => controller.abort()) // runs before next re-execution
358+
})
359+
```
360+
361+
Alternatively, use `watch` when you need old/new values along with cleanup:
348362

349363
```ts
350-
// Use watch for effects that need cleanup
351364
watch(
352365
() => query(),
353366
(q) => {
@@ -1160,12 +1173,12 @@ scope.runInScope(() => {
11601173
})
11611174
```
11621175

1163-
## runUntracked
1176+
## runUntracked / untrack
11641177

1165-
Run a function without registering any reactive dependencies. Useful inside effects when you need to read a signal without subscribing to it.
1178+
Run a function without registering any reactive dependencies. Useful inside effects when you need to read a signal without subscribing to it. `untrack` is a shorter alias for `runUntracked` -- both are identical.
11661179

11671180
```ts
1168-
import { runUntracked, signal, effect } from "@pyreon/reactivity"
1181+
import { runUntracked, untrack, signal, effect } from "@pyreon/reactivity"
11691182

11701183
const a = signal(1)
11711184
const b = signal(2)
@@ -1473,6 +1486,8 @@ editor.redo() // current() === "Hello, World"
14731486
14741487
<APICard name="runUntracked" type="function" signature="runUntracked<T>(fn: () => T): T" description="Runs a function without tracking any reactive dependencies." />
14751488
1489+
<APICard name="untrack" type="function" signature="untrack<T>(fn: () => T): T" description="Alias for runUntracked. Shorter name, identical behavior." />
1490+
14761491
<APICard name="createSelector" type="function" signature="createSelector<T>(source: () => T): (key: T) => boolean" description="Creates an O(1) equality selector for efficient list item matching." />
14771492
14781493
### Debug Utilities

0 commit comments

Comments
 (0)