@solidrt/core 0.0.57 → 0.0.59

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
@@ -111,12 +111,18 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
111
111
  ```
112
112
 
113
113
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
114
- 2.0.0-rc.6); bun resolves them from peerDependencies. On a version bump,
114
+ 2.0.0-rc.8); bun resolves them from peerDependencies. On a version bump,
115
115
  `bun install` only warns ("incorrect peer dependency") and keeps the old
116
116
  ones, and `bun update solid-js @solidjs/signals @solidjs/universal` does
117
117
  not read the pin either: it adds all three to package.json at registry
118
- latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
119
- `rm -rf bun.lock node_modules/solid-js node_modules/@solidjs && bun install`.
118
+ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works
119
+ deletes the installed copies and leaves the lockfile alone:
120
+ `rm -rf node_modules/solid-js node_modules/@solidjs && bun install`
121
+ (delete any nested `node_modules/@solidjs` too, or the old copy keeps
122
+ resolving). Removing the directories is what forces re-resolution. Do not
123
+ delete `bun.lock` to force it: that re-resolves every unrelated dependency
124
+ as well, and a package that floats elsewhere fails the typecheck in a way
125
+ that reads exactly like Solid fallout.
120
126
 
121
127
  ## Element model (the parts that are easy to get wrong)
122
128
 
@@ -361,7 +367,17 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
361
367
  `<View2d pointer>` take a feed and spread it on their built-in leaf) and
362
368
  bind its gestures `drag` (one pointer), `pan` (two), `pinch`, `twist`,
363
369
  `wheel` and `mouseDelta` (raw motion while pointerLocked()). One
364
- merged recognizer serves them, so they arbitrate in the arena as one. A
370
+ merged recognizer serves them, so they arbitrate in the arena as one.
371
+ Each gesture takes a chord in the key-spec grammar: `pointer.drag("Ctrl")`
372
+ fires only when the event that opens the gesture carries Ctrl, and then
373
+ instead of the bare `drag` (the most specific bound chord wins, as a
374
+ key spec does), held for the whole gesture - so plain drag on `rotate`
375
+ and `drag("Ctrl")` on `pan` is one binding each, the every-viewport
376
+ default. A bracketed spec may end in its button (`drag("Right")`,
377
+ `drag("Shift+Middle")`; bare means Left), a discriminator rather than
378
+ a modifier: a right drag feeds only Right specs. Touch carries no
379
+ modifiers or buttons: a chord is the desktop path, the bare binding
380
+ stays for fingers. A
365
381
  detached d-* leaf has no layout box: give the feed `{ layout }` or it
366
382
  throws at the first press. `invert(source)` and `scale(source, k)` are
367
383
  the two processors: keys and sticks move the CAMERA where a drag moves
@@ -445,13 +461,32 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
445
461
  write targets, and the runtime animates natively with no per-frame JS.
446
462
  Enter and exit are per-property entries on that same spec: `from` is the
447
463
  value the property animates in from at first attach, `exit` the value it
448
- animates to on removal (the node stays painted until it settles, then
449
- frees - no AnimatePresence equivalent needed). `stagger` (ms) goes on an
464
+ animates to on removal. An exit reaches the whole unmounted subtree: every
465
+ `exit` declared under the node that unmounts plays, and the node stays
466
+ painted until the last of them settles, then frees - a panel's contents
467
+ leave with `exit` beside their `from`, no closing signal, held mount or
468
+ settle cue (no AnimatePresence equivalent needed). One rule in both
469
+ trees: a node animating out is a ghost, painted and nothing else -
470
+ hit-test invisible here, and in the spatial arena (a @solidrt/2d sprite,
471
+ a @solidrt/3d node, whose `destroy` verbs carry the same `exit`)
472
+ invisible to every pick, raycast, overlap and sweep - while an entering
473
+ node is live from its first frame, whatever `from` it passes through.
474
+ Either endpoint takes
475
+ `{ value, curve?, duration?, bounce?, delay? }` to own its direction's
476
+ motion (an ease-out enter, an ease-in exit). `stagger` (ms) goes on an
450
477
  ANCESTOR, never on the animating elements: it delays each descendant's
451
478
  enter/exit by `index * stagger`, and cascades nothing unless the
452
479
  descendants declare `from`/`exit`. An enter plays once per mount; to
453
480
  replay it, remount the subtree (`<Show when={epoch()} keyed>` around it,
454
- then bump `epoch`). See examples/stagger.tsx.
481
+ then bump `epoch`). See examples/stagger.tsx. The companion to the exit
482
+ pop-out is `layout` on the same spec: an element declaring it slides
483
+ from where it was painted to the box a layout gives it (a row removed
484
+ above it, one inserted, a `<For>` reorder) instead of jumping, retargets
485
+ if the layout changes again mid-slide, and is hit-tested where it is
486
+ drawn. It is parent-relative, so declare it on every level that should
487
+ glide; `layout: true` borrows the `all` motion, and `all` alone never
488
+ slides. Position only today, a size change snaps. See
489
+ examples/layout-slide.tsx.
455
490
  Per-frame work is for genuinely procedural motion, and a game or a
456
491
  simulation is that in full: its natural shape is ONE `onFrame` doing one
457
492
  sim step and N property (or sprite) writes, which is not a smell but the
package/docs/index.md CHANGED
@@ -146,6 +146,29 @@ let camera = createCamera()
146
146
  The same pattern covers `@solidrt/core/microphone`, `/sound`,
147
147
  `/speech-recognition`, `/text-input`, `/image`, `/color`, and `/gpu`.
148
148
 
149
+ ## Lifecycle
150
+
151
+ The runtime reports the platform's lifecycle facts and holds the platform
152
+ open while the app acts on them; what to do with them is the app's policy.
153
+ `onBack` is the user's back intent (prevent it for in-app navigation, or let
154
+ it fall through to the platform's default: `background()` on Android, where
155
+ the system keeps the app behind the others, `exit()` elsewhere). `onSuspend` fires when the app is being
156
+ suspended and may be killed without notice (switching away on Android or
157
+ iOS; never on desktop): persist session state there, async is fine, the
158
+ runtime waits for the returned promise up to its deadline. `onQuit` fires
159
+ when this app instance is ending (`exit()`, the window closing, the Android
160
+ activity finishing) and waits the same way, inside the platform's close
161
+ budget. `env.launch` says how this process came to run: "restored" when the
162
+ system recreated the app from a session it ended on its own, "fresh" for a
163
+ launch the user started. Whether to load what `onSuspend` saved is the app's
164
+ call. `env.visibility` is only whether anything is on screen, for pausing
165
+ animation and audio, not the persistence moment.
166
+
167
+ ```tsx
168
+ onSuspend(() => file("session.json").write(JSON.stringify(state)))
169
+ if (env.launch === "restored") state = JSON.parse(await file("session.json").text())
170
+ ```
171
+
149
172
  ## Reference
150
173
 
151
174
  The [reference](/core/reference/) covers the API by subject: the element
@@ -21,6 +21,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
21
21
  - `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
