@solidrt/core 0.0.23 → 0.0.25

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/jsx-runtime.d.ts CHANGED
@@ -7,7 +7,6 @@ import type {
7
7
  ViewProps,
8
8
  TextProps,
9
9
  TextureProps,
10
- AudioProps,
11
10
  LayoutProps,
12
11
  Element as CoreElement,
13
12
  ElementChildrenAttribute as CoreElementChildrenAttribute
@@ -40,7 +39,6 @@ export namespace JSX {
40
39
  path: PathProps & LayoutProps & ElementRef
41
40
  svg: SvgProps & LayoutProps & ElementRef
42
41
  texture: TextureProps & LayoutProps & ElementRef
43
- audio: AudioProps & ElementRef
44
42
  "d-view": ViewProps & ElementRef
45
43
  "d-rect": RectProps & ElementRef
46
44
  "d-oval": OvalProps & ElementRef
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
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
+ "./sound": "./src/sound.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",
@@ -26,7 +27,7 @@
26
27
  "colord": "^2.9.3"
27
28
  },
28
29
  "devDependencies": {
29
- "@solidrt/flux-types": "0.0.23"
30
+ "@solidrt/flux-types": "0.0.25"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@solidjs/signals": "2.0.0-beta.15",
package/src/color.ts CHANGED
@@ -43,6 +43,11 @@ type Stop = { offset: number; color: number }
43
43
  // element's box), so one gradient can be reused on elements of any size. Branded
44
44
  // so the renderer can tell it from a solid color string. The object crosses to
45
45
  // the runtime as-is and is decoded by key (see properties/paint.rs).
46
+ //
47
+ // These 0..1 coords are deliberately their own normalized space, NOT the pixel/
48
+ // `pct()` length vocabulary used by layout and transformOrigin: a gradient's
49
+ // position is naturally a fraction (like a stop offset), so 0..1 reads cleaner
50
+ // than pct(0)..pct(100). Do not "unify" them onto pct().
46
51
  export type Gradient =
47
52
  | { readonly __gradient: "linear"; x0: number; y0: number; x1: number; y1: number; stops: Stop[] }
48
53
  | { readonly __gradient: "radial"; cx: number; cy: number; r: number; circle: boolean; stops: Stop[] }
@@ -10,6 +10,14 @@ import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } fro
10
10
  // been seen this session. Seen-flags only ever go from false to true, so a
11
11
  // capability derived from them can appear mid-session (e.g. the first mouse
12
12
  // move) but never flickers away.
13
+ //
14
+ // `ownedWrite: true` on the signals below: each is lazily created the first
15
+ // time its ensure* function runs, which can happen inside a tracked scope
16
+ // (e.g. a memo's first read of env.inputDevices). Sticky events replay their
17
+ // cached value synchronously on subscribe (srt:events' on()), so that same
18
+ // call can immediately write the signal it just created. That's a legitimate
19
+ // internal-state write, not a stray write escaping a computation, so it opts
20
+ // out of the write-in-owned-scope guard.
13
21
 
14
22
  /** Connected input device classes, as reported by the runtime. */
15
23
  export interface InputDevices {
@@ -26,7 +34,7 @@ let devicesAccessor: (() => InputDevices | undefined) | undefined
26
34
 
27
35
  function ensureDevicesState() {
28
36
  if (devicesAccessor) return
29
- let [devices, setDevices] = createSignal<InputDevices | undefined>(undefined)
37
+ let [devices, setDevices] = createSignal<InputDevices | undefined>(undefined, { ownedWrite: true })
30
38
  // Sticky: the current state replays on subscribe, so the first read already
31
39
  // sees it on runtimes that report devices.
32
40
  on("inputDevices", (d: InputDevices) => {
@@ -39,7 +47,7 @@ let systemThemeAccessor: (() => SystemTheme) | undefined
39
47
 
40
48
  function ensureSystemThemeState() {
41
49
  if (systemThemeAccessor) return
42
- let [theme, setTheme] = createSignal<SystemTheme>("unknown")
50
+ let [theme, setTheme] = createSignal<SystemTheme>("unknown", { ownedWrite: true })
43
51
  on("systemTheme", (e: { theme?: SystemTheme }) => setTheme(e.theme ?? "unknown"))
44
52
  systemThemeAccessor = theme
45
53
  }
@@ -48,7 +56,7 @@ let orientationAccessor: (() => Orientation) | undefined
48
56
 
49
57
  function ensureOrientationState() {
50
58
  if (orientationAccessor) return
51
- let [orientation, setOrientation] = createSignal<Orientation>("unknown")
59
+ let [orientation, setOrientation] = createSignal<Orientation>("unknown", { ownedWrite: true })
52
60
  on("displayOrientation", (e: { orientation?: Orientation }) => {
53
61
  setOrientation(e.orientation ?? "unknown")
54
62
  })
@@ -59,7 +67,7 @@ let textScaleAccessor: (() => number) | undefined
59
67
 
60
68
  function ensureTextScaleState() {
61
69
  if (textScaleAccessor) return
62
- let [scale, setScale] = createSignal(1)
70
+ let [scale, setScale] = createSignal(1, { ownedWrite: true })
63
71
  // Sticky, like systemTheme. Guard nonsense values: a runtime bug reporting
64
72
  // 0 or a negative would otherwise collapse all text.
65
73
  on("textScale", (e: { scale?: number }) => {
package/src/gpu.ts CHANGED
@@ -19,6 +19,13 @@ import * as gpu from "flux:gpu"
19
19
  // `<texture params={...}>` when a `<texture>` element is already in the tree.
20
20
  export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
21
21
 
22
+ // captureSnapshot renders a node to a texture and readTexture reads any
23
+ // texture's bytes back. Re-exported raw (no reactive auto-cleanup wrapper):
24
+ // captureSnapshot resolves asynchronously, by which point the reactive owner is
25
+ // no longer current, so the caller owns the returned id and frees it with
26
+ // destroyTexture (as with any texture created after an await).
27
+ export { captureSnapshot, readTexture } from "flux:gpu"
28
+
22
29
  /**
23
30
  * Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
24
31
  * it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
package/src/index.ts CHANGED
@@ -32,11 +32,16 @@ export type {
32
32
  PathProps,
33
33
  TextProps,
34
34
  TextureProps,
35
- AudioProps,
36
35
  Color,
36
+ Pct,
37
37
  } from "./types"
38
38
  export type { MeasureTextOptions } from "flux:rendertree"
39
39
 
40
+ // A percentage value for dimensional props (e.g. transformOrigin): `pct(50)` is
41
+ // half the element box. Keeps percentages a first-class branded value rather
42
+ // than a string that has to be parsed - a bare number stays pixels.
43
+ export let pct = (v: number): import("./types").Pct => ({ __unit: "pct", v })
44
+
40
45
  // --- Authoring-surface re-exports -------------------------------------------
41
46
  // A SolidRT app is built from three substrate packages: @solidjs/signals
42
47
  // (reactivity), solid-js (control-flow components), and @solidjs/universal (the
package/src/sound.ts ADDED
@@ -0,0 +1,119 @@
1
+ // Sound playback, reactive (SolidJS) layer. `createSound` decodes an encoded
2
+ // clip (Ogg/Vorbis or WAV) once and owns its lifecycle: the decoded clip is
3
+ // released, and any playing voices stopped, when the reactive owner is disposed.
4
+ // Each play() is cheap (no re-decode). `createSoundStream` is the same but reads
5
+ // a large track from a file path on demand instead of decoding it into memory.
6
+ //
7
+ // The imperative primitive lives in the `flux:audio` module; import
8
+ // { play, load, stream } from "flux:audio" for non-reactive use.
9
+
10
+ import { createSignal, onCleanup } from "@solidjs/signals"
11
+ import { load, stream } from "flux:audio"
12
+ import { file } from "flux:fs"
13
+
14
+ type FluxFile = ReturnType<typeof file>
15
+
16
+ type LoadedSound = ReturnType<typeof load>
17
+
18
+ export type SoundOptions = {
19
+ /** Repeat the clip until stopped. Defaults to false. */
20
+ loop?: boolean
21
+ /** Volume scale, 1.0 leaves the clip unchanged. Defaults to 1.0. */
22
+ gain?: number
23
+ /**
24
+ * Let play() stack overlapping voices instead of restarting. Defaults to
25
+ * true: rapid triggers overlap. Set false for a single-voice sound where each
26
+ * play() cuts off the previous one.
27
+ */
28
+ overlap?: boolean
29
+ }
30
+
31
+ /** Options for a streamed sound. Streams are always single-voice. */
32
+ export type SoundStreamOptions = {
33
+ /** Repeat the track until stopped. Defaults to false. */
34
+ loop?: boolean
35
+ /** Volume scale, 1.0 leaves the track unchanged. Defaults to 1.0. */
36
+ gain?: number
37
+ }
38
+
39
+ /** A decoded sound with reactive lifecycle. */
40
+ export type Sound = {
41
+ /** Start the clip. Overlaps or restarts per the `overlap` option. */
42
+ play(): void
43
+ /** Stop every voice started from this sound. */
44
+ stop(): void
45
+ /** True after play() until stop() (does not track natural completion). */
46
+ playing(): boolean
47
+ /** Set if loading failed. */
48
+ error(): Error | undefined
49
+ }
50
+
51
+ // Shared reactive wrapper: owns the loaded handle, tracks live voices, and
52
+ // disposes both on cleanup. `loader` runs once (may throw -> error signal).
53
+ function reactiveSound(
54
+ loader: () => LoadedSound,
55
+ overlap: boolean,
56
+ playOptions: { loop?: boolean; gain?: number },
57
+ ): Sound {
58
+ let [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true })
59
+ let [playing, setPlaying] = createSignal(false, { ownedWrite: true })
60
+
61
+ let handle: LoadedSound | undefined
62
+ let voices: { stop(): void }[] = []
63
+ try {
64
+ handle = loader()
65
+ } catch (e) {
66
+ setError(e instanceof Error ? e : new Error(String(e)))
67
+ }
68
+
69
+ let stopAll = () => {
70
+ for (let v of voices) v.stop()
71
+ voices = []
72
+ setPlaying(false)
73
+ }
74
+
75
+ onCleanup(() => {
76
+ stopAll()
77
+ if (handle) {
78
+ handle.unload()
79
+ handle = undefined
80
+ }
81
+ })
82
+
83
+ return {
84
+ play() {
85
+ if (!handle) return
86
+ if (!overlap) stopAll()
87
+ voices.push(handle.play(playOptions))
88
+ setPlaying(true)
89
+ },
90
+ stop: stopAll,
91
+ playing,
92
+ error,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Decodes a sound once and owns its lifecycle: releases the clip and stops its
98
+ * voices when the reactive owner is disposed. play() replays without decoding.
99
+ * For imperative use, call load()/play() from "flux:audio".
100
+ */
101
+ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sound {
102
+ return reactiveSound(() => load(source), options.overlap ?? true, {
103
+ loop: options.loop,
104
+ gain: options.gain,
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Streams a large track, decoding on demand instead of loading it into memory.
110
+ * Single-voice: each play() restarts it. Pass a path (resolved like flux:fs,
111
+ * relative to the process cwd) or a `file()` from flux:fs; a path is wrapped in
112
+ * `file()` for you, so a dev-server-proxied file streams over the proxy. Owns
113
+ * the stream's lifecycle: stopped and released when the reactive owner is
114
+ * disposed. For imperative use, call stream()/play() from "flux:audio".
115
+ */
116
+ export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
117
+ let src = typeof source === "string" ? file(source) : source
118
+ return reactiveSound(() => stream(src), false, { loop: options.loop, gain: options.gain })
119
+ }
package/src/types.d.ts CHANGED
@@ -44,9 +44,9 @@ type Children = Element
44
44
  // alone (e.g. shorthand-vs-longhand precedence, a subsetted value range).
45
45
 
46
46
  interface FlexboxProps {
47
- gap?: number
48
- rowGap?: number
49
- columnGap?: number
47
+ gap?: LengthPercentage
48
+ rowGap?: LengthPercentage
49
+ columnGap?: LengthPercentage
50
50
  /** Shorthand; overridden by flexGrow/flexShrink/flexBasis when they're also set. */
51
51
  flex?: number | "none" | "auto" | (string & {})
52
52
  flexGrow?: number
@@ -74,8 +74,14 @@ interface GridProps {
74
74
  gridTemplateRows?: string
75
75
  }
76
76
 
77
- /** A layout length: a bare number is pixels, plus "auto" and percent strings. */
78
- type Dimension = number | "auto" | `${number}%`
77
+ /**
78
+ * A layout length: a bare number is pixels, `pct(n)` is a percentage of the
79
+ * containing block, plus "auto" and the `"50%"` string form (kept for paste).
80
+ */
81
+ type Dimension = number | Pct | "auto" | `${number}%`
82
+
83
+ /** Like {@link Dimension} without "auto" (e.g. gap, which has no auto value). */
84
+ type LengthPercentage = number | Pct | `${number}%`
79
85
 
80
86
  export interface LayoutProps extends FlexboxProps, GridProps {
81
87
  display?: "block" | "flex" | "grid" | "none"
@@ -131,6 +137,16 @@ export interface PaintProps {
131
137
  strokeWidth?: number
132
138
  }
133
139
 
140
+ /** A percentage value, from `pct(50)`. Resolves against the element box. */
141
+ export type Pct = { readonly __unit: "pct"; v: number }
142
+
143
+ // One axis of the transform origin (the point rotate/scale/3D pivot around),
144
+ // split per axis to match the engine's x/y prop convention. A bare number is
145
+ // pixels; `pct(50)` is a fraction of the box, so a percentage origin tracks the
146
+ // layout size with no reactive wiring. Unset defaults to the axis center.
147
+ type OriginX = number | Pct | "left" | "center" | "right"
148
+ type OriginY = number | Pct | "top" | "center" | "bottom"
149
+
134
150
  export interface TransformProps {
135
151
  rotate?: number
136
152
  scale?: number
@@ -149,8 +165,8 @@ export interface TransformProps {
149
165
  perspective?: number
150
166
  x?: number
151
167
  y?: number
152
- cx?: number
153
- cy?: number
168
+ originX?: OriginX
169
+ originY?: OriginY
154
170
  // Group opacity in 0..1: children are composited together, then faded as a
155
171
  // whole (CSS `opacity`). Does not affect hit testing.
156
172
  opacity?: number
@@ -212,7 +228,7 @@ interface Position {
212
228
 
213
229
  // Primitives
214
230
 
215
- export interface WindowProps extends LayoutProps {
231
+ export interface WindowProps extends LayoutProps, PointerProps {
216
232
  children?: Children
217
233
  title?: string
218
234
  fullscreen?: boolean
@@ -242,16 +258,6 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
242
258
  repaintBoundary?: boolean | "snapshot"
243
259
  }
244
260
 
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
- */
250
- export interface AudioProps {
251
- src?: Uint8Array
252
- play?: number
253
- }
254
-
255
261
  // draw primitives
256
262
 
257
263
  export interface RectProps extends Position, PaintProps, PointerProps {
@@ -309,7 +315,7 @@ export interface TextProps extends Position, PaintProps, PointerProps {
309
315
  maxLines?: number
310
316
  }
311
317
 
312
- export interface TextureProps extends Position {
318
+ export interface TextureProps extends Position, PointerProps {
313
319
  src?: number
314
320
  w?: number
315
321
  h?: number
package/src/window.ts CHANGED
@@ -93,7 +93,9 @@ export function onResize(fn: (data: ResizeEvent) => void) {
93
93
  // Singleton accessors over the same events as onResize / onWindowFocus. There
94
94
  // is one window, so these are bare accessors rather than a createX instance.
95
95
  // Lazily subscribed on first read (resize is sticky, so the first read sees the
96
- // current value); app-lifetime, so no onCleanup.
96
+ // current value); app-lifetime, so no onCleanup. `ownedWrite: true` on the
97
+ // signals below is the sticky-replay-into-a-tracked-scope case explained on
98
+ // the ensure* functions in environment.ts.
97
99
 
98
100
  let sizeAccessor: (() => { width: number; height: number }) | undefined
99
101
  let safeAreaAccessor: (() => SafeArea) | undefined
@@ -101,9 +103,9 @@ let displayScaleAccessor: (() => number) | undefined
101
103
 
102
104
  function ensureResizeState() {
103
105
  if (sizeAccessor) return
104
- let [size, setSize] = createSignal({ width: 0, height: 0 })
105
- let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 })
106
- let [scale, setScale] = createSignal(1)
106
+ let [size, setSize] = createSignal({ width: 0, height: 0 }, { ownedWrite: true })
107
+ let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 }, { ownedWrite: true })
108
+ let [scale, setScale] = createSignal(1, { ownedWrite: true })
107
109
  on("resize", (e: ResizeEvent) => {
108
110
  setSize({ width: e.width, height: e.height })
109
111
  setSafe(e.safeArea)