@solidrt/core 0.0.22 → 0.0.23

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
@@ -7,6 +7,61 @@ trust this file and the types in src/types.d.ts and jsx-runtime.d.ts.
7
7
  SolidRT is a custom SolidJS renderer: it paints through a Rust runtime, not the
8
8
  DOM. There is no HTML, no CSS cascade, no `className`.
9
9
 
10
+ ## The window is device-sized - design fluid
11
+
12
+ A SolidRT window is host-sized and resizable, and the SAME app runs on phones,
13
+ tablets, and desktops. There is no default "desktop" size to design against.
14
+ Design fluid by default: derive sizing and layout from the live window, do not
15
+ hardcode desktop pixels.
16
+
17
+ - Size the layout from `windowSize()` / `capabilities.windowSizeClass`, not from
18
+ fixed pixel widths. Let flex (`flex`, `flexWrap`, `gap`) and percentages carry
19
+ the layout so it reflows on resize instead of clipping or leaving dead space.
20
+ - Gate hover-only affordances on `capabilities.hover`. Hover-zoom, hover-reveal,
21
+ and tooltips are dead on a touch device with no pointer that can rest. When
22
+ `capabilities.touch`, provide a tap/press path to the same action.
23
+ - `windowSizeClass` uses Material 3 width breakpoints (logical px): `compact`
24
+ (<600), `medium` (600-840), `expanded` (>=840). Drive column counts / layout
25
+ switches off it (see the responsive-grid example).
26
+
27
+ `env` and `capabilities` (both exported from `@solidrt/core`) are the two
28
+ objects that expose this. They are plain objects with REACTIVE GETTERS, not
29
+ functions - read them as `capabilities.windowSizeClass`, `env.displayScale`
30
+ (NOT `capabilities()`); the getter reads reactive state underneath, so a read
31
+ inside JSX / a memo / an effect re-runs when it changes. (`windowSize()` and
32
+ `safeArea()` from `./window` ARE functions - call those.)
33
+
34
+ ```ts
35
+ import { env, capabilities } from "@solidrt/core"
36
+
37
+ env.windowSize // { width, height } (same value as windowSize())
38
+ env.displayScale // device pixel ratio (hi-DPI factor)
39
+ env.safeArea // inset distances from each edge
40
+ env.systemTheme // "dark" | "light" | "unknown" (resolves after startup)
41
+ env.orientation // "portrait" | "landscape" | ... | "unknown"
42
+
43
+ capabilities.windowSizeClass // "compact" | "medium" | "expanded"
44
+ capabilities.hover // a pointer can rest over content (mouse/trackpad)
45
+ capabilities.touch // direct touch input present
46
+ capabilities.precisePointer // pixel-precise pointing (mouse/trackpad)
47
+ capabilities.keyboardNav // hardware-key navigation available
48
+ ```
49
+
50
+ Read behavior decisions through `capabilities`; read `env` directly only when
51
+ you need the raw fact (e.g. `env.displayScale` for asset sizing below).
52
+
53
+ ### Vectors vs raster, and hi-DPI
54
+
55
+ Because the drawn size is fluid and the display DPI varies, asset format is a
56
+ real design decision, not an afterthought:
57
+
58
+ - Prefer VECTORS (`<svg>`, `<d-path>`) whenever the render size is fluid or DPI
59
+ varies - they stay crisp at any size x `displayScale()`.
60
+ - RASTER (`<texture>` / `createImage`) needs source resolution >= displayed size
61
+ x `env.displayScale`, or it softens on hi-DPI. Author raster at 2-3x the
62
+ largest size you will ever draw it. A 256px PNG blown up large on a hi-DPI
63
+ tablet will look soft; the same art as SVG will not.
64
+
10
65
  ## Setup
11
66
 
12
67
  ```sh
@@ -16,18 +16,26 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
16
16
  - `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
