@solidrt/core 0.0.50 → 0.0.52

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.
Files changed (42) hide show
  1. package/AGENTS.md +102 -21
  2. package/README.md +1 -1
  3. package/agents/painting.md +61 -0
  4. package/agents/performance.md +216 -0
  5. package/docs/index.md +154 -0
  6. package/docs/reference/detached.md +85 -0
  7. package/docs/reference/drawing.md +95 -0
  8. package/docs/reference/elements.md +56 -0
  9. package/docs/reference/gpu.md +204 -0
  10. package/docs/reference/index.md +50 -0
  11. package/docs/reference/input.md +58 -0
  12. package/docs/reference/layout.md +44 -0
  13. package/docs/reference/shaders.md +46 -0
  14. package/docs/reference/text.md +46 -0
  15. package/docs/reference/transforms.md +35 -0
  16. package/docs/reference/types.md +34 -0
  17. package/examples/README.md +7 -5
  18. package/examples/{sound.tsx → audio.tsx} +1 -1
  19. package/examples/gpu-pipeline.tsx +2 -2
  20. package/examples/gpu-sprites.tsx +102 -0
  21. package/examples/line-points.tsx +145 -0
  22. package/examples/parse-svg.tsx +6 -6
  23. package/examples/responsive-grid.tsx +1 -1
  24. package/examples/scroll.tsx +2 -2
  25. package/examples/snapshot-texture.tsx +72 -0
  26. package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
  27. package/jsx-runtime.d.ts +15 -14
  28. package/package.json +11 -9
  29. package/src/{sound.ts → audio.ts} +68 -15
  30. package/src/color.ts +17 -18
  31. package/src/core.ts +40 -3
  32. package/src/data.ts +99 -0
  33. package/src/gpu.ts +88 -34
  34. package/src/index.ts +9 -3
  35. package/src/logo.tsx +92 -0
  36. package/src/renderer.ts +219 -53
  37. package/src/runtime-modules.d.ts +7 -2
  38. package/src/scroll.ts +51 -15
  39. package/src/svg.ts +1 -1
  40. package/src/text-input.ts +297 -61
  41. package/src/types.d.ts +291 -31
  42. package/src/window.ts +110 -14
package/src/renderer.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { createRoot, onCleanup } from "@solidjs/signals"
1
+ import { createRoot, onCleanup, NotReadyError } from "@solidjs/signals"
2
2
  import { createRenderer } from "@solidjs/universal"
3
+ import { createErrorBoundary } from "solid-js"
3
4
  import type { Element } from "solid-js"
4
5
  import * as tree from "flux:rendertree"
5
- import { attachWindow } from "./window"
6
+ import { attachWindow, setWindowRoot } from "./window"
6
7
  import { setEventHandler, setFocusable, setTextInputHints, cleanupNode, focusedNode, setFocus } from "./core"
7
- import { parseColor, isGradient } from "./color"
8
8
 
9
9
  export { getEventHandler } from "./core"
10
10
 
