@caperjs/solid 0.7.0

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 (52) hide show
  1. package/README.md +191 -0
  2. package/jsx.d.ts +39 -0
  3. package/lib/AnimatedShow.d.ts +25 -0
  4. package/lib/AnimatedShow.d.ts.map +1 -0
  5. package/lib/AnimatedShow.test.d.ts +2 -0
  6. package/lib/AnimatedShow.test.d.ts.map +1 -0
  7. package/lib/Composable.d.ts +39 -0
  8. package/lib/Composable.d.ts.map +1 -0
  9. package/lib/Composable.test.d.ts +2 -0
  10. package/lib/Composable.test.d.ts.map +1 -0
  11. package/lib/animated.d.ts +16 -0
  12. package/lib/animated.d.ts.map +1 -0
  13. package/lib/animated.test.d.ts +2 -0
  14. package/lib/animated.test.d.ts.map +1 -0
  15. package/lib/asComponent.d.ts +19 -0
  16. package/lib/asComponent.d.ts.map +1 -0
  17. package/lib/asComponent.test.d.ts +2 -0
  18. package/lib/asComponent.test.d.ts.map +1 -0
  19. package/lib/caper-plugin-solid.mjs +337 -0
  20. package/lib/caper-plugin-solid.mjs.map +1 -0
  21. package/lib/catalog.d.ts +17 -0
  22. package/lib/catalog.d.ts.map +1 -0
  23. package/lib/index.d.ts +9 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/renderer.d.ts +4 -0
  26. package/lib/renderer.d.ts.map +1 -0
  27. package/lib/renderer.test.d.ts +2 -0
  28. package/lib/renderer.test.d.ts.map +1 -0
  29. package/lib/useTick.d.ts +4 -0
  30. package/lib/useTick.d.ts.map +1 -0
  31. package/lib/useTick.test.d.ts +2 -0
  32. package/lib/useTick.test.d.ts.map +1 -0
  33. package/lib/version.d.ts +2 -0
  34. package/lib/version.d.ts.map +1 -0
  35. package/package.json +66 -0
  36. package/src/AnimatedShow.test.tsx +146 -0
  37. package/src/AnimatedShow.tsx +87 -0
  38. package/src/Composable.test.ts +214 -0
  39. package/src/Composable.ts +84 -0
  40. package/src/animated.test.ts +94 -0
  41. package/src/animated.ts +39 -0
  42. package/src/asComponent.test.ts +168 -0
  43. package/src/asComponent.ts +37 -0
  44. package/src/catalog.ts +28 -0
  45. package/src/index.ts +11 -0
  46. package/src/renderer.test.ts +272 -0
  47. package/src/renderer.ts +155 -0
  48. package/src/useTick.test.ts +28 -0
  49. package/src/useTick.ts +10 -0
  50. package/src/version.ts +1 -0
  51. package/vite.d.mts +10 -0
  52. package/vite.mjs +25 -0
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @caperjs/solid
2
+
3
+ A declarative [SolidJS](https://www.solidjs.com/) view layer for Caper. Any
4
+ `Container` subclass may declare its members by returning JSX from a `compose()`
5
+ method; Solid's universal renderer mounts that tree into the container **once**,
6
+ and signals do every update after that — fine-grained, no VDOM, no re-render.
7
+ It is purely additive: imperative Caper (`this.add.*`, `added()`, `update()`)
8
+ keeps working exactly as before.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @caperjs/solid solid-js
14
+ ```
15
+
16
+ ## tsconfig contract
17
+
18
+ Three settings, and all three are required:
19
+
20
+ ```jsonc
21
+ {
22
+ "compilerOptions": {
23
+ // Leave the JSX alone — vite-plugin-solid does the real compilation.
24
+ "jsx": "preserve",
25
+ // Names the *type* lookup root for JSX. Nothing called `CaperJSX.h` ever
26
+ // runs; it exists because a transitive `@types/react` shadows any global
27
+ // `JSX` namespace, and `jsxImportSource` only applies in `react-jsx` mode.
28
+ "jsxFactory": "CaperJSX.h",
29
+ // Ships that `CaperJSX` namespace: the intrinsic elements and their props.
30
+ "types": ["@caperjs/core/client", "@caperjs/solid/jsx"]
31
+ }
32
+ }
33
+ ```
34
+
35
+ `caper doctor` checks all three whenever the app depends on `@caperjs/solid`,
36
+ and prints the exact keys that are missing. It walks the `extends` chain, so
37
+ these three can live in a shared base config instead of the app's own file.
38
+
39
+ ## Compose
40
+
41
+ ```tsx
42
+ import { ComposableContainer, type Composes } from '@caperjs/solid';
43
+ import { createSignal } from 'solid-js';
44
+
45
+ export class HealthBar extends ComposableContainer implements Composes {
46
+ private health = createSignal(100);
47
+
48
+ /** Imperative API — an ordinary method that happens to write a signal. */
49
+ public damage(amount: number) {
50
+ this.health[1]((hp) => Math.max(0, hp - amount));
51
+ }
52
+
53
+ public compose() {
54
+ const hp = this.health[0];
55
+ return (
56
+ <container>
57
+ <graphics draw={BAR_BG} />
58
+ <graphics draw={BAR_FILL} scale={{ x: hp() / 100, y: 1 }} />
59
+ <text text={`${hp()} HP`} anchor={0.5} x={WIDTH / 2} y={HEIGHT / 2} />
60
+ </container>
61
+ );
62
+ }
63
+ }
64
+ ```
65
+
66
+ `compose()` runs the first time the container hits the stage, and the Solid root
67
+ is disposed in `destroy()`. Re-adding does not remount. The rule of thumb:
68
+ **`compose()` declares what exists, signals say when state changed, `update()`
69
+ moves things every frame.**
70
+
71
+ Intrinsic elements are `container`, `sprite`, `text`, `graphics` and
72
+ `flexContainer`. For anything else, `asComponent(Ctor, defaults?)` lifts an
73
+ existing display class into JSX — it constructs the instance once and applies
74
+ props reactively:
75
+
76
+ ```tsx
77
+ const Orbiter$ = asComponent(Orbiter);
78
+ <Orbiter$ x={40} y={80} ref={(el: Orbiter) => (this.orbiter = el)} />;
79
+ ```
80
+
81
+ `useTick(fn)` runs `fn` every frame for the lifetime of the owning component —
82
+ `update()` for function components.
83
+
84
+ ## Animation
85
+
86
+ `animated(source, opts?)` returns a **gliding** accessor: it reads like any other
87
+ signal, but eases toward its source instead of snapping to it. The binding stays
88
+ declarative — nothing in the view knows an animation is running.
89
+
90
+ ```tsx
91
+ const width = animated(() => hp() / 100); // opts: { duration, ease }
92
+ <graphics draw={BAR_FILL} scale={{ x: width(), y: 1 }} />;
93
+ ```
94
+
95
+ `<AnimatedShow>` is `<Show>` that holds its children on stage until the exit
96
+ animation finishes, so popups and panels can leave gracefully:
97
+
98
+ ```tsx
99
+ <AnimatedShow when={open} exit={{ pixi: { alpha: 0, y: 20 }, duration: 0.3 }}>
100
+ <container>…</container>
101
+ </AnimatedShow>
102
+ ```
103
+
104
+ `enter` / `exit` are plain GSAP vars; target properties live in a `pixi: {...}`
105
+ block, which is GSAP's PixiPlugin — caper's `GSAPPlugin` registers it at app
106
+ bootstrap, so inside a running app there is nothing to wire up. Both APIs must be
107
+ called inside a reactive owner (a component body or a `compose()`).
108
+
109
+ One-shot effects (shake, pulse) stay imperative through a ref: every Caper
110
+ container carries the `Animated` mixin already.
111
+
112
+ ## Authoring rules
113
+
114
+ Six rules. The first is the mental model; the rest are the edges that bite.
115
+
116
+ 1. **Signals for event-rate state, `update()` for per-frame bulk motion.**
117
+ `text={purse()}` on a value that changes when the player does something is
118
+ free. Hundreds of per-frame bindings are fine too, but moving a crowd of
119
+ objects every frame still belongs in an imperative `update()` — or in
120
+ `useTick(fn)` + a `ref` when the code lives in a function component. Per-frame
121
+ signals are the seasoning, not the meal.
122
+
123
+ 2. **`draw` functions must be stable references.** Solid batches an element's
124
+ dynamic props into one `!==`-guarded effect, so an inline
125
+ `draw={dot(3, color)}` allocates a fresh closure on every change of *any* prop
126
+ in that batch and forces a full `clear()` + redraw. Hoist the factory result
127
+ to a module constant (or a `createMemo`) and pass the same function every
128
+ time.
129
+
130
+ 3. **Never imperatively remove or reparent a child JSX created.** Solid owns
131
+ those nodes and will try to move or dispose them later. Imperative work
132
+ alongside a composed tree is fine — `initialize()`, `this.add.*`, `update()`
133
+ all keep working, and `compose()` appends rather than taking over — it just
134
+ must not reach into the declarative half.
135
+
136
+ 4. **Each `compose()` is its own reactive root.** Signals cross roots fine, so
137
+ instance fields are the bridge between a class's imperative API and its view.
138
+ Solid **context** does not cross the class boundary — pass props or read an
139
+ instance field instead.
140
+
141
+ 5. **`text` elements default to `eventMode: 'none'`.** A hit-testable but
142
+ non-interactive label sitting over a button would otherwise swallow the tap.
143
+ Attaching an `on*` prop flips the element to `'static'` automatically, and an
144
+ explicit `eventMode` prop overrides the default.
145
+
146
+ 6. **One-shot effects stay imperative.** Shake, pulse and friends are a `ref`
147
+ away: a Caper container mounted from JSX still carries the `Animated` mixin,
148
+ so `bar.shake()` works unchanged. Reserve `animated()` / `<AnimatedShow>` for
149
+ motion that is a function of state.
150
+
151
+ ## Testing your components
152
+
153
+ Vitest resolves `solid-js` to its **server** build by default, where signals set
154
+ once and never update again. Force the reactive build in your app's
155
+ `vitest.config.ts` (this package's own config does the same):
156
+
157
+ ```ts
158
+ export default defineConfig({
159
+ // Externalized deps are resolved by node, so the conditions only take effect
160
+ // once solid is inlined — both halves are needed.
161
+ resolve: { conditions: ['development', 'browser'] },
162
+ test: { server: { deps: { inline: [/solid-js/] } } },
163
+ });
164
+ ```
165
+
166
+ ## Build wiring
167
+
168
+ One flag in the app's vite config:
169
+
170
+ ```ts
171
+ import { caper } from '@caperjs/core/vite';
172
+
173
+ export default defineConfig({
174
+ plugins: [caper({ solid: true })],
175
+ });
176
+ ```
177
+
178
+ The preset resolves `@caperjs/solid/vite` from the **app's** `node_modules`, so
179
+ the app needs `@caperjs/solid` and `solid-js` installed; `vite-plugin-solid`
180
+ comes with this package and never has to be installed by hand. Pass an object to
181
+ narrow what gets compiled as JSX: `caper({ solid: { include: ['src/ui/**/*.tsx'] } })`
182
+ (default `['**/*.tsx']`).
183
+
184
+ Outside the preset — a bare vite config, or a vitest config that has to compile
185
+ JSX — use the plugin directly:
186
+
187
+ ```ts
188
+ import { caperSolid } from '@caperjs/solid/vite';
189
+
190
+ export default defineConfig({ plugins: [caperSolid()] });
191
+ ```
package/jsx.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * JSX types for `@caperjs/solid`. Add "@caperjs/solid/jsx" to the "types" array
3
+ * of your tsconfig, alongside `"jsx": "preserve"` and
4
+ * `"jsxFactory": "CaperJSX.h"`.
5
+ *
6
+ * The namespace hangs off `CaperJSX` rather than the global `JSX` namespace
7
+ * because `@types/react` is routinely in an app's program (pulled in
8
+ * transitively) and its `declare global { namespace JSX }` shadows any global
9
+ * one we add. `jsxFactory` names the type-lookup root instead; under
10
+ * `jsx: "preserve"` it is *only* a type lookup — vite-plugin-solid still does
11
+ * the real compilation, so nothing named `CaperJSX.h` is ever called.
12
+ * (`jsxImportSource` would be the modern answer, but it only applies in
13
+ * `react-jsx` mode.)
14
+ *
15
+ * Element props are deliberately loose for v1; a later release types each
16
+ * intrinsic against its Pixi class.
17
+ */
18
+
19
+ declare namespace CaperJSX {
20
+ function h(type: any, props?: any, ...children: any[]): any;
21
+
22
+ /** The v1 prop bag for every intrinsic element: anything the class accepts. */
23
+ type ElementProps = Record<string, any> & { children?: any };
24
+
25
+ namespace JSX {
26
+ interface IntrinsicElements {
27
+ container: ElementProps;
28
+ sprite: ElementProps;
29
+ text: ElementProps;
30
+ graphics: ElementProps;
31
+ flexContainer: ElementProps;
32
+ }
33
+ type Element = any;
34
+ type ElementType = any;
35
+ interface ElementChildrenAttribute {
36
+ children: {};
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Conditional UI with enter / exit animations.
3
+ *
4
+ * ```tsx
5
+ * <AnimatedShow when={open}>
6
+ * <container>…</container>
7
+ * </AnimatedShow>
8
+ * ```
9
+ *
10
+ * Custom `enter` / `exit` are plain GSAP vars — put target properties in a
11
+ * `pixi: {...}` block (`{ pixi: { y: 20, alpha: 0 }, duration: 0.3 }`).
12
+ * There is no `fallback` — the whole point is that the node outlives the flag.
13
+ *
14
+ * Requires GSAP's PixiPlugin, which caper's `GSAPPlugin` registers during app
15
+ * bootstrap. This package deliberately does not register it itself; outside a
16
+ * booted `Application` (tests, standalone use) call
17
+ * `gsap.registerPlugin(PixiPlugin)` yourself first.
18
+ */
19
+ export declare function AnimatedShow(props: {
20
+ when: boolean | (() => boolean);
21
+ children: any;
22
+ enter?: gsap.TweenVars;
23
+ exit?: gsap.TweenVars;
24
+ }): any;
25
+ //# sourceMappingURL=AnimatedShow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedShow.d.ts","sourceRoot":"","sources":["../src/AnimatedShow.tsx"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,IAAI,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAChC,QAAQ,EAAE,GAAG,CAAC;IACd,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC;IACvB,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC;CACvB,OAiDA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AnimatedShow.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedShow.test.d.ts","sourceRoot":"","sources":["../src/AnimatedShow.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { Container, Constructor, Scene } from '@caperjs/core';
2
+ /**
3
+ * What `compose()` hands back. `unknown` for now so this file stays JSX-free;
4
+ * it becomes the real element type when `jsx.d.ts` lands.
5
+ */
6
+ export type Composed = unknown;
7
+ /**
8
+ * The optional contract a `Composable` subclass opts into. Declared separately
9
+ * from the mixin (rather than merged onto it) so the mixin never claims a
10
+ * `compose` member of its own — the runtime just checks for one.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * class HealthBar extends ComposableContainer implements Composes {
15
+ * compose() {
16
+ * return <text text="100 HP" />;
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+ export interface Composes {
22
+ compose(): Composed;
23
+ }
24
+ /**
25
+ * Give a display class the `compose()` contract.
26
+ *
27
+ * The mixin adds no public members, so the returned constructor keeps `Base`'s
28
+ * own type — `compose()` is declared by the subclass via {@link Composes}.
29
+ *
30
+ * @param Base - The display class to make composable.
31
+ */
32
+ export declare function Composable<TBase extends Constructor<any>>(Base: TBase): TBase;
33
+ /** A caper {@link Container} that can declare its members with JSX. */
34
+ export declare const ComposableContainer: typeof Container;
35
+ export type ComposableContainer = InstanceType<typeof ComposableContainer>;
36
+ /** A caper {@link Scene} that can declare its members with JSX. */
37
+ export declare const ComposableScene: typeof Scene;
38
+ export type ComposableScene = InstanceType<typeof ComposableScene>;
39
+ //# sourceMappingURL=Composable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Composable.d.ts","sourceRoot":"","sources":["../src/Composable.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAM9D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC;AAE/B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,QAAQ;IACvB,OAAO,IAAI,QAAQ,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,SAAS,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,KAAK,CAiC7E;AAED,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,kBAAwB,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAE3E,mEAAmE;AACnE,eAAO,MAAM,eAAe,cAAoB,CAAC;AACjD,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Composable.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Composable.test.d.ts","sourceRoot":"","sources":["../src/Composable.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Follow `source` smoothly.
3
+ *
4
+ * Must be called inside a reactive owner (a component body or a `compose()`),
5
+ * because it registers an `onCleanup` to kill its animation.
6
+ *
7
+ * ```ts
8
+ * const barWidth = animated(() => hp() / 100);
9
+ * <graphics scale={{ x: barWidth(), y: 1 }} />
10
+ * ```
11
+ */
12
+ export declare function animated(source: () => number, opts?: {
13
+ duration?: number;
14
+ ease?: string;
15
+ }): () => number;
16
+ //# sourceMappingURL=animated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animated.d.ts","sourceRoot":"","sources":["../src/animated.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,MAAM,CAoBxG"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=animated.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animated.test.d.ts","sourceRoot":"","sources":["../src/animated.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import { PixiNode } from './renderer';
2
+ /**
3
+ * Lift an existing imperative display class into a JSX component.
4
+ *
5
+ * The instance is constructed once (untracked, so constructor reads don't
6
+ * subscribe to anything) and every prop is then applied through the renderer's
7
+ * own `spread`, which gives them the same reactivity as an intrinsic element —
8
+ * including `ref`.
9
+ *
10
+ * ```tsx
11
+ * const Orbiter$ = asComponent(Orbiter);
12
+ * <Orbiter$ x={40} y={80} />
13
+ * ```
14
+ *
15
+ * @param Ctor - The display class to lift. Called as `new Ctor(defaults)`.
16
+ * @param defaults - Config handed to the constructor on every mount.
17
+ */
18
+ export declare function asComponent<TNode extends PixiNode, TConfig>(Ctor: new (config?: TConfig) => TNode, defaults?: TConfig): (props: Record<string, any>) => TNode;
19
+ //# sourceMappingURL=asComponent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asComponent.d.ts","sourceRoot":"","sources":["../src/asComponent.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,QAAQ,EAAE,OAAO,EACzD,IAAI,EAAE,KAAK,MAAM,CAAC,EAAE,OAAO,KAAK,KAAK,EACrC,QAAQ,CAAC,EAAE,OAAO,GACjB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,KAAK,CASvC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=asComponent.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asComponent.test.d.ts","sourceRoot":"","sources":["../src/asComponent.test.ts"],"names":[],"mappings":""}