17
17
  - `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
18
18
 
19
+ ## Performance
20
+ - `repaint-boundary.tsx` - `repaintBoundary` on a `<view>` to keep static content from rebuilding while a neighbor animates: `{true}` retains the recorded draw list, `"snapshot"` also retains the rasterized pixels as a GPU texture (for raster-expensive, screen-aligned, static subtrees).
21
+
22
+ ## Scrolling
23
+ - `scroll.tsx` - `createScroll`, the headless scroll primitive: it owns only the clamped offset (re-clamped on layout); you supply the viewport/content nodes via refs, apply the offset to `scrollX`/`scrollY`, and wire input (e.g. `onWheel`) to `scrollBy` yourself.
24
+
19
25
  ## Window state
20
26
  - `window-signals.tsx` - reactive `windowSize()` / `safeArea()` accessors (prefer over `onResize`).
27
+ - `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize.
21
28
 
22
29
  ## Overlays
23
30
  - `portal.tsx` - `createPortal` relocating content to the window root to escape clipping.
24
31
 
25
32
  ## Images and GPU
26
33
  - `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
27
- - `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated via `setShaderParams`.
34
+ - `inline-image.tsx` - bytes already in memory: `decodeImage` + `createTexture` (both synchronous) show an image with no `<Loading>` boundary. The sync counterpart to `image.tsx`.
35
+ - `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated by driving its `iTime` uniform declaratively through the `<texture params={{...}}>` prop.
28
36
 
29
37
  ## Vector graphics
30
38
  - `svg.tsx` - `<svg src={...}>` draws a whole SVG *document string* (not HTML/JSX children); multi-color fills vs a `currentColor` icon recolored by the `color` prop. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `src`.
31
39
 
32
40
  ## Bundling assets
33
- - `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (combine with `image.tsx` to display an inlined image). `with { type: "text" }` works the same way for a string.
41
+ - `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (the bytes are in memory, so `inline-image.tsx` displays them with the synchronous `decodeImage` + `createTexture` path). `with { type: "text" }` works the same way for a string.
@@ -6,8 +6,10 @@
6
6
  //
7
7
  // This example shows only the binary import itself: it reports the imported
8
8
  // file's length and leading bytes. What you do with the bytes afterwards is a
9
- // separate concern - e.g. hand them to createImage to decode and display an
10
- // image (see image.tsx), or to decodeImage/createTexture for manual control.
9
+ // separate concern. Since inlined bytes are already in memory, the synchronous
10
+ // path fits: decodeImage(bytes) + createTexture (no fetch, no <Loading>) - see
11
+ // inline-image.tsx. Reach for createImage instead when the source is a string
12
+ // URL loaded at runtime, or when you want the source to swap reactively.
11
13
  import { render } from "@solidrt/core"
12
14
  import bytes from "./logo.png" with { type: "binary" }
13
15
 
@@ -6,10 +6,11 @@
6
6
  // disposed.
7
7
  //
8
8
  // iResolution is filled in for you, but iTime is NOT - drive it (and any other
9
- // uniform) yourself with setShaderParams from onFrame to animate. The shader's
10
- // size is baked in at creation.
11
- import { render, onFrame } from "@solidrt/core"
12
- import { createShader, setShaderParams } from "@solidrt/core/gpu"
9
+ // uniform) declaratively via the <texture> element's params prop; it applies at
10
+ // the next repaint, so a signal updated every frame stays paced to actual frames.
11
+ // The shader's size is baked in at creation.
12
+ import { render, onFrame, createSignal } from "@solidrt/core"
13
+ import { createShader } from "@solidrt/core/gpu"
13
14
 
14
15
  let FRAGMENT = `
