@solidrt/core 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -29,7 +29,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
29
29
  ```
30
30
 
31
31
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
32
- 2.0.0-beta.14); bun resolves them from peerDependencies.
32
+ 2.0.0-beta.15); bun resolves them from peerDependencies.
33
33
 
34
34
  ## Element model (the parts that are easy to get wrong)
35
35
 
@@ -63,6 +63,24 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
63
63
  directly-positioned, often-animating elements (e.g. hundreds of balls), `d-`
64
64
  skips the per-element layout that plain elements would incur.
65
65
 
66
+ - Layout-affecting vs not (this matters for per-frame work). Props fall in three
67
+ buckets, split by where they take effect:
68
+ - `LayoutProps` - width/height, min/max sizes, margin, padding, `position` and
69
+ its `top`/`right`/`bottom`/`left` offsets, flex*/gap/display, grid*,
70
+ aspectRatio, overflow. Changing ANY of these triggers a Taffy reflow of the
71
+ node and its subtree.
72
+ - `TransformProps` - x, y, scale/scaleX/scaleY, rotate/rotateX/rotateY,
73
+ perspective, cx/cy, scrollX/scrollY. Applied at paint/composite; NO reflow.
74
+ - `PaintProps` - color, drawStyle, strokeWidth, blendMode, radius. Also no
75
+ reflow.
76
+ So to MOVE or animate an element - dragging, transitions, per-frame motion -
77
+ translate it with the transform `x`/`y` (or scale/rotate), never by animating
78
+ `left`/`top`/`margin`/`width`. Common trap: `left`/`top` read like "position"
79
+ but they are LAYOUT offsets (for `position:absolute`), so driving them every
80
+ frame reflows the tree. Anchor the element once with layout (e.g.
81
+ `position:absolute` at `left:0,top:0`, or just let normal flow place it) and
82
+ then translate it with `x`/`y`.
83
+
66
84
  - Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
67
85
  with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
68
86
  onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "./gpu": "./src/gpu.ts",
12
12
  "./image": "./src/image.ts",
13
13
  "./microphone": "./src/microphone.ts",
14
+ "./scroll": "./src/scroll.ts",
14
15
  "./speech-recognition": "./src/speech-recognition.ts",
15
16
  "./text-input": "./src/text-input.ts",
16
17
  "./jsx-runtime": "./jsx-runtime.d.ts",
@@ -25,11 +26,11 @@
25
26
  "colord": "^2.9.3"
26
27
  },
27
28
  "devDependencies": {
28
- "@solidrt/flux-types": "0.0.13"
29
+ "@solidrt/flux-types": "0.0.0"
29
30
  },
30
31
  "peerDependencies": {
31
- "@solidjs/signals": "2.0.0-beta.14",
32
- "@solidjs/universal": "2.0.0-beta.14",
33
- "solid-js": "2.0.0-beta.14"
32
+ "@solidjs/signals": "2.0.0-beta.15",
33
+ "@solidjs/universal": "2.0.0-beta.15",
34
+ "solid-js": "2.0.0-beta.15"
34
35
  }
35
36
  }
package/src/gpu.ts CHANGED
@@ -6,6 +6,13 @@
6
6
  import { getOwner, onCleanup } from "@solidjs/signals"
7
7
  import * as gpu from "flux:gpu"
8
8
 
9
+ // The imperative destroy, surfaced here for the manual-cleanup path documented
10
+ // on the create* helpers (textures made outside a reactive scope, e.g. after an
11
+ // await, are not auto-freed). Re-exported so callers that depend on
12
+ // @solidrt/core -- like @solidrt/components -- can free textures without
13
+ // importing flux directly.
14
+ export { destroyTexture } from "flux:gpu"
15
+
9
16
  /**
10
17
  * Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
11
18
  * it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
package/src/index.ts CHANGED
@@ -29,3 +29,54 @@ export type {
29
29
  Color,
30
30
  } from "./types"
31
31
  export type { MeasureTextOptions } from "flux:rendertree"
32
+
33
+ // --- Authoring-surface re-exports -------------------------------------------
34
+ // A SolidRT app is built from three substrate packages: @solidjs/signals
35
+ // (reactivity), solid-js (control-flow components), and @solidjs/universal (the
36
+ // renderer factory, surfaced via ./renderer). Forwarding the app-facing pieces
37
+ // here means an app imports its whole vocabulary from "@solidrt/core" instead of
38
+ // having to know which substrate package each symbol lives in. These are already
39
+ // peerDependencies, so this adds no new dependency. Curated on purpose - do not
40
+ // `export *` from solid-js, which would leak DOM/hydration-only helpers that are
41
+ // meaningless on the flux runtime.
42
+
43
+ // Reactivity (from @solidjs/signals).
44
+ export {
45
+ createSignal,
46
+ createMemo,
47
+ createEffect,
48
+ createRenderEffect,
49
+ createRoot,
50
+ createStore,
51
+ reconcile,
52
+ mapArray,
53
+ repeat,
54
+ untrack,
55
+ onCleanup,
56
+ onSettled,
57
+ } from "@solidjs/signals"
58
+ export type { Accessor, Setter, Signal, Store, StoreSetter } from "@solidjs/signals"
59
+
60
+ // Control flow, components, and context (from solid-js).
61
+ export {
62
+ For,
63
+ Show,
64
+ Switch,
65
+ Match,
66
+ Repeat,
67
+ Loading,
68
+ Errored,
69
+ Reveal,
70
+ lazy,
71
+ createUniqueId,
72
+ createContext,
73
+ useContext,
74
+ children,
75
+ } from "solid-js"
76
+ export type {
77
+ Component,
78
+ ParentComponent,
79
+ FlowComponent,
80
+ VoidComponent,
81
+ ComponentProps,
82
+ } from "solid-js"
package/src/renderer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createRoot } from "@solidjs/signals"
1
+ import { createRoot, onCleanup } from "@solidjs/signals"
2
2
  import { createRenderer } from "@solidjs/universal"
3
3
  import * as tree from "flux:rendertree"
4
4
  import { attachWindow } from "./window"
@@ -30,6 +30,34 @@ function createProxyNode(elementType: ElementType): ProxyNode {
30
30
  return node
31
31
  }
32
32
 
33
+ // Detaches `node` from `parent` and destroys it (and all descendants) on both
34
+ // the JS and native sides. Hoisted out of the renderer config so createPortal
35
+ // can reuse it (createRenderer does not return its removeNode hook).
36
+ function removeNode(parent: ProxyNode, node: ProxyNode): void {
37
+ if (!node || !parent) return
38
+
39
+ // console.debug("[srt] removeNode", parent.id, node.id)
40
+
41
+ // Update JS tree references
42
+ let index = parent.children.indexOf(node)
43
+ if (index !== -1) {
44
+ parent.children.splice(index, 1)
45
+ }
46
+ node.parent = undefined
47
+
48
+ tree.deleteNode(parent.id, node.id)
49
+
50
+ // Recursively clean up node and all descendants. Clear focus before
51
+ // dropping handlers so onBlur still fires for a focused descendant.
52
+ let cleanup = (n: ProxyNode) => {
53
+ for (let child of n.children) cleanup(child)
54
+ if (n.id === getFocusedNodeId()) setFocus(null)
55
+ nodes.delete(n.id)
56
+ cleanupNodeHandlers(n.id)
57
+ }
58
+ cleanup(node)
59
+ }
60
+
33
61
  export let {
34
62
  effect,
35
63
  memo,
@@ -116,30 +144,7 @@ export let {
116
144
  }
117
145
  },
118
146
 
119
- removeNode: (parent: ProxyNode, node: ProxyNode): void => {
120
- if (!node || !parent) return
121
-
122
- // console.debug("[srt] removeNode", parent.id, node.id)
123
-
124
- // Update JS tree references
125
- let index = parent.children.indexOf(node)
126
- if (index !== -1) {
127
- parent.children.splice(index, 1)
128
- }
129
- node.parent = undefined
130
-
131
- tree.deleteNode(parent.id, node.id)
132
-
133
- // Recursively clean up node and all descendants. Clear focus before
134
- // dropping handlers so onBlur still fires for a focused descendant.
135
- let cleanup = (n: ProxyNode) => {
136
- for (let child of n.children) cleanup(child)
137
- if (n.id === getFocusedNodeId()) setFocus(null)
138
- nodes.delete(n.id)
139
- cleanupNodeHandlers(n.id)
140
- }
141
- cleanup(node)
142
- },
147
+ removeNode,
143
148
 
144
149
  getParentNode: (node: ProxyNode) => node?.parent,
145
150
  getFirstChild: (node: ProxyNode) => node?.children[0],
@@ -152,6 +157,10 @@ export let {
152
157
  },
153
158
  })
154
159
 
160
+ // The app's single <window> node, set by render(). Serves as the default mount
161
+ // target for createPortal (single window by design, so one ambient ref).
162
+ let windowRoot: ProxyNode | undefined
163
+
155
164
  /**
156
165
  * Mounts a SolidRT app. Call once at the top level: `render(() => <App />)`.
157
166
  * The element returned by `code` MUST be a `<window>` (it becomes the native
@@ -164,7 +173,32 @@ export function render(code: () => any) {
164
173
  if (!root || root.elementType !== "window") {
165
174
  throw new Error("render() root must be a <window> element")
166
175
  }
176
+ windowRoot = root
167
177
  attachWindow(root.id)
168
178
  insert(null, root)
169
179
  })
170
180
  }
181
+
182
+ /**
183
+ * Relocates an already-built node out of its lexical position to `mount` (the
184
+ * window root by default), then removes it again when the surrounding reactive
185
+ * scope disposes. The low-level portal primitive: it moves a single node and
186
+ * nothing more. Conveniences (an overlay layer, centering, a backdrop) belong
187
+ * in higher packages built on top of it.
188
+ *
189
+ * `node` is a concrete node, not an accessor: its own children (including any
190
+ * reactive content) are already wired by the JSX that built it and keep working
191
+ * wherever it is mounted. We only move the root.
192
+ *
193
+ * The default mount is the window's flex root, so a portaled node that is not
194
+ * `position: "absolute"` will take flow space and displace app content. Position
195
+ * the portal root absolutely, or pass a `mount` target that does it for you.
196
+ */
197
+ export function createPortal(node: ProxyNode, mount?: ProxyNode): void {
198
+ let target = mount ?? windowRoot
199
+ if (!target) {
200
+ throw new Error("createPortal: no mount target (called before render()?)")
201
+ }
202
+ insertNode(target, node)
203
+ onCleanup(() => removeNode(target, node))
204
+ }
package/src/scroll.ts ADDED
@@ -0,0 +1,93 @@
1
+ // Headless scroll mechanism. This primitive owns the objective part of a
2
+ // scrollable region -- the offset and its clamping against the measured content
3
+ // and viewport sizes -- and nothing with a UI opinion. Wheel/drag input,
4
+ // momentum, scrollbars and styling are policy and belong to the component (the
5
+ // "skin") that composes this, the same way createCaretScroll backs TextInput.
6
+
7
+ import { createSignal, flush } from "@solidjs/signals"
8
+ import { getBoundingBox } from "./core"
9
+ import { onLayout } from "./window"
10
+
11
+ export type ScrollAxis = "vertical" | "horizontal" | "both"
12
+
13
+ export type ScrollOffset = { x: number; y: number }
14
+
15
+ export type ScrollOptions = {
16
+ /** Which axes can scroll. Locked axes are pinned to 0. Default "vertical". */
17
+ axis?: ScrollAxis
18
+ }
19
+
20
+ export type Scroll = {
21
+ /** Current clamped offset, as a reactive accessor. */
22
+ offset(): ScrollOffset
23
+ /** Scroll by a delta (positive moves content up/left), clamped to range. */
24
+ scrollBy(dx: number, dy: number): void
25
+ /** Scroll to an absolute offset, clamped to range. */
26
+ scrollTo(x: number, y: number): void
27
+ }
28
+
29
+ /**
30
+ * Returns the scroll offset for a viewport node given its content node. The
31
+ * offset is retained between frames and re-clamped in onLayout against the
32
+ * current content-vs-viewport overflow, so the view stays valid when content
33
+ * grows or shrinks (e.g. an offset that scrolled to the bottom snaps up when the
34
+ * list gets shorter). scrollBy/scrollTo clamp against the most recently measured
35
+ * range. Pure geometry: no input handling and no visual policy.
36
+ *
37
+ * The viewport node is the clipping box (overflow hidden); the content node is
38
+ * the inner wrapper that holds the children and takes their natural size. Apply
39
+ * the returned offset to the viewport's scrollX/scrollY.
40
+ */
41
+ export function createScroll(
42
+ viewport: () => { id: number } | undefined,
43
+ content: () => { id: number } | undefined,
44
+ options: ScrollOptions = {},
45
+ ): Scroll {
46
+ let axis = options.axis ?? "vertical"
47
+ let canX = axis === "horizontal" || axis === "both"
48
+ let canY = axis === "vertical" || axis === "both"
49
+
50
+ let [offset, setOffset] = createSignal<ScrollOffset>({ x: 0, y: 0 })
51
+
52
+ // Last measured overflow, refreshed each layout. scrollBy/scrollTo clamp
53
+ // against these between layouts; onLayout re-clamps once new sizes are known.
54
+ let maxX = 0
55
+ let maxY = 0
56
+
57
+ let clamp = (x: number, y: number): ScrollOffset => ({
58
+ x: canX ? Math.max(0, Math.min(x, maxX)) : 0,
59
+ y: canY ? Math.max(0, Math.min(y, maxY)) : 0,
60
+ })
61
+
62
+ let set = (x: number, y: number) => {
63
+ let cur = offset()
64
+ let next = clamp(x, y)
65
+ if (next.x !== cur.x || next.y !== cur.y) setOffset(next)
66
+ }
67
+
68
+ onLayout(() => {
69
+ let vp = viewport()
70
+ let ct = content()
71
+ if (!vp || !ct) return
72
+ let vb = getBoundingBox(vp)
73
+ let cb = getBoundingBox(ct)
74
+ if (!vb || !cb) return
75
+ maxX = Math.max(0, cb.width - vb.width)
76
+ maxY = Math.max(0, cb.height - vb.height)
77
+ let cur = offset()
78
+ let next = clamp(cur.x, cur.y)
79
+ if (next.x !== cur.x || next.y !== cur.y) {
80
+ setOffset(next)
81
+ flush()
82
+ }
83
+ })
84
+
85
+ return {
86
+ offset,
87
+ scrollBy: (dx, dy) => {
88
+ let cur = offset()
89
+ set(cur.x + dx, cur.y + dy)
90
+ },
91
+ scrollTo: (x, y) => set(x, y),
92
+ }
93
+ }
package/src/types.d.ts CHANGED
@@ -153,14 +153,22 @@ export interface TransformProps {
153
153
  scrollY?: number
154
154
  }
155
155
 
156
+ // Window-relative pointer coordinates are reported as clientX/clientY (matching
157
+ // the DOM MouseEvent). pointerType distinguishes mouse from touch; button is the
158
+ // pressed button on down/up (0 = primary); the modifier flags mirror the DOM.
156
159
  export interface PointerEvent {
157
- x: number
158
- y: number
160
+ clientX: number
161
+ clientY: number
162
+ pointerId: number
163
+ pointerType: "mouse" | "touch" | "pen" | (string & {})
164
+ button?: number
165
+ shiftKey: boolean
166
+ ctrlKey: boolean
167
+ altKey: boolean
168
+ metaKey: boolean
159
169
  }
160
170
 
161
- export interface WheelEvent {
162
- x: number
163
- y: number
171
+ export interface WheelEvent extends PointerEvent {
164
172
  deltaX: number
165
173
  deltaY: number
166
174
  }