paperlab 0.5.0 → 0.5.2

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/README.md CHANGED
@@ -144,7 +144,7 @@ And it is navigable rather than a video. It drifts on its own until you touch it
144
144
  - **Physics** — curated idle motion (`float`, `tumble`, `dangle`, `taped`, `breeze`) that composes with behaviors, and a verlet **cloth** mode: pin the top edge, add wind, grab the sheet and pull. Cloth and behaviors are mutually exclusive by schema — cloth owns the vertices.
145
145
  - **Interaction states** — a preset can carry `states`: overrides-on-base diffs keyed `rest` / `hover` / `pressed` / `picked` / `placed`, with the triggers built in. Drag a stamp past its threshold and it tears off its sheet (the perforation edges facing its neighbours flip to torn), release it over a `<DropZone>` and it settles, release it anywhere else and it flutters home. The whole flow is reachable from the keyboard: focus a paper, Enter picks, arrows move between zones, Enter places, Escape returns it.
146
146
  - **Hardware that holds the paper up** — thread to the ceiling or a rod across the top edge, gripped by a clip or a peg. A hung thing that shows what holds it stops reading as a rectangle that happens to float.
147
- - **Presets** — 15 paper presets and 6 stage presets, and everything serializes to `.paper` JSON validated by a zod schema. Diffable, forkable, shareable.
147
+ - **Presets** — 16 paper presets and 6 stage presets, and everything serializes to `.paper` JSON validated by a zod schema. Diffable, forkable, shareable.
148
148
  - **Agent-first export** — the editor's **Copy for AI** button produces a self-contained brief you paste into a coding agent: install line, inlined component, placement contract, and a verification step the agent can self-check. See [AGENTS.md](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md) and [docs/llms.txt](https://github.com/NourMtir0722/Paperlab/blob/main/docs/llms.txt).
149
149
  - **Accessible by default** — `prefers-reduced-motion` freezes behaviors at their pose and disables physics and entrances, a hidden DOM mirror carries the content for screen readers and find-in-page, and a flat DOM fallback renders when WebGL isn't available.
150
150
 
@@ -221,13 +221,15 @@ pnpm test # 700+ unit tests — deformer math, schema, cloth, layouts,
221
221
  pnpm test:parity # 37 golden-vector cases: every deformer's GLSL twin vs its JS twin
222
222
  pnpm test:drive # the stage really walks when you drag, wheel or arrow it
223
223
  pnpm test:share # sculpt → link → a browser that has never seen the paper
224
+ pnpm test:dropdown # every dropdown option is reachable, including below the fold
225
+ pnpm test:route # the site root sends each device to the app built for it
224
226
  pnpm typecheck
225
227
  pnpm lint
226
228
  pnpm knip # dead code and unused exports
227
229
  pnpm build
228
230
  ```
229
231
 
230
- Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All four test commands are CI gates, along with `publint` and `are-the-types-wrong` on the published package.
232
+ Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All six test commands are CI gates, along with `publint` and `are-the-types-wrong` on the published package.
231
233
 
232
234
  ### Measurement
233
235
 
@@ -5664,8 +5664,23 @@ registerLayout(sheet);
5664
5664
  // src/PaperField.tsx
5665
5665
  import * as THREE16 from "three";
5666
5666
  import { gsap as gsap5 } from "gsap";
5667
- import { Canvas, useFrame as useFrame5, useThree as useThree5 } from "@react-three/fiber";
5668
- import { forwardRef as forwardRef2, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef8 } from "react";
5667
+ import { Canvas, useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
5668
+ import { forwardRef as forwardRef2, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef8 } from "react";
5669
+
5670
+ // src/scene/release.tsx
5671
+ import { useThree as useThree4 } from "@react-three/fiber";
5672
+ import { useEffect as useEffect9 } from "react";
5673
+ function ReleaseContextOnUnmount() {
5674
+ const gl = useThree4((s) => s.gl);
5675
+ useEffect9(
5676
+ () => () => {
5677
+ gl.forceContextLoss();
5678
+ gl.dispose();
5679
+ },
5680
+ [gl]
5681
+ );
5682
+ return null;
5683
+ }
5669
5684
 
5670
5685
  // src/field/slots.ts
5671
5686
  var EMPTY_SET = /* @__PURE__ */ new Set();
@@ -5716,12 +5731,12 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
5716
5731
  import * as THREE13 from "three";
5717
5732
  import { gsap as gsap3 } from "gsap";
5718
5733
  import { useFrame as useFrame3 } from "@react-three/fiber";
5719
- import { useEffect as useEffect10, useMemo as useMemo6, useRef as useRef6 } from "react";
5734
+ import { useEffect as useEffect11, useMemo as useMemo6, useRef as useRef6 } from "react";
5720
5735
  import CustomShaderMaterial2 from "three-custom-shader-material";
5721
5736
 
5722
5737
  // src/content/atlas.ts
5723
5738
  import * as THREE12 from "three";
5724
- import { useEffect as useEffect9, useState as useState5 } from "react";
5739
+ import { useEffect as useEffect10, useState as useState5 } from "react";
5725
5740
  var MAX_ATLAS = 4096;
5726
5741
  function atlasGrid(count, aspect = 1) {
5727
5742
  const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
@@ -5730,7 +5745,7 @@ function atlasGrid(count, aspect = 1) {
5730
5745
  function useContentAtlas(contents, sheet2, stock) {
5731
5746
  const [atlas, setAtlas] = useState5(null);
5732
5747
  const stableContents = useStable(contents);
5733
- useEffect9(() => {
5748
+ useEffect10(() => {
5734
5749
  let disposed = false;
5735
5750
  const aspect = sheet2.height / sheet2.width;
5736
5751
  const { cols, rows } = atlasGrid(contents.length, aspect);
@@ -5982,7 +5997,7 @@ function FieldGroup({
5982
5997
  geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
5983
5998
  return geo;
5984
5999
  }, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
5985
- useEffect10(() => () => geometry.dispose(), [geometry]);
6000
+ useEffect11(() => () => geometry.dispose(), [geometry]);
5986
6001
  const atlas = useContentAtlas(contents, config.sheet, stock);
5987
6002
  const shader = useMemo6(() => {
5988
6003
  const composed = buildDisplacementGLSL(initialStack, config.sheet);
@@ -6013,19 +6028,19 @@ function FieldGroup({
6013
6028
  config.surface.showThrough,
6014
6029
  config.surface.translucency
6015
6030
  ]);
6016
- useEffect10(() => {
6031
+ useEffect11(() => {
6017
6032
  const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
6018
6033
  shader.uniforms.uTranslucency.value = values.translucency;
6019
6034
  shader.uniforms.uBackLightDir.value.copy(values.direction);
6020
6035
  shader.uniforms.uBackLightColor.value.copy(values.color);
6021
6036
  shader.uniforms.uAmbientTransmission.value = values.ambient;
6022
6037
  }, [shader, rig, config.surface.translucency, stock.translucency]);
6023
- useEffect10(() => {
6038
+ useEffect11(() => {
6024
6039
  if (!atlas) return;
6025
6040
  shader.uniforms.uAtlas.value = atlas.texture;
6026
6041
  shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
6027
6042
  }, [atlas, shader]);
6028
- useEffect10(() => {
6043
+ useEffect11(() => {
6029
6044
  if (!behavior || shared.reduced) return;
6030
6045
  const state = { p: progressRef.current };
6031
6046
  const tween = gsap3.to(state, {
@@ -6190,7 +6205,7 @@ var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
6190
6205
 
6191
6206
  // src/field/backingSheet.tsx
6192
6207
  import * as THREE14 from "three";
6193
- import { useEffect as useEffect11, useMemo as useMemo7 } from "react";
6208
+ import { useEffect as useEffect12, useMemo as useMemo7 } from "react";
6194
6209
 
6195
6210
  // src/content/backing.ts
6196
6211
  function silhouetteRects(o, count) {
@@ -6273,12 +6288,12 @@ function BackingSheet({
6273
6288
  }, [width, height]);
6274
6289
  const texture = useMemo7(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
6275
6290
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
6276
- useEffect11(() => {
6291
+ useEffect12(() => {
6277
6292
  if (!canvas || !texture) return;
6278
6293
  drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
6279
6294
  texture.needsUpdate = true;
6280
6295
  }, [canvas, texture, JSON.stringify(options), count, removedKey]);
6281
- useEffect11(() => () => texture?.dispose(), [texture]);
6296
+ useEffect12(() => () => texture?.dispose(), [texture]);
6282
6297
  return /* @__PURE__ */ jsxs4("mesh", { receiveShadow: true, children: [
6283
6298
  /* @__PURE__ */ jsx8("planeGeometry", { args: [width, height] }),
6284
6299
  /* @__PURE__ */ jsx8("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
@@ -6288,8 +6303,8 @@ function BackingSheet({
6288
6303
  // src/field/interactiveField.tsx
6289
6304
  import * as THREE15 from "three";
6290
6305
  import { gsap as gsap4 } from "gsap";
6291
- import { useFrame as useFrame4, useThree as useThree4 } from "@react-three/fiber";
6292
- import { useContext as useContext3, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
6306
+ import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
6307
+ import { useContext as useContext3, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
6293
6308
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
6294
6309
  var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
6295
6310
  function InteractiveField(props) {
@@ -6299,9 +6314,9 @@ function InteractiveField(props) {
6299
6314
  const reduced = usePrefersReducedMotion(props.reducedMotion);
6300
6315
  const contextRegistry = useContext3(DropZoneContext);
6301
6316
  const registry4 = useMemo8(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
6302
- const camera = useThree4((s) => s.camera);
6303
- const gl = useThree4((s) => s.gl);
6304
- const controls = useThree4((s) => s.controls);
6317
+ const camera = useThree5((s) => s.camera);
6318
+ const gl = useThree5((s) => s.gl);
6319
+ const controls = useThree5((s) => s.controls);
6305
6320
  const [removed, setRemoved] = useState6(EMPTY_SET);
6306
6321
  const [slotPatches, setSlotPatches] = useState6({});
6307
6322
  const [slotStates, setSlotStates] = useState6({});
@@ -6510,7 +6525,7 @@ function InteractiveField(props) {
6510
6525
  if (zone) settleInto(carried, zone);
6511
6526
  else returnHome(carried);
6512
6527
  };
6513
- useEffect12(() => {
6528
+ useEffect13(() => {
6514
6529
  const move = (e) => onPointerMoveRef.current(e);
6515
6530
  const up = (e) => onPointerUpRef.current(e);
6516
6531
  window.addEventListener("pointermove", move);
@@ -6583,7 +6598,7 @@ function InteractiveField(props) {
6583
6598
  zoneIds: () => zonesLive().map((z25) => z25.id),
6584
6599
  slotState: (slot) => slotStates[slot] ?? "rest"
6585
6600
  };
6586
- useEffect12(() => {
6601
+ useEffect13(() => {
6587
6602
  const ref = props.a11yRef;
6588
6603
  if (!ref) return;
6589
6604
  ref.current = {
@@ -6814,11 +6829,11 @@ var PaperFieldMesh = forwardRef2(
6814
6829
  const mountTimeRef = useRef8(-1);
6815
6830
  const morphRef = useRef8({ from: null, t: 1 });
6816
6831
  const prevLayout = useRef8({ id: layoutId, options: layoutOptions });
6817
- const gl = useThree5((s) => s.gl);
6832
+ const gl = useThree6((s) => s.gl);
6818
6833
  const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
6819
6834
  const speed = props.motion?.speed ?? 0.5;
6820
6835
  const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
6821
- useEffect13(() => {
6836
+ useEffect14(() => {
6822
6837
  const prev = prevLayout.current;
6823
6838
  if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
6824
6839
  morphRef.current = { from: prev, t: 0 };
@@ -6826,7 +6841,7 @@ var PaperFieldMesh = forwardRef2(
6826
6841
  prevLayout.current = { id: layoutId, options: layoutOptions };
6827
6842
  }
6828
6843
  }, [layoutId, layoutOptions]);
6829
- useEffect13(() => {
6844
+ useEffect14(() => {
6830
6845
  if (driver !== "drag") return;
6831
6846
  const el = gl.domElement;
6832
6847
  let lastX = null;
@@ -6910,9 +6925,9 @@ var PaperFieldMesh = forwardRef2(
6910
6925
  }
6911
6926
  );
6912
6927
  function FitCamera(meshProps) {
6913
- const camera = useThree5((s) => s.camera);
6914
- const width = useThree5((s) => s.size.width);
6915
- const height = useThree5((s) => s.size.height);
6928
+ const camera = useThree6((s) => s.camera);
6929
+ const width = useThree6((s) => s.size.width);
6930
+ const height = useThree6((s) => s.size.height);
6916
6931
  const papersProp = useStable(meshProps.papers ?? null);
6917
6932
  const imagesProp = useStable(meshProps.images ?? null);
6918
6933
  const presetProp = useStable(meshProps.preset ?? null);
@@ -6925,7 +6940,7 @@ function FitCamera(meshProps) {
6925
6940
  const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
6926
6941
  return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
6927
6942
  }, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
6928
- useEffect13(() => {
6943
+ useEffect14(() => {
6929
6944
  if (!(camera instanceof THREE16.PerspectiveCamera)) return;
6930
6945
  const { position, target } = fitCamera(
6931
6946
  field.layout,
@@ -6961,7 +6976,8 @@ var PaperField = forwardRef2(function PaperField2({ children, className, style,
6961
6976
  /* @__PURE__ */ jsx11(PaperBackdrop, { backdrop: rig.backdrop }),
6962
6977
  /* @__PURE__ */ jsx11(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
6963
6978
  /* @__PURE__ */ jsx11(PaperFieldMesh, { ref, a11yControllerRef: a11yRef, ...meshProps }),
6964
- children
6979
+ children,
6980
+ /* @__PURE__ */ jsx11(ReleaseContextOnUnmount, {})
6965
6981
  ] }),
6966
6982
  interactive && /* @__PURE__ */ jsx11(FieldKeyboardMirror, { papers, controller: a11yRef })
6967
6983
  ] }) });
@@ -7197,6 +7213,7 @@ export {
7197
7213
  cssColorOr,
7198
7214
  PaperLighting,
7199
7215
  PaperBackdrop,
7216
+ ReleaseContextOnUnmount,
7200
7217
  DropZone,
7201
7218
  sheetLayoutSchema,
7202
7219
  buildDisplacementGLSL,
@@ -7214,4 +7231,4 @@ export {
7214
7231
  describeConfig,
7215
7232
  buildAgentPayload
7216
7233
  };
7217
- //# sourceMappingURL=chunk-6IADJZX5.js.map
7234
+ //# sourceMappingURL=chunk-HRXQTJFS.js.map