15
16
  void main() {
@@ -24,11 +25,12 @@ void main() {
24
25
 
25
26
  function App() {
26
27
  let id = createShader(FRAGMENT, 512, 512, { iTime: 0 })
27
- onFrame((tick) => setShaderParams(id, { iTime: tick / 1000 }))
28
+ let [time, setTime] = createSignal(0)
29
+ onFrame((tick) => setTime(tick / 1000))
28
30
 
29
31
  return (
30
32
  <window alignItems="center" justifyContent="center">
31
- <texture src={id} width={400} height={400} />
33
+ <texture src={id} params={{ iTime: time() }} width={400} height={400} />
32
34
  </window>
33
35
  )
34
36
  }
@@ -8,8 +8,19 @@
8
8
  // (this is the 2.0 async mechanic, not a manual undefined-signal + <Show>); a
9
9
  // load failure surfaces to <Errored>. Pass an accessor (createImage(() => src()))
10
10
  // to make the source reactive - the image reloads and the old texture is freed.
11
- // For manual control, decodeImage + createTexture (from @solidrt/core/gpu) are
12
- // the primitives underneath.
11
+ //
12
+ // The rule: createImage always suspends (needs <Loading>), because it is an
13
+ // async value. For bytes you already hold - a `with { type: "binary" }` import
14
+ // or anything in memory - skip it: decodeImage(bytes) + createTexture (both
15
+ // synchronous) build the texture with no boundary at all. See inline-image.tsx.
16
+ // createImage earns its async only when it fetches a string URL or swaps source
17
+ // reactively; decodeImage + createTexture are the primitives underneath.
18
+ //
19
+ // Raster vs vector: a texture has a fixed source resolution, so it softens when
20
+ // drawn larger than (displayed size x env.displayScale) on a hi-DPI display.
21
+ // Author raster at 2-3x the largest size you will draw it. When the render size
22
+ // is fluid or DPI varies, prefer a vector (svg.tsx / <d-path>) instead - it
23
+ // stays crisp at any size.
13
24
  import { render, createImage, Loading } from "@solidrt/core"
14
25
 
15
26
  function App() {
@@ -0,0 +1,27 @@
1
+ // Displaying an image whose bytes are already in memory - here a
2
+ // `with { type: "binary" }` import that inlines the file into the bundle. Because
3
+ // the bytes are on hand, the whole path is synchronous: decodeImage(bytes) turns
4
+ // the encoded PNG into raw RGBA8 pixels, and createTexture uploads them to the
5
+ // GPU and returns an id. No fetch, no async value, so no <Loading> boundary - the
6
+ // texture is ready in the same tick the component builds.
7
+ //
8
+ // Contrast image.tsx, which uses createImage for a string URL: that fetches at
9
+ // runtime, so it suspends and must be read inside <Loading>. The rule: createImage
10
+ // (async) for URLs or reactive sources; decodeImage + createTexture (sync) for
11
+ // bytes you already hold. Called here in the component body, the texture is freed
12
+ // automatically when the owner is disposed.
13
+ import { render, decodeImage, createTexture } from "@solidrt/core"
14
+ import bytes from "./logo.png" with { type: "binary" }
15
+
16
+ function App() {
17
+ let { data, width, height } = decodeImage(bytes)
18
+ let id = createTexture(data, width, height)
19
+
20
+ return (
21
+ <window alignItems="center" justifyContent="center">
22
+ <texture src={id} />
23
+ </window>
24
+ )
25
+ }
26
+
27
+ render(() => <App />)
@@ -0,0 +1,56 @@
1
+ // repaintBoundary marks a <view> subtree as its own retained cache, so nearby
2
+ // frequently changing content does not force it to rebuild every frame. It has
3
+ // two modes - both shown here, next to a square that spins every frame:
4
+ //
5
+ // - repaintBoundary={true} retains the recorded DRAW LIST: the subtree is
6
+ // recorded once and the commands are replayed until something inside changes.
7
+ // Skips re-recording. The default choice for static content next to animation.
8
+ //
9
+ // - repaintBoundary="snapshot" additionally retains the RASTERIZED PIXELS as a
10
+ // GPU texture, so replay skips rasterization too. Worth the texture memory
11
+ // only for raster-expensive static content (many glyphs, blurs, dense vector).
12
+ // It re-rasters on layout-size / display-scale changes, crops anything painted
13
+ // outside the layout box, and an ancestor scale animation smears the bitmap -
14
+ // so reach for it only for screen-aligned, static, raster-heavy subtrees.
15
+ //
16
+ // Rule of thumb: start with {true}; upgrade to "snapshot" only when the cached
17
+ // content is expensive to rasterize and stays screen-aligned and static.
18
+ import { render, onFrame, createSignal, For } from "@solidrt/core"
19
+
20
+ // Raster-expensive static content: a dense grid of rects. Cheap to replay from a
21
+ // draw list, but re-rasterizing it every frame would be wasteful - the case
22
+ // "snapshot" is built for.
23
+ function Grid(props: { boundary: true | "snapshot" }) {
24
+ let cells = Array.from({ length: 64 }, (_, i) => i)
25
+ return (
26
+ <view
27
+ repaintBoundary={props.boundary}
28
+ width={200}
29
+ height={200}
30
+ flexDirection="row"
31
+ flexWrap="wrap"
32
+ gap={3}
33
+ >
34
+ <For each={cells}>
35
+ {(i) => <rect width={22} height={22} radius={3} color={i % 2 ? "#2a3f5f" : "#3366b3"} />}
36
+ </For>
37
+ </view>
38
+ )
39
+ }
40
+
41
+ function App() {
42
+ let [angle, setAngle] = createSignal(0)
43
+ onFrame((tick) => setAngle(tick / 1000))
44
+
45
+ return (
46
+ <window flexDirection="row" alignItems="center" justifyContent="center" gap={32}>
47
+ <Grid boundary={true} />
48
+ <Grid boundary="snapshot" />
49
+ <view width={120} height={120} rotate={angle()}>
50
+ <rect width={120} height={120} radius={16} color="#e0245e" />
51
+ </view>
52
+ </window>
53
+ )
54
+ }
55
+
56
+ render(() => <App />)
@@ -0,0 +1,42 @@
1
+ // Responsive layout: the SAME app on a phone, tablet, or desktop. The window is
2
+ // host-sized and resizable, so drive the layout from the live window instead of
3
+ // hardcoding pixels. Here the column count comes from
4
+ // `capabilities.windowSizeClass` (Material 3 width breakpoints: compact <600,
5
+ // medium 600-840, expanded >=840). Resize the window and the grid reflows.
6
+ //
7
+ // `capabilities` and `env` are plain objects with reactive GETTERS, not
8
+ // functions - read `capabilities.windowSizeClass` (no call). Reading it inside
9
+ // JSX tracks, so the memo below re-runs on every resize. `windowSize()` IS a
10
+ // function (call it) - we read its width to size each card exactly.
11
+ import { render, capabilities, windowSize, createMemo, For } from "@solidrt/core"
12
+
13
+ const GAP = 16
14
+ const PAD = 24
15
+ const COLORS = ["#1f6feb", "#3fb950", "#db6d28", "#a371f7", "#e3b341", "#f778ba", "#2dd4bf", "#f85149"]
16
+
17
+ function App() {
18
+ // Column count from the size class; card width derived so N fit per row.
19
+ let cols = createMemo(() => (capabilities.windowSizeClass === "expanded" ? 3 : capabilities.windowSizeClass === "medium" ? 2 : 1))
20
+ let cardWidth = createMemo(() => (windowSize().width - PAD * 2 - GAP * (cols() - 1)) / cols())
21
+
22
+ return (
23
+ <window>
24
+ <d-rect color="#0b0f17" />
25
+ <view flex={1} flexDirection="column" gap={GAP} padding={PAD}>
26
+ <text color="#e6e6e6" fontSize={18}>{capabilities.windowSizeClass} - {cols()} column{cols() === 1 ? "" : "s"}</text>
27
+ <view flexDirection="row" flexWrap="wrap" gap={GAP}>
28
+ <For each={COLORS}>
29
+ {(c) => (
30
+ <view width={cardWidth()} height={96} alignItems="center" justifyContent="center">
31
+ <d-rect color={c} radius={12} />
32
+ <text color="#0b0f17" fontSize={16} fontWeight={700}>card</text>
33
+ </view>
34
+ )}
35
+ </For>
36
+ </view>
37
+ </view>
38
+ </window>
39
+ )
40
+ }
41
+
42
+ render(() => <App />)
@@ -0,0 +1,56 @@
1
+ // createScroll is a HEADLESS scroll primitive: it owns only the geometry - the
2
+ // clamped offset, re-clamped in onLayout against the measured content-vs-viewport
3
+ // overflow - and nothing with a UI opinion. Input handling, momentum, and
4
+ // scrollbars are policy you supply. (@solidrt/components ScrollView is one such
5
+ // skin built on top of this.)
6
+ //
7
+ // The shape it expects:
8
+ // - a VIEWPORT node: the clipping box, overflow="hidden".
9
+ // - a CONTENT node inside it: an inner wrapper that takes the children's natural
10
+ // size (flexShrink={0} so it can exceed the viewport).
11
+ // Capture both with refs, pass their accessors to createScroll, then apply the
12
+ // returned offset to the viewport's scrollX/scrollY. createScroll does no input,
13
+ // so wire an event (here onWheel) to scroll.scrollBy - positive dy moves content
14
+ // up. scrollTo(x, y) jumps to an absolute, clamped offset.
15
+ import { render, For, createScroll } from "@solidrt/core"
16
+ import type { WheelEvent } from "@solidrt/core"
17
+
18
+ function App() {
19
+ let viewport: { id: number } | undefined
20
+ let content: { id: number } | undefined
21
+
22
+ // Default axis is "vertical"; pass { axis: "horizontal" } or "both" for others.
23
+ let scroll = createScroll(() => viewport, () => content)
24
+
25
+ let onWheel = (e: WheelEvent) => scroll.scrollBy(e.deltaX, e.deltaY)
26
+
27
+ let rows = Array.from({ length: 30 }, (_, i) => i)
28
+
29
+ return (
30
+ <window alignItems="center" justifyContent="center">
31
+ <view
32
+ ref={(n: { id: number }) => (viewport = n)}
33
+ width={220}
34
+ height={280}
35
+ overflow="hidden"
36
+ clipRadius={12}
37
+ scrollY={scroll.offset().y}
38
+ onWheel={onWheel}
39
+ >
40
+ <d-rect color="#1a2233" radius={12} />
41
+ <view ref={(n: { id: number }) => (content = n)} flexShrink={0} padding={12} gap={8}>
42
+ <For each={rows}>
43
+ {(i) => (
44
+ <view height={40} justifyContent="center" paddingLeft={12}>
45
+ <rect height={40} radius={6} color={i % 2 ? "#2a3f5f" : "#3366b3"} />
46
+ <text color="#ffffff">Row {i}</text>
47
+ </view>
48
+ )}
49
+ </For>
50
+ </view>
51
+ </view>
52
+ </window>
53
+ )
54
+ }
55
+
56
+ render(() => <App />)
package/examples/svg.tsx CHANGED
@@ -10,6 +10,10 @@
10
10
  // "currentColor" is recolored by the host `color` prop. For per-shape authored
11
11
  // or animated vector art, compose <d-path> instead of this document layer.
12
12
  //
13
+ // Being a vector, an <svg> is resolution-independent: it stays crisp at any
14
+ // drawn size x displayScale(). Prefer it over a raster <texture> (image.tsx)
15
+ // whenever the render size is fluid or the display DPI varies.
16
+ //
13
17
  // This is how you use an existing icon library (Lucide, Heroicons, Feather,
14
18
  // Material, etc.): those ship SVG source, so import/inline the icon string and
15
19
  // hand it to `src`. The `currentColor` convention they follow means the `color`
package/jsx-runtime.d.ts CHANGED
@@ -23,26 +23,30 @@ export namespace JSX {
23
23
  type RefCallback<T> = (el: T) => unknown
24
24
  type Ref<T> = T | RefCallback<T>
25
25
 
26
- interface IntrinsicAttributes {
26
+ // ref lives on the element prop types (intersected into every entry below)
27
+ // rather than in IntrinsicAttributes: under our config, declaring it only in
28
+ // IntrinsicAttributes did not make it reach intrinsic elements, so `ref` on a
29
+ // host element was reported as an excess property.
30
+ interface ElementRef {
27
31
  ref?: Ref<{ id: number }> | undefined
28
32
  }
29
33
 
30
34
  interface IntrinsicElements {
31
- window: WindowProps
32
- view: ViewProps
33
- text: TextProps & LayoutProps
34
- rect: RectProps & LayoutProps
35
- oval: OvalProps & LayoutProps
36
- path: PathProps & LayoutProps
37
- svg: SvgProps & LayoutProps
38
- texture: TextureProps & LayoutProps
39
- audio: AudioProps
40
- "d-view": ViewProps
41
- "d-rect": RectProps
42
- "d-oval": OvalProps
43
- "d-path": PathProps
44
- "d-svg": SvgProps
45
- "d-texture": TextureProps
46
- "d-text": TextProps
35
+ window: WindowProps & ElementRef
36
+ view: ViewProps & ElementRef
37
+ text: TextProps & LayoutProps & ElementRef
38
+ rect: RectProps & LayoutProps & ElementRef
39
+ oval: OvalProps & LayoutProps & ElementRef
40
+ path: PathProps & LayoutProps & ElementRef
41
+ svg: SvgProps & LayoutProps & ElementRef
42
+ texture: TextureProps & LayoutProps & ElementRef
43
+ audio: AudioProps & ElementRef
44
+ "d-view": ViewProps & ElementRef
45
+ "d-rect": RectProps & ElementRef
46
+ "d-oval": OvalProps & ElementRef
47
+ "d-path": PathProps & ElementRef
48
+ "d-svg": SvgProps & ElementRef
49
+ "d-texture": TextureProps & ElementRef
50
+ "d-text": TextProps & ElementRef
47
51
  }
48
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -11,7 +11,6 @@
11
11
  "./gpu": "./src/gpu.ts",
12
12
  "./image": "./src/image.ts",
13
13
  "./microphone": "./src/microphone.ts",
14
- "./scroll": "./src/scroll.ts",
15
14
  "./speech-recognition": "./src/speech-recognition.ts",
16
15
  "./text-input": "./src/text-input.ts",
17
16
  "./jsx-runtime": "./jsx-runtime.d.ts",
@@ -27,7 +26,7 @@
27
26
  "colord": "^2.9.3"
28
27
  },
29
28
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.22"
29
+ "@solidrt/flux-types": "0.0.23"
31
30
  },
32
31
  "peerDependencies": {
33
32
  "@solidjs/signals": "2.0.0-beta.15",
package/src/gpu.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  // GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
2
- // their texture automatically when the reactive owner is disposed. The imperative
3
- // primitive lives in the `flux:gpu` module; import { uploadTexture,
4
- // setShaderParams, destroyTexture, ... } from "flux:gpu" for non-reactive use.
2
+ // their texture automatically when the reactive owner is disposed. Drive a
3
+ // shader's uniforms declaratively with `<texture src={id} params={{...}} />`
4
+ // (see TextureProps) - the preferred way, deferred to the next real repaint so
5
+ // a fast-changing signal stays paced to actual frames. setShaderParams is the
6
+ // imperative exception: reach for it only when there is no `<texture>` element
7
+ // to hold a params prop, e.g. a shader that only feeds another shader as a
8
+ // sampler2D input. The imperative primitives (uploadTexture, setShaderParams,
9
+ // destroyTexture, ...) live in the `flux:gpu` module.
5
10
 
6
11
  import { getOwner, onCleanup } from "@solidjs/signals"
7
12
  import * as gpu from "flux:gpu"
8
13
 
9
- // Imperative companions to the reactive create* helpers, re-exported so callers
10
- // that depend on @solidrt/core -- like @solidrt/components -- need not import flux
11
- // directly: destroyTexture for the manual-cleanup path (textures made outside a
12
- // reactive scope, e.g. after an await, are not auto-freed), setShaderParams to
13
- // drive a shader's uniforms over time, and uploadTexture to push new pixels into
14
- // a mutable texture.
14
+ // Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
15
+ // -- need not import flux directly: destroyTexture for the manual-cleanup path
16
+ // (textures made outside a reactive scope, e.g. after an await, are not
17
+ // auto-freed), uploadTexture to push new pixels into a mutable texture, and
18
+ // setShaderParams as the non-reactive exception described above - prefer
19
+ // `<texture params={...}>` when a `<texture>` element is already in the tree.
15
20
  export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
16
21
 
17
22
  /**
@@ -48,12 +53,14 @@ export function createMutableTexture(data: Uint8Array, width: number, height: nu
48
53
  * Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
49
54
  * returning the texture id (usable anywhere a normal texture id is, e.g.
50
55
  * `<texture src>`). The fragment body may reference `vUV` (0..1, top-left
51
- * origin), `iResolution`, `iTime`, and any `uniform float` it declares; pass
52
- * their values via `params`. `textures` binds each declared `uniform sampler2D`
53
- * to an existing texture id (e.g. a camera or decoded image) so the shader can
54
- * read it; those inputs are re-sampled on every `setShaderParams` call, so live
55
- * sources stay current. Frees the texture and shader program when the reactive
56
- * owner is disposed; create outside any reactive scope for app-lifetime shaders.
56
+ * origin), `iResolution`, `iTime`, and any `uniform float` it declares; drive
57
+ * their values with `<texture src={id} params={{...}} />` (preferred) or, when
58
+ * there is no `<texture>` element for it, imperatively with `setShaderParams`.
59
+ * `textures` binds each declared `uniform sampler2D` to an existing texture id
60
+ * (e.g. a camera or decoded image) so the shader can read it; those inputs are
61
+ * re-sampled on every params update, so live sources stay current. Frees the
62
+ * texture and shader program when the reactive owner is disposed; create
63
+ * outside any reactive scope for app-lifetime shaders.
57
64
  */
58
65
  export function createShader(
59
66
  fragmentSrc: string,
package/src/image.ts CHANGED
@@ -34,6 +34,12 @@ export type ImageSource = string | Uint8Array
34
34
  * current texture is freed when the owner is disposed. Display it with
35
35
  * `<texture src={id()} />`; the texture carries its own pixel size, so no
36
36
  * width/height is needed unless you want to scale it.
37
+ *
38
+ * For bytes you already hold (a `with { type: "binary" }` import, or anything in
39
+ * memory) this suspends needlessly: `decodeImage` + `createTexture` are both
40
+ * synchronous, so reach for them directly and skip the `<Loading>` boundary.
41
+ * `createImage` earns its async only for a fetched string URL or a reactive
42
+ * source.
37
43
  */
38
44
  export function createImage(src: ImageSource | (() => ImageSource)): () => number {
39
45
  let getSrc = typeof src === "function" ? src : () => src
package/src/index.ts CHANGED
@@ -13,6 +13,8 @@ export type { Capabilities, WindowSizeClass } from "./capabilities"
13
13
  export { createTexture } from "./gpu"
14
14
  export { createImage, decodeImage } from "./image"
15
15
  export type { DecodedImage, ImageSource } from "./image"
16
+ export { createScroll } from "./scroll"
17
+ export type { Scroll, ScrollAxis, ScrollOffset, ScrollOptions } from "./scroll"
16
18
  export type {
17
19
  LayoutProps,
18
20
  TransformProps,
package/src/types.d.ts CHANGED
@@ -37,10 +37,17 @@ export interface ElementChildrenAttribute {
37
37
 
38
38
  type Children = Element
39
39
 
40
+ // Doc-comment policy for this file: JSX props mostly mirror CSS/DOM, which any
41
+ // developer or agent already knows, so a comment restating standard semantics
42
+ // is noise. Add one only where a prop deviates from that standard, is bespoke
43
+ // to this engine, or has an interaction that isn't decidable from the type
44
+ // alone (e.g. shorthand-vs-longhand precedence, a subsetted value range).
45
+
40
46
  interface FlexboxProps {
41
47
  gap?: number
42
48
  rowGap?: number
43
49
  columnGap?: number
50
+ /** Shorthand; overridden by flexGrow/flexShrink/flexBasis when they're also set. */
44
51
  flex?: number | "none" | "auto" | (string & {})
45
52
  flexGrow?: number
46
53
  flexShrink?: number
@@ -54,6 +61,7 @@ interface FlexboxProps {
54
61
  justifyContent?: "start" | "end" | "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-evenly" | "space-around"
55
62
  }
56
63
 
64
+ /** CSS grid subset: line-based placement only, no named lines, no grid-template-areas, and auto tracks take a fixed size (no minmax/fr/keyword). */
57
65
  interface GridProps {
58
66
  gridAutoFlow?: "row" | "column" | "row-dense" | "column-dense"
59
67
  gridAutoColumns?: number
@@ -66,10 +74,17 @@ interface GridProps {
66
74
  gridTemplateRows?: string
67
75
  }
68
76
 
77
+ /** A layout length: a bare number is pixels, plus "auto" and percent strings. */
69
78
  type Dimension = number | "auto" | `${number}%`
70
79
 
71
80
  export interface LayoutProps extends FlexboxProps, GridProps {
72
81
  display?: "block" | "flex" | "grid" | "none"
82
+ /**
83
+ * No "fixed" or "sticky". Unlike CSS, `absolute` does not itself become a
84
+ * containing block: an absolute element resolves against the nearest
85
+ * ancestor with `position: relative`, so a chain of absolute elements
86
+ * resolves against whatever relative element is above all of them.
87
+ */
73
88
  position?: "relative" | "absolute"
74
89
 
75
90
  top?: Dimension
@@ -201,8 +216,6 @@ export interface WindowProps extends LayoutProps {
201
216
  children?: Children
202
217
  title?: string
203
218
  fullscreen?: boolean
204
- vsync?: boolean
205
- fps?: boolean
206
219
  }
207
220
 
208
221
  export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
@@ -229,6 +242,11 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
229
242
  repaintBoundary?: boolean | "snapshot"
230
243
  }
231
244
 
245
+ /**
246
+ * Not implemented: there is no native `audio` element kind yet, so using
247
+ * `<audio>` panics ("unknown node kind: audio"). Typed ahead of the backing
248
+ * work; treat this interface as a plan, not a working API.
249
+ */
232
250
  export interface AudioProps {
233
251
  src?: Uint8Array
234
252
  play?: number
@@ -245,7 +263,9 @@ export interface RectProps extends Position, PaintProps, PointerProps {
245
263
  }
246
264
 
247
265
  export interface OvalProps extends Position, PaintProps, PointerProps {
266
+ /** Bounding box width of the ellipse (not a radius); defaults to the layout box. */
248
267
  w?: number
268
+ /** Bounding box height of the ellipse (not a radius); defaults to the layout box. */
249
269
  h?: number
250
270
  }
251
271
 
@@ -254,7 +274,9 @@ export interface LineProps extends PaintProps, PointerProps {
254
274
  y1?: number
255
275
  x2?: number
256
276
  y2?: number
277
+ /** Dash pattern in local units: the drawn segment length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
257
278
  onLength?: number
279
+ /** Dash pattern in local units: the gap length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
258
280
  offLength?: number
259
281
  }
260
282
 
@@ -289,11 +311,14 @@ export interface TextProps extends Position, PaintProps, PointerProps {
289
311
 
290
312
  export interface TextureProps extends Position {
291
313
  src?: number
292
- imageWidth?: number
293
- imageHeight?: number
314
+ w?: number
315
+ h?: number
294
316
  srcX?: number
295
317
  srcY?: number
296
318
  srcW?: number
297
319
  srcH?: number
320
+ // Shader uniform values, when src names a shader texture. Applied at the
321
+ // next repaint (not synchronously), so a fast-changing signal stays paced
322
+ // to real frames rather than triggering a GL render pass per write.
298
323
  params?: Record<string, number>
299
324
  }