@@ -166,53 +166,55 @@ function setTreeProperty(node: ProxyNode, name: string, value: unknown): void {
166
166
  // Shared by the renderer's setProperty hook and by createElement, which since
167
167
  // the dom-expressions "universal" template passes static props inline as a
168
168
  // second argument rather than as separate setProp calls.
169
+ // A property's route is a function of its name alone, so the classifying
170
+ // regex and compares run once per unique name; per write it is one Map get.
171
+ const ROUTE_TREE = 0
172
+ const ROUTE_EVENT = 1
173
+ const ROUTE_FOCUSABLE = 2
174
+ const ROUTE_HINTS = 3
175
+ let propRoutes = new Map<string, number>()
176
+
177
+ function routeFor(name: string): number {
178
+ let route = propRoutes.get(name)
179
+ if (route === undefined) {
180
+ route = /^on[A-Z]/.test(name)
181
+ ? ROUTE_EVENT
182
+ : name === "focusable"
183
+ ? ROUTE_FOCUSABLE
184
+ : name === "textInputHints"
185
+ ? ROUTE_HINTS
186
+ : ROUTE_TREE
187
+ propRoutes.set(name, route)
188
+ }
189
+ return route
190
+ }
191
+
169
192
  function applyProp<T>(node: ProxyNode, name: string, value: T): void {
170
193
  if (!node) return
171
194
 
172
195
  // console.debug("[srt] applyProp", node.id, name, value)
173
196
 
174
- if (/^on[A-Z]/.test(name) && (value == null || typeof value === "function")) {
175
- setEventHandler(node.id, name, value as Function | null | undefined)
176
- return
177
- }
178
-
179
- if (name === "focusable") {
180
- setFocusable(node.id, value === true)
181
- return
182
- }
183
-
184
- if (name === "textInputHints") {
185
- setTextInputHints(node.id, value as any)
186
- return
187
- }
188
-
189
- if (name === "color" && isGradient(value)) {
190
- setTreeProperty(node, name, value)
191
- return
192
- }
193
-
194
- if (name === "color" && typeof value === "string") {
195
- setTreeProperty(node, name, parseColor(value))
196
- return
197
+ switch (routeFor(name)) {
198
+ case ROUTE_EVENT:
199
+ // A non-function, non-null value on an on* name is not a handler;
200
+ // fall through to the tree so the native side rejects it.
201
+ if (value == null || typeof value === "function") {
202
+ setEventHandler(node.id, name, value as Function | null | undefined)
203
+ return
204
+ }
205
+ break
206
+ case ROUTE_FOCUSABLE:
207
+ setFocusable(node.id, value === true)
208
+ return
209
+ case ROUTE_HINTS:
210
+ setTextInputHints(node.id, value as any)
211
+ return
197
212
  }
198
213
 
199
214
  setTreeProperty(node, name, value)
200
215
  }
201
216
 
202
- export let {
203
- effect,
204
- memo,
205
- createComponent,
206
- createElement,
207
- createTextNode,
208
- insertNode,
209
- insert,
210
- spread,
211
- setProp,
212
- mergeProps,
213
- applyRef,
214
- ref,
215
- } = createRenderer<ProxyNode>({
217
+ let renderer = createRenderer<ProxyNode>({
216
218
  createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
217
219
  let proxy = createProxyNode(elementType)
218
220
 
@@ -221,11 +223,13 @@ export let {
221
223
  if (elementType === "window") tree.createRoot(proxy.id)
222
224
  else tree.createNode(proxy.id, elementType)
223
225
 
224
- // The universal JSX template hands static props here as an object; children
225
- // and ref arrive through their own hooks, so skip them.
226
+ // The universal JSX template hands static props here as an object. The
227
+ // compiler routes children/ref expressions through their own hooks, so
228
+ // those names only appear here in degenerate literal forms (children="hi",
229
+ // a bare ref) - let them flow to the tree so the unknown-property warning
230
+ // reports them instead of dropping them silently.
226
231
  if (props) {
227
232
  for (let name in props) {
228
- if (name === "children" || name === "ref") continue
229
233
  applyProp(proxy, name, props[name])
230
234
  }
231
235
  }
@@ -295,28 +299,185 @@ export let {
295
299
  },
296
300
  })
297
301
 
298
- // The app's single <window> node, set by render(). Serves as the default mount
299
- // target for createPortal (single window by design, so one ambient ref).
302
+ export let { memo, createComponent, createElement, createTextNode, insertNode, spread, setProp, mergeProps, applyRef, ref } =
303
+ renderer
304
+ let { effect: rawEffect, insert: rawInsert } = renderer
305
+
306
+ // ------ Per-node error containment --------
307
+ //
308
+ // Every reactive write into the tree goes through two exports the compiled
309
+ // JSX calls: `effect` (an element's dynamic props) and `insert` (a child
310
+ // expression). An error thrown while computing either is contained right
311
+ // there: the props or children keep their last good value, the effect stays
312
+ // subscribed (the throwing read is tracked, so fixing it recomputes and the
313
+ // node recovers on its own), and the rest of the app keeps running. Without
314
+ // this one unclaimed error halts the whole reactive system. A NotReadyError
315
+ // is not an error but a pending async read on its way to the nearest
316
+ // <Loading>, so it passes through. Reported once per site until it recovers,
317
+ // not once per run. What escapes these two (a user createEffect that throws)
318
+ // still reaches render()'s root boundary.
319
+ const SKIP = Symbol("skip")
320
+
321
+ function guard<T>(fn: (prev?: T) => T, describe: () => string, nested: boolean, empty: T): (prev?: T) => T {
322
+ let last = empty
323
+ let failing = false
324
+ return (prev?: T) => {
325
+ try {
326
+ let value = fn(prev === SKIP ? undefined : prev)
327
+ if (failing) {
328
+ failing = false
329
+ console.warn(`Recovered: ${describe()} computes again`)
330
+ }
331
+ // A child expression resolving to a function is read by an inner
332
+ // effect (universal's insert); that read gets the same containment.
333
+ // Solid's flatten only unwraps zero-arity functions and inserts any
334
+ // other function as a node, so only accessors are wrapped, and the
335
+ // wrapper keeps arity 0.
336
+ if (nested && typeof value === "function" && value.length === 0) {
337
+ let inner = guard(value as any, describe, true, empty)
338
+ value = (() => inner()) as any
339
+ }
340
+ last = value
341
+ return value
342
+ } catch (e) {
343
+ if (e instanceof NotReadyError) throw e
344
+ if (!failing) {
345
+ failing = true
346
+ console.error(`Contained error: ${describe()} threw and keeps its last value until it computes again.`, e)
347
+ }
348
+ return last
349
+ }
350
+ }
351
+ }
352
+
353
+ // Universal's declarations trail its runtime (effect takes options, insert
354
+ // takes initial and options), so the wrappers carry the runtime signatures.
355
+ type EffectFn = <T>(fn: (prev?: T) => T, effectFn?: (value: T, prev?: T) => void, options?: unknown) => void
356
+ type InsertFn = (parent: ProxyNode, accessor: unknown, marker?: unknown, initial?: unknown, options?: unknown) => ProxyNode
357
+ let effectRaw = rawEffect as unknown as EffectFn
358
+ let insertRaw = rawInsert as unknown as InsertFn
359
+
360
+ export let effect: EffectFn = (fn, effectFn, options) =>
361
+ effectRaw<any>(
362
+ guard<any>(fn, () => "an element's prop expression", false, SKIP),
363
+ effectFn && ((value, prev) => (value === SKIP ? undefined : effectFn(value, prev === SKIP ? undefined : prev))),
364
+ options,
365
+ )
366
+
367
+ export let insert: InsertFn = (parent, accessor, marker, initial, options) =>
368
+ insertRaw(
369
+ parent,
370
+ typeof accessor === "function"
371
+ ? guard(accessor as any, () => `a child expression of <${parent.elementType}> ${getNodePath(parent.id).join("/")}`, true, undefined)
372
+ : accessor,
373
+ marker,
374
+ initial,
375
+ options,
376
+ )
377
+
378
+ // The current <window> node: the app's, or the error window standing in for
379
+ // it. Serves as the default mount target for createPortal (single window by
380
+ // design, so one ambient ref).
300
381
  let windowRoot: ProxyNode | undefined
382
+ let rendered = false
383
+ // Ids of error windows built by the root boundary, alive only while shown.
384
+ let errorWindows = new Set<number>()
301
385
 
302
386
  /**
303
387
  * Mounts a SolidRT app. Call once at the top level: `render(() => <App />)`.
304
388
  * The element returned by `code` MUST be a `<window>` (it becomes the native
305
389
  * window and root of the render tree); anything else throws. Runs inside a
306
390
  * reactive root, so the whole tree is disposed together on engine reload.
391
+ *
392
+ * The whole app, window included, sits inside an error boundary: an error no
393
+ * <Errored> claims replaces the app's window with an error window (message,
394
+ * stack, a reset button) instead of halting the reactive system for good.
395
+ * The app's subtree stays alive behind it - the boundary keeps it and marks
396
+ * only the failed computations - so reset recomputes those in place and the
397
+ * same window node comes back.
307
398
  */
308
399
  export function render(code: () => any) {
400
+ // Once per app: there is no unmount; teardown is engine teardown.
401
+ if (rendered) {
402
+ throw new Error("render() already called; an app has exactly one render()")
403
+ }
404
+ rendered = true
309
405
  createRoot(() => {
310
- let root = code()
311
- if (!root || root.elementType !== "window") {
312
- throw new Error("render() root must be a <window> element")
313
- }
314
- windowRoot = root
315
- attachWindow(root.id)
316
- insert(null, root)
406
+ let root = createErrorBoundary(
407
+ () => {
408
+ let win = code()
409
+ if (!win || win.elementType !== "window") {
410
+ throw new Error("render() root must be a <window> element")
411
+ }
412
+ return win
413
+ },
414
+ (error, reset) => {
415
+ // The boundary hands the error as an accessor.
416
+ let err = error()
417
+ console.error("Uncaught error: the app is replaced by the error window until reset or reload.", err)
418
+ let win = errorWindow(err, reset)
419
+ errorWindows.add(win.id)
420
+ return win
421
+ },
422
+ )
423
+ rawEffect(
424
+ () => root() as ProxyNode,
425
+ (win, prev) => swapRoot(win, prev),
426
+ )
317
427
  })
318
428
  }
319
429
 
430
+ // The boundary's value changed: the app's window on mount and after a
431
+ // successful reset, an error window after an error. Creating a window already
432
+ // made it the native root; setRoot is the way back to an existing one.
433
+ function swapRoot(win: ProxyNode, prev?: ProxyNode) {
434
+ windowRoot = win
435
+ if (prev === undefined) {
436
+ attachWindow(win.id)
437
+ return
438
+ }
439
+ // Creating the error window made it the native root; the app's window
440
+ // coming back is the case that needs the explicit way back.
441
+ if (!errorWindows.has(win.id)) tree.setRoot(win.id)
442
+ setWindowRoot(win.id)
443
+ // Keys route to the focused node; one inside the hidden window must not
444
+ // keep hearing them.
445
+ setFocus(null)
446
+ // The app's window survives behind an error window (the boundary keeps its
447
+ // subtree for reset); an error window replaced by anything is dead.
448
+ if (errorWindows.has(prev.id) || !errorWindows.has(win.id)) {
449
+ errorWindows.delete(prev.id)
450
+ destroyNode(prev)
451
+ }
452
+ }
453
+
454
+ // The error window: the in-app sibling of the runtime's startup BSOD, built
455
+ // from the primitives directly (no JSX in core). Static content; the reset
456
+ // button recomputes the failed sources, and a reload replaces everything.
457
+ function errorWindow(err: unknown, reset: () => void): ProxyNode {
458
+ let message = err instanceof Error ? err.message : String(err)
459
+ let stack = err instanceof Error && err.stack ? err.stack : ""
460
+ let text = (content: string, props: Record<string, any>) => {
461
+ let node = createElement("text", props)
462
+ insertNode(node, createTextNode(content))
463
+ return node
464
+ }
465
+ let win = createElement("window", { title: "Application error" })
466
+ insertNode(win, createElement("d-rect", { color: "#1144bb" }))
467
+ let column = createElement("view", { flexGrow: 1, flexDirection: "column", padding: 40, gap: 12 })
468
+ insertNode(column, text(":(", { color: "white", fontSize: 64, fontWeight: 700 }))
469
+ insertNode(column, text("Something went wrong", { color: "white", fontSize: 22 }))
470
+ insertNode(column, text(message, { color: "white", fontSize: 16 }))
471
+ if (stack) insertNode(column, text(stack, { color: "#aac2ff", fontSize: 12, fontFamily: "mono" }))
472
+ insertNode(column, text("Fix the error and save to reload, or reset to retry the failed computations.", { color: "#aac2ff", fontSize: 14 }))
473
+ let button = createElement("view", { alignSelf: "flex-start", padding: 12, onPointerDown: () => reset() })
474
+ insertNode(button, createElement("d-rect", { color: "white", radius: 6 }))
475
+ insertNode(button, text("Reset", { color: "#1144bb", fontSize: 16, fontWeight: 600 }))
476
+ insertNode(column, button)
477
+ insertNode(win, column)
478
+ return win
479
+ }
480
+
320
481
  /**
321
482
  * Relocates an already-built node out of its lexical position to `mount` (the
322
483
  * window root by default), then removes it again when the surrounding reactive
@@ -348,6 +509,11 @@ export function createPortal(node: Element, mount?: ProxyNode): null {
348
509
  throw new Error("createPortal: node must be a single built element")
349
510
  }
350
511
  insertNode(target, node as ProxyNode)
351
- onCleanup(() => removeNode(target, node as ProxyNode))
512
+ // A destroyed mount target has already swept the portaled node with it
513
+ // (destroy walks the mount tree), so detaching then would hand freed ids to
514
+ // the native side, which panics. Gone from the proxy map = already freed.
515
+ onCleanup(() => {
516
+ if (nodes.has((node as ProxyNode).id)) removeNode(target, node as ProxyNode)
517
+ })
352
518
  return null
353
519
  }
@@ -47,6 +47,10 @@ declare module "*.ogg" {
47
47
  const bytes: Uint8Array
48
48
  export default bytes
49
49
  }
50
+ declare module "*.glb" {
51
+ const bytes: Uint8Array
52
+ export default bytes
53
+ }
50
54
 
51
55
  // UI event bus (lattice), provided by the runtime as a builtin module.
52
56
  // on/once return an unsubscribe function. Notable events: the routed pointer
@@ -88,8 +92,9 @@ declare module "srt:dev" {
88
92
  /**
89
93
  * Register a named debug command, listable and callable from the dev server
90
94
  * (the list_debug / call_debug MCP tools). `args` arrives JSON-parsed; the
91
- * return value must be JSON-serializable and synchronous (promises are not
92
- * awaited). Re-registering a name replaces it; registrations reset on hot
95
+ * return value must be JSON-serializable and synchronous (an async command's
96
+ * Promise is not awaited - the call errors). Re-registering a name replaces
97
+ * it; registrations reset on hot
93
98
  * reload, so register at module init. Callable in every build, but only dev
94
99
  * clients ever invoke commands.
95
100
  */
package/src/scroll.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  // scrollable region -- the offset and its clamping against the measured content
3
3
  // and viewport sizes -- and nothing with a UI opinion. Wheel/drag input,
4
4
  // momentum, scrollbars and styling are policy and belong to the component (the
5
- // "skin") that composes this, the same way createCaretScroll backs TextInput.
5
+ // "skin") that composes this, the same way createTextEditorLayout backs TextInput.
6
6
 
7
- import { createSignal, flush } from "@solidjs/signals"
7
+ import { createSignal } from "@solidjs/signals"
8
8
  import { getBoundingBox } from "./core"
9
9
  import { onLayout } from "./window"
10
10
 
@@ -12,6 +12,15 @@ export type ScrollAxis = "vertical" | "horizontal" | "both"
12
12
 
13
13
  export type ScrollOffset = { x: number; y: number }
14
14
 
15
+ /** The web's scroll behavior word, simplified: "instant" asks the skin for no
16
+ * motion; "auto" (the default) and "smooth" both mean its usual motion, since
17
+ * the default already animates. */
18
+ export type ScrollBehavior = "auto" | "instant" | "smooth"
19
+
20
+ /** Target of scrollTo/scrollBy. An omitted axis keeps its offset (scrollTo)
21
+ * or moves by 0 (scrollBy). */
22
+ export type ScrollToOptions = { x?: number; y?: number; behavior?: ScrollBehavior }
23
+
15
24
  export type ScrollOptions = {
16
25
  /** Which axes can scroll. Locked axes are pinned to 0. Default "vertical". */
17
26
  axis?: ScrollAxis
@@ -20,10 +29,18 @@ export type ScrollOptions = {
20
29
  export type Scroll = {
21
30
  /** Current clamped offset, as a reactive accessor. */
22
31
  offset(): ScrollOffset
23
- /** Scroll by a delta (positive moves content up/left), clamped to range. */
24
- scrollBy(dx: number, dy: number): void
32
+ /** Largest reachable offset per axis (content overflow, 0 on a locked axis),
33
+ * as a reactive accessor refreshed each layout. Watching it is how a scroll
34
+ * policy learns that the content or the viewport changed size. */
35
+ range(): ScrollOffset
36
+ /** Behavior of the latest scrollTo/scrollBy, as a reactive accessor: the
37
+ * skin reads it to withhold its motion for an instant write. Layout
38
+ * re-clamps leave it alone. */
39
+ behavior(): ScrollBehavior
25
40
  /** Scroll to an absolute offset, clamped to range. */
26
- scrollTo(x: number, y: number): void
41
+ scrollTo(options: ScrollToOptions): void
42
+ /** Scroll by a delta (positive moves content up/left), clamped to range. */
43
+ scrollBy(options: ScrollToOptions): void
27
44
  }
28
45
 
29
46
  /**
@@ -32,7 +49,9 @@ export type Scroll = {
32
49
  * current content-vs-viewport overflow, so the view stays valid when content
33
50
  * grows or shrinks (e.g. an offset that scrolled to the bottom snaps up when the
34
51
  * list gets shorter). scrollBy/scrollTo clamp against the most recently measured
35
- * range. Pure geometry: no input handling and no visual policy.
52
+ * range. Pure geometry: no input handling and no visual policy. Anything beyond
53
+ * clamping (following a growing log, keeping an item in view) is a policy the
54
+ * caller writes against `range()` and `offset()`.
36
55
  *
37
56
  * The viewport node is the clipping box (overflow hidden); the content node is
38
57
  * the inner wrapper that holds the children and takes their natural size. Apply
@@ -48,6 +67,11 @@ export function createScroll(
48
67
  let canY = axis === "vertical" || axis === "both"
49
68
 
50
69
  let [offset, setOffset] = createSignal<ScrollOffset>({ x: 0, y: 0 })
70
+ let [range, setRange] = createSignal<ScrollOffset>({ x: 0, y: 0 })
71
+ let [behavior, setBehavior] = createSignal<ScrollBehavior>("auto")
72
+ // Mirrors the signal: a setter's value is not readable until the flush, and
73
+ // two writes in one batch must still compare against the latest.
74
+ let lastBehavior: ScrollBehavior = "auto"
51
75
 
52
76
  // A scroll viewport with no explicit main-axis size resolves to 0 in flex
53
77
  // layout and its content silently vanishes - a classic trap (maxHeight alone
@@ -59,6 +83,8 @@ export function createScroll(
59
83
 
60
84
  // Last measured overflow, refreshed each layout. scrollBy/scrollTo clamp
61
85
  // against these between layouts; onLayout re-clamps once new sizes are known.
86
+ // Kept as plain values beside the `range` signal because a signal write is
87
+ // not readable until the flush, and clamping needs the number now.
62
88
  let maxX = 0
63
89
  let maxY = 0
64
90
 
@@ -67,10 +93,14 @@ export function createScroll(
67
93
  y: canY ? Math.max(0, Math.min(y, maxY)) : 0,
68
94
  })
69
95
 
70
- let set = (x: number, y: number) => {
96
+ let set = (x: number, y: number, b: ScrollBehavior = "auto") => {
71
97
  let cur = offset()
72
98
  let next = clamp(x, y)
73
99
  if (next.x !== cur.x || next.y !== cur.y) setOffset(next)
100
+ if (b !== lastBehavior) {
101
+ lastBehavior = b
102
+ setBehavior(b)
103
+ }
74
104
  }
75
105
 
76
106
  onLayout(() => {
@@ -94,20 +124,26 @@ export function createScroll(
94
124
  }
95
125
  maxX = Math.max(0, cb.width - vb.width)
96
126
  maxY = Math.max(0, cb.height - vb.height)
127
+ let r = range()
128
+ let rx = canX ? maxX : 0
129
+ let ry = canY ? maxY : 0
130
+ if (r.x !== rx || r.y !== ry) setRange({ x: rx, y: ry })
97
131
  let cur = offset()
98
132
  let next = clamp(cur.x, cur.y)
99
- if (next.x !== cur.x || next.y !== cur.y) {
100
- setOffset(next)
101
- flush()
102
- }
133
+ if (next.x !== cur.x || next.y !== cur.y) setOffset(next)
103
134
  })
104
135
 
105
136
  return {
106
137
  offset,
107
- scrollBy: (dx, dy) => {
138
+ range,
139
+ behavior,
140
+ scrollTo: (o) => {
108
141
  let cur = offset()
109
- set(cur.x + dx, cur.y + dy)
142
+ set(o.x ?? cur.x, o.y ?? cur.y, o.behavior)
143
+ },
144
+ scrollBy: (o) => {
145
+ let cur = offset()
146
+ set(cur.x + (o.x ?? 0), cur.y + (o.y ?? 0), o.behavior)
110
147
  },
111
- scrollTo: (x, y) => set(x, y),
112
148
  }
113
- }
149
+ }
package/src/svg.ts CHANGED
@@ -46,7 +46,7 @@ export type SvgDocument = {
46
46
  * that fits the document's coordinate space into its box:
47
47
  *
48
48
  * let doc = createMemo(() => parseSvg(src))
49
- * <view repaintBoundary viewBox={[doc().width, doc().height]} width={48} height={48}>
49
+ * <view repaintBoundary designSize={[doc().width, doc().height]} width={48} height={48}>
50
50
  * {doc().draws.map((draw) => <d-path {...draw} />)}
51
51
  * </view>
52
52
  *