22
22
  - `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
23
23
  - `stagger.tsx` - enter/exit/stagger with no per-frame JS: `from` and `exit` per property on each row's own `transition`, `stagger` on the ANCESTOR cascading them in occurrence order, and the replay idiom - a keyed `<Show>` remounts the subtree when its `when` value changes, so bumping an epoch replays the whole cascade. Exiting rows stay painted until they settle.
24
+ - `layout-slide.tsx` - the `layout` transition, the companion to exit: rows declaring it slide from the box they had to the box a layout gives them instead of jumping - tap a row to remove it (its exit plays where it was while the rows below slide up), the header to shuffle the order (a `<For>` reorder), the footer to add one back. No per-frame JS, no hand-animated `y`.
24
25
 
25
26
  ## Pointer input
26
27
  - `pointer-local-coords.tsx` - the three pointer coordinate frames (`clientX` window, `localX` the handling node's own frame, `parentX` its path-parent's frame - where the node's x/y live) and the transform-proof drag idiom: grab offset from `localX` at down, place with `parentX - offset` on moves. Exact inside rotated/scaled ancestors and when the pointer leaves the node mid-drag.
@@ -60,22 +60,22 @@ let FRAGMENT_PULSE = glsl`
60
60
  function App() {
61
61
  let triangle = createBuffer(new Float32Array([0, 0.6, -0.5, -0.4, 0.5, -0.4]), { label: "tri" })
62
62
  let vs = compileShader("vertex", VERTEX, { header: true })
63
- let attrs = [{ name: "aPos", format: "vec2" as const }]
63
+ let attrs = [{ name: "aPos", format: "float32x2" as const }]
64
64
  let warm = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_WARM, { header: true })), {
65
- attributes: attrs,
65
+ buffers: [{ attributes: attrs }],
66
66
  depth: true,
67
67
  })
68
68
  let cool = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_COOL, { header: true })), {
69
- attributes: attrs,
69
+ buffers: [{ attributes: attrs }],
70
70
  depth: true,
71
71
  })
72
72
  let pulse = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_PULSE, { header: true })), {
73
- attributes: attrs,
73
+ buffers: [{ attributes: attrs }],
74
74
  })
75
75
 
76
76
  let target = createDrawTarget(512, 512, null, { depth: true, clearColor: [0.04, 0.04, 0.08, 1], label: "orbits" })
77
- let warmDraw = addDraw(target, warm, { uAngle: 0 }, { buffer: triangle })
78
- let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, { buffer: triangle })
77
+ let warmDraw = addDraw(target, warm, { uAngle: 0 }, { buffers: [triangle] })
78
+ let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, { buffers: [triangle] })
79
79
 
80
80
  // A third entry blinks in and out every second: add/remove are ordinary
81
81
  // per-frame-affordable writes, and a removed DrawId simply retires. Its
@@ -91,7 +91,7 @@ function App() {
91
91
  setDrawParams(target, coolDraw, { uAngle: t + Math.PI })
92
92
  let wantPulse = Math.floor(t) % 2 === 0
93
93
  if (wantPulse && pulseDraw === null) {
94
- pulseDraw = addDraw(target, pulse, { uAngle: t * 0.3, uPhase: t * 5 }, { buffer: triangle, before: warmDraw })
94
+ pulseDraw = addDraw(target, pulse, { uAngle: t * 0.3, uPhase: t * 5 }, { buffers: [triangle], before: warmDraw })
95
95
  } else if (!wantPulse && pulseDraw !== null) {
96
96
  removeDraw(target, pulseDraw)
97
97
  pulseDraw = null
@@ -1,7 +1,7 @@
1
1
  // Instanced drawing with per-instance attributes: one 3-vertex triangle in
2
2
  // the vertex buffer, drawn hundreds of times, each copy reading its own
3
- // record from an instance buffer (instanceAttributes on the pipeline +
4
- // instanceBuffer on the target, vertex divisor 1 underneath). Here each
3
+ // record from an instance buffer (a `stepMode: "instance"` layout on the
4
+ // pipeline, vertex divisor 1 underneath). Here each
5
5
  // record is one petal of a phyllotaxis spiral - its angle, radius, size, and
6
6
  // tint, computed once in JS - so the vertex stage just reads state instead
7
7
  // of re-deriving it from gl_InstanceID every frame, and the geometry buffer
@@ -45,7 +45,7 @@ let FRAGMENT = glsl`
45
45
  }
46
46
  `
47
47
 
48
- // One record per petal (matching instanceAttributes: 6 floats): radius grows
48
+ // One record per petal (matching the instance layout: 6 floats): radius grows
49
49
  // with sqrt(index), the angle steps by the golden angle (~2.39996 rad) - the
50
50
  // sunflower layout - petals shrink toward the rim, and the tint walks a
51
51
  // cosine palette.
@@ -70,15 +70,19 @@ function App() {
70
70
  let instanceId = createBuffer(petalRecords(), { label: "petal-records" })
71
71
  let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0 }, {
72
72
  label: "petals",
73
- attributes: [{ name: "aPos", format: "vec2" }],
74
- buffer: bufferId,
75
- instanceAttributes: [
76
- { name: "iAngle", format: "f32" },
77
- { name: "iRadius", format: "f32" },
78
- { name: "iScale", format: "f32" },
79
- { name: "iTint", format: "vec3" },
73
+ buffers: [
74
+ { attributes: [{ name: "aPos", format: "float32x2" }], buffer: bufferId },
75
+ {
76
+ stepMode: "instance",
77
+ attributes: [
78
+ { name: "iAngle", format: "float32" },
79
+ { name: "iRadius", format: "float32" },
80
+ { name: "iScale", format: "float32" },
81
+ { name: "iTint", format: "float32x3" },
82
+ ],
83
+ buffer: instanceId,
84
+ },
80
85
  ],
81
- instanceBuffer: instanceId,
82
86
  instanceCount: 1,
83
87
  clearColor: [0.03, 0.03, 0.06, 1],
84
88
  })
@@ -74,11 +74,15 @@ function App() {
74
74
  let bufferId = createBuffer(particles(1500), { label: "particle-verts" })
75
75
  let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0, uTintA: [1.0, 0.45, 0.15], uTintB: [0.25, 0.5, 1.0] }, {
76
76
  label: "particles",
77
- attributes: [
78
- { name: "aPos", format: "vec3" },
79
- { name: "aSeed", format: "f32" },
77
+ buffers: [
78
+ {
79
+ attributes: [
80
+ { name: "aPos", format: "float32x3" },
81
+ { name: "aSeed", format: "float32" },
82
+ ],
83
+ buffer: bufferId,
84
+ },
80
85
  ],
81
- buffer: bufferId,
82
86
  topology: "points",
83
87
  blend: "add",
84
88
  clearColor: [0.02, 0.02, 0.05, 1],
@@ -87,11 +87,15 @@ function App() {
87
87
  let indexId = createBuffer(cubeIndices(), { label: "cube-indices" })
88
88
  let id = createPipelineTexture(VERTEX, FRAGMENT, 1024, 1024, { uTime: 0 }, {
89
89
  label: "cube",
90
- attributes: [
91
- { name: "aPos", format: "vec3" },
92
- { name: "aColor", format: "vec3" },
90
+ buffers: [
91
+ {
92
+ attributes: [
93
+ { name: "aPos", format: "float32x3" },
94
+ { name: "aColor", format: "float32x3" },
95
+ ],
96
+ buffer: bufferId,
97
+ },
93
98
  ],
94
- buffer: bufferId,
95
99
  indexBuffer: indexId,
96
100
  indexFormat: "uint16",
97
101
  depth: true,
@@ -65,12 +65,12 @@ let FRAGMENT_MAP = glsl`
65
65
  function App() {
66
66
  let quad = createBuffer(new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]), { label: "quad" })
67
67
  let vs = compileShader("vertex", VERTEX, { header: true })
68
- let attrs = [{ name: "aPos", format: "vec2" as const }]
68
+ let attrs = [{ name: "aPos", format: "float32x2" as const }]
69
69
  let tint = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_TINT, { header: true })), {
70
- attributes: attrs,
70
+ buffers: [{ attributes: attrs }],
71
71
  })
72
72
  let mapped = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_MAP, { header: true })), {
73
- attributes: attrs,
73
+ buffers: [{ attributes: attrs }],
74
74
  })
75
75
 
76
76
  // 2x2 patterns, nearest + repeat, so gl_FragCoord tiling shows hard cells.
@@ -99,12 +99,12 @@ function App() {
99
99
  if (i === 1 || i === 5) {
100
100
  // Patterned entries read the shared uMap; the one at i === 5 brings
101
101
  // its own binding and keeps its stripes through every shared swap.
102
- addDraw(target, mapped, { uCenter }, i === 5 ? { buffer: quad, textures: { uMap: stripes } } : { buffer: quad })
102
+ addDraw(target, mapped, { uCenter }, i === 5 ? { buffers: [quad], textures: { uMap: stripes } } : { buffers: [quad] })
103
103
  } else if (i === 3) {
104
104
  // The override quad: its own uTint beats the shared color cycle.
105
- addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, { buffer: quad })
105
+ addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, { buffers: [quad] })
106
106
  } else {
107
- addDraw(target, tint, { uCenter }, { buffer: quad })
107
+ addDraw(target, tint, { uCenter }, { buffers: [quad] })
108
108
  }
109
109
  }
110
110
 
@@ -60,14 +60,18 @@ function App() {
60
60
  label: "sprites",
61
61
  topology: "triangle-strip",
62
62
  vertexCount: 4,
63
- attributes: [{ name: "aPos", format: "vec2" }],
64
- buffer: quad,
65
- instanceAttributes: [
66
- { name: "iCenter", format: "vec2" },
67
- { name: "iSize", format: "f32" },
68
- { name: "iTint", format: "vec3" },
63
+ buffers: [
64
+ { attributes: [{ name: "aPos", format: "float32x2" }], buffer: quad },
65
+ {
66
+ stepMode: "instance",
67
+ attributes: [
68
+ { name: "iCenter", format: "float32x2" },
69
+ { name: "iSize", format: "float32" },
70
+ { name: "iTint", format: "float32x3" },
71
+ ],
72
+ buffer: records,
73
+ },
69
74
  ],
70
- instanceBuffer: records,
71
75
  instanceCount: MAX_SPRITES,
72
76
  clearColor: [0.03, 0.03, 0.06, 1],
73
77
  })
@@ -0,0 +1,78 @@
1
+ // The `layout` transition, the companion to `exit`: a row declaring it
2
+ // slides from the box it had to the box the next layout gives it, so the
3
+ // list never jumps when a row leaves, arrives or moves. Nothing here
4
+ // animates `y` by hand and there is no per-frame JS: the rows keep their
5
+ // flex layout, and the runtime slides each one from where it was painted.
6
+ // Tap a row to remove it - its exit plays where it was (the pop-out) while
7
+ // the rows below slide up past it. Tap the header to shuffle: a `<For>`
8
+ // reorder moves the surviving nodes, so every displaced row slides. Tap
9
+ // the footer to add a row back: the rows below its slot slide apart and
10
+ // the new row enters through its `from`. A reflow that lands mid-slide
11
+ // retargets, and a spring keeps its momentum. `layout` is parent-relative
12
+ // (declare it on every level that should glide) and position only: a size
13
+ // change snaps.
14
+ import { render, createSignal, For } from "@solidrt/core"
15
+ import type { Transition } from "@solidrt/core"
16
+
17
+ const ALL_ROWS = ["Signals", "Effects", "Memos", "Stores", "Boundaries", "Resources"]
18
+ // Rows the list starts with; the rest come back through the footer.
19
+ const INITIAL_ROWS = 4
20
+ // Every reflow slides on this spring.
21
+ const SLIDE = { duration: 400, bounce: 0.15 } satisfies Transition
22
+ // Enter from the left, leave to the right, each direction on its own curve.
23
+ const ARRIVE = {
24
+ duration: 300,
25
+ curve: "ease-out",
26
+ from: -80,
27
+ exit: { value: 80, curve: "ease-in", duration: 200 },
28
+ } satisfies Transition
29
+ const FADE = {
30
+ duration: 300,
31
+ curve: "ease-out",
32
+ from: 0,
33
+ exit: { value: 0, curve: "ease-in", duration: 200 },
34
+ } satisfies Transition
35
+
36
+ function shuffled(items: string[]): string[] {
37
+ let pool = items.slice()
38
+ let out: string[] = []
39
+ while (pool.length > 0) out.push(...pool.splice(Math.floor(Math.random() * pool.length), 1))
40
+ return out
41
+ }
42
+
43
+ function App() {
44
+ let [rows, setRows] = createSignal(ALL_ROWS.slice(0, INITIAL_ROWS))
45
+ let addRow = () => {
46
+ let next = ALL_ROWS.find((label) => !rows().includes(label))
47
+ if (next) setRows([...rows(), next])
48
+ }
49
+
50
+ return (
51
+ <window padding={40} gap={12}>
52
+ <view height={40} justifyContent="center" onPointerDown={() => setRows(shuffled(rows()))}>
53
+ <text color="#8899aa" fontSize={14}>Tap here to shuffle, a row to remove it</text>
54
+ </view>
55
+ <For each={rows()}>
56
+ {(label) => (
57
+ <view
58
+ x={0}
59
+ opacity={1}
60
+ transition={{ layout: SLIDE, x: ARRIVE, opacity: FADE }}
61
+ height={48}
62
+ justifyContent="center"
63
+ paddingLeft={16}
64
+ onPointerDown={() => setRows(rows().filter((row) => row !== label))}
65
+ >
66
+ <d-rect color="#2a3a55" radius={10} />
67
+ <text color="#e8eef6" fontSize={18}>{label}</text>
68
+ </view>
69
+ )}
70
+ </For>
71
+ <view height={40} justifyContent="center" transition={{ layout: SLIDE }} onPointerDown={addRow}>
72
+ <text color="#8899aa" fontSize={14}>Tap here to add a row back</text>
73
+ </view>
74
+ </window>
75
+ )
76
+ }
77
+
78
+ render(() => <App />)
@@ -1,16 +1,24 @@
1
1
  // Enter, exit and stagger are declared on elements; the runtime plays them
2
2
  // with no per-frame JS. `from` is the value a property animates in from at
3
- // first attach, `exit` the value it animates to on removal - the node stays
4
- // painted until that settles, then frees. Both are per-property entries on
5
- // the element's own transition spec. `stagger` (ms) goes on an ANCESTOR,
6
- // never on the animating elements: every descendant enter or exit that
7
- // begins in the same frame gets index * stagger of extra delay, in
8
- // occurrence order. It cascades nothing on its own - the descendants must
9
- // declare from/exit.
3
+ // first attach, `exit` the value it animates to on removal. Both are
4
+ // per-property entries on the element's own transition spec. An exit
5
+ // reaches the whole removed subtree: every `exit` declared under the node
6
+ // that unmounts plays, and that node stays painted until the last of them
7
+ // settles, then frees. It leaves the layout at once, so a fresh panel
8
+ // mounted mid-exit takes its place while the old one fades over it. The
9
+ // rows below never unmount one by one - the panel around them does - and
10
+ // they still leave the way they arrived. Each direction owns its motion:
11
+ // `from`/`exit` take a bare value (the entry's curve, duration and delay)
12
+ // or `{ value, curve?, duration?, bounce?, delay? }`, so the fade below
13
+ // eases out on the way in and eases in, shorter, on the way out.
14
+ // `stagger` (ms) goes on an ANCESTOR, never on the animating elements: every
15
+ // descendant enter or exit that begins in the same frame gets index * stagger
16
+ // of extra delay, in tree order. It cascades nothing on its own - the
17
+ // descendants must declare from/exit.
10
18
  //
11
19
  // An enter plays once per mount. To replay it, remount the subtree: a keyed
12
20
  // <Show> re-creates its child whenever `when` changes value, so bumping an
13
- // epoch replays the whole cascade. Tap anywhere: odd taps unmount the list
21
+ // epoch replays the whole cascade. Tap anywhere: odd taps unmount the panel
14
22
  // (staggered exits), even taps mount a fresh one (staggered enters).
15
23
  import { render, createSignal, Show } from "@solidrt/core"
16
24
  import type { Transition } from "@solidrt/core"
@@ -18,25 +26,33 @@ import type { Transition } from "@solidrt/core"
18
26
  const ROWS = ["Signals", "Effects", "Memos", "Stores", "Boundaries"]
19
27
  // Extra delay per row before its enter or exit starts.
20
28
  const STAGGER_MS = 70
21
- // Slide in from the left, out to the right; fade both ways.
29
+ // Slide in from the left, out to the right; fade both ways, the exit on
30
+ // its own curve and clock.
22
31
  const SLIDE = { duration: 500, bounce: 0.2, from: -80, exit: 80 } satisfies Transition
23
- const FADE = { duration: 350, curve: "ease-out", from: 0, exit: 0 } satisfies Transition
32
+ const FADE = {
33
+ duration: 350,
34
+ curve: "ease-out",
35
+ from: 0,
36
+ exit: { value: 0, curve: "ease-in", duration: 200 },
37
+ } satisfies Transition
24
38
 
25
39
  function App() {
26
40
  let [epoch, setEpoch] = createSignal(1)
27
41
 
28
42
  return (
29
- <window padding={40} gap={12} onPointerDown={() => setEpoch((e) => e + 1)} transition={{ stagger: STAGGER_MS }}>
43
+ <window padding={40} gap={12} onPointerDown={() => setEpoch((e) => e + 1)}>
30
44
  <text color="#8899aa" fontSize={14}>Tap to replay</text>
31
45
  <Show when={epoch() % 2 === 1 ? epoch() : 0} keyed>
32
- {(_epoch) =>
33
- ROWS.map((label) => (
34
- <view x={0} opacity={1} transition={{ x: SLIDE, opacity: FADE }} height={48} justifyContent="center" paddingLeft={16}>
35
- <d-rect color="#2a3a55" radius={10} />
36
- <text color="#e8eef6" fontSize={18}>{label}</text>
37
- </view>
38
- ))
39
- }
46
+ {(_epoch) => (
47
+ <view gap={12} transition={{ stagger: STAGGER_MS }}>
48
+ {ROWS.map((label) => (
49
+ <view x={0} opacity={1} transition={{ x: SLIDE, opacity: FADE }} height={48} justifyContent="center" paddingLeft={16}>
50
+ <d-rect color="#2a3a55" radius={10} />
51
+ <text color="#e8eef6" fontSize={18}>{label}</text>
52
+ </view>
53
+ ))}
54
+ </view>
55
+ )}
40
56
  </Show>
41
57
  </window>
42
58
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -31,11 +31,11 @@
31
31
  "AGENTS.md"
32
32
  ],
33
33
  "devDependencies": {
34
- "@solidrt/flux-types": "0.0.57"
34
+ "@solidrt/flux-types": "0.0.59"
35
35
  },
36
36
  "peerDependencies": {
37
- "@solidjs/signals": "2.0.0-rc.6",
38
- "@solidjs/universal": "2.0.0-rc.6",
39
- "solid-js": "2.0.0-rc.6"
37
+ "@solidjs/signals": "2.0.0-rc.8",
38
+ "@solidjs/universal": "2.0.0-rc.8",
39
+ "solid-js": "2.0.0-rc.8"
40
40
  }
41
41
  }
@@ -37,6 +37,8 @@ export type SystemTheme = "dark" | "light" | "unknown"
37
37
 
38
38
  export type Visibility = "visible" | "hidden"
39
39
 
40
+ export type Launch = "fresh" | "restored"
41
+
40
42
  export type Orientation = "portrait" | "portraitFlipped" | "landscape" | "landscapeFlipped" | "unknown"
41
43
 
42
44
  let devicesAccessor: (() => InputDevices | undefined) | undefined
@@ -65,6 +67,22 @@ function ensureSystemThemeState() {
65
67
  })
66
68
  }
67
69
 
70
+ // Fixed for the process, so a plain value rather than a signal: the sticky
71
+ // `launch` event replays synchronously on subscribe, and the runner emits it
72
+ // before any app code runs.
73
+ let launchValue: Launch | undefined
74
+
75
+ function ensureLaunchState() {
76
+ if (launchValue) return
77
+ runWithOwner(null, () => {
78
+ on("launch", (e: { state?: Launch }) => {
79
+ launchValue = e.state === "restored" ? "restored" : "fresh"
80
+ })
81
+ })
82
+ // A runtime that reports no launch fact (desktop today) launches fresh.
83
+ launchValue ??= "fresh"
84
+ }
85
+
68
86
  let visibilityAccessor: (() => Visibility) | undefined
69
87
 
70
88
  function ensureVisibilityState() {
@@ -218,15 +236,27 @@ export let env = {
218
236
  * `visibilityState` vocabulary without the `document` machinery - react
219
237
  * to it in a tracked scope (JSX, memo, effect).
220
238
  *
221
- * This is the persistence moment: there is no close event on any
222
- * platform (Android gives no time, desktop window close never enters
223
- * JS), so save state when this goes "hidden". While hidden, timers keep
224
- * running but no frames are produced.
239
+ * A rendering and UI fact (nothing is on screen: stop animating, pause
240
+ * the audio), not the persistence moment - that is `onSuspend`, which the
241
+ * runtime waits for. While hidden no frames are produced.
225
242
  */
226
243
  get visibility(): Visibility {
227
244
  ensureVisibilityState()
228
245
  return visibilityAccessor!()
229
246
  },
247
+ /**
248
+ * How this process came to run. "restored": the system recreated the app
249
+ * from a session it ended on its own (a suspended app reclaimed in the
250
+ * background; Android's saved instance state). "fresh": launched anew, a
251
+ * first start or one after `exit()` or a close ended the previous
252
+ * instance, and always on desktop. Reported, not interpreted: whether to
253
+ * load what `onSuspend` saved is the app's decision. Fixed for the
254
+ * process, so it needs no tracked scope.
255
+ */
256
+ get launch(): Launch {
257
+ ensureLaunchState()
258
+ return launchValue!
259
+ },
230
260
  /** Orientation of the display the window is on. */
231
261
  get orientation(): Orientation {
232
262
  ensureOrientationState()