From 237242500648bdc9db79d0e828db506d543a1550 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Fri, 8 May 2026 18:03:46 -0400
Subject: [PATCH 1/2] Initial commit of new orientation primitive
---
.changeset/orientation-initial.md | 12 ++
packages/orientation/LICENSE | 21 +++
packages/orientation/README.md | 84 ++++++++++
packages/orientation/dev/index.tsx | 23 +++
packages/orientation/package.json | 67 ++++++++
packages/orientation/src/index.ts | 118 ++++++++++++++
packages/orientation/test/index.test.ts | 191 +++++++++++++++++++++++
packages/orientation/test/server.test.ts | 28 ++++
packages/orientation/tsconfig.json | 16 ++
pnpm-lock.yaml | 16 +-
10 files changed, 573 insertions(+), 3 deletions(-)
create mode 100644 .changeset/orientation-initial.md
create mode 100644 packages/orientation/LICENSE
create mode 100644 packages/orientation/README.md
create mode 100644 packages/orientation/dev/index.tsx
create mode 100644 packages/orientation/package.json
create mode 100644 packages/orientation/src/index.ts
create mode 100644 packages/orientation/test/index.test.ts
create mode 100644 packages/orientation/test/server.test.ts
create mode 100644 packages/orientation/tsconfig.json
diff --git a/.changeset/orientation-initial.md b/.changeset/orientation-initial.md
new file mode 100644
index 000000000..f0b543b81
--- /dev/null
+++ b/.changeset/orientation-initial.md
@@ -0,0 +1,12 @@
+---
+"@solid-primitives/orientation": minor
+---
+
+Add `@solid-primitives/orientation` package (Stage 0)
+
+New primitives for tracking screen orientation via the Screen Orientation API.
+
+- **`makeOrientation(onChange)`** — Non-reactive base primitive. Attaches a listener for `screen.orientation` `change` events (or the legacy `orientationchange` event as fallback) and returns a cleanup function. Does not fire on mount.
+- **`createOrientation()`** — Reactive primitive returning `angle` and `type` signal accessors, initialized to the current orientation and updated on every change. SSR-safe: returns static defaults (`angle: 0`, `type: "portrait-primary"`) on the server.
+
+Peer dependencies: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10`.
diff --git a/packages/orientation/LICENSE b/packages/orientation/LICENSE
new file mode 100644
index 000000000..38b41d975
--- /dev/null
+++ b/packages/orientation/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Solid Primitives Working Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/orientation/README.md b/packages/orientation/README.md
new file mode 100644
index 000000000..5c17c36ef
--- /dev/null
+++ b/packages/orientation/README.md
@@ -0,0 +1,84 @@
+
+
+
+
+# @solid-primitives/orientation
+
+[](https://bundlephobia.com/package/@solid-primitives/orientation)
+[](https://www.npmjs.com/package/@solid-primitives/orientation)
+[](https://github.com/solidjs-community/solid-primitives#contribution-process)
+
+Primitives for tracking screen orientation via the [Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API).
+
+- [`makeOrientation`](#makeorientation) — Non-reactive listener; attaches a callback for each orientation change and returns a cleanup function.
+- [`createOrientation`](#createorientation) — Reactive primitive; returns `angle` and `type` as signal accessors that update on orientation change.
+
+## Installation
+
+```bash
+npm install @solid-primitives/orientation
+# or
+yarn add @solid-primitives/orientation
+# or
+pnpm add @solid-primitives/orientation
+```
+
+## `makeOrientation`
+
+A non-reactive base primitive. Attaches a listener for screen orientation changes and returns a cleanup function. The callback fires on every subsequent change but **not** on mount — use `createOrientation` if you need the initial value reactively.
+
+Uses `screen.orientation` when available, falling back to the legacy `orientationchange` event on `window`.
+
+```ts
+import { makeOrientation } from "@solid-primitives/orientation";
+
+const cleanup = makeOrientation(({ angle, type }) => {
+ console.log(angle); // 0 | 90 | 180 | 270
+ console.log(type); // "portrait-primary" | "landscape-primary" | ...
+});
+
+// remove listener when done
+cleanup();
+```
+
+## `createOrientation`
+
+A reactive primitive that tracks the screen orientation. Returns `angle` and `type` signal accessors, initialized to the current orientation and updated on every change. Automatically removes the event listener on cleanup.
+
+On the server, returns static defaults: `angle: 0`, `type: "portrait-primary"`.
+
+```ts
+import { createOrientation } from "@solid-primitives/orientation";
+import { createEffect } from "solid-js";
+
+const { angle, type } = createOrientation();
+
+createEffect(() => {
+ console.log(angle()); // 0 | 90 | 180 | 270
+ console.log(type()); // "portrait-primary" | "landscape-primary" | ...
+});
+```
+
+## Types
+
+```ts
+export type OrientationType =
+ | "landscape-primary"
+ | "landscape-secondary"
+ | "portrait-primary"
+ | "portrait-secondary"
+ | "unknown";
+
+export interface OrientationState {
+ readonly angle: number;
+ readonly type: OrientationType;
+}
+```
+
+## Browser Support
+
+`screen.orientation` is supported in Chrome 38+, Firefox 43+, and Safari 16.4+. On older browsers the primitive falls back to the deprecated `window.orientationchange` event.
+
+## Changelog
+
+See [CHANGELOG.md](./CHANGELOG.md)
diff --git a/packages/orientation/dev/index.tsx b/packages/orientation/dev/index.tsx
new file mode 100644
index 000000000..e109f42a7
--- /dev/null
+++ b/packages/orientation/dev/index.tsx
@@ -0,0 +1,23 @@
+import { type Component } from "solid-js";
+import { createOrientation } from "../src/index.js";
+
+const App: Component = () => {
+ const { angle, type } = createOrientation();
+
+ return (
+
+
+
Screen Orientation
+
Rotate your device or resize to a narrow viewport to see changes.
+
+ Angle: {angle()}°
+
+
+ Type: {type()}
+
+
+
+ );
+};
+
+export default App;
diff --git a/packages/orientation/package.json b/packages/orientation/package.json
new file mode 100644
index 000000000..c21e69a13
--- /dev/null
+++ b/packages/orientation/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@solid-primitives/orientation",
+ "version": "0.0.100",
+ "description": "Primitives to track screen orientation using the Screen Orientation API",
+ "author": "David Di Biase ",
+ "contributors": [],
+ "license": "MIT",
+ "homepage": "https://primitives.solidjs.community/package/orientation",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/solidjs-community/solid-primitives.git"
+ },
+ "bugs": {
+ "url": "https://github.com/solidjs-community/solid-primitives/issues"
+ },
+ "primitive": {
+ "name": "orientation",
+ "stage": 0,
+ "list": [
+ "makeOrientation",
+ "createOrientation"
+ ],
+ "category": "Sensors"
+ },
+ "keywords": [
+ "solid",
+ "orientation",
+ "screen",
+ "device",
+ "primitives"
+ ],
+ "private": false,
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "type": "module",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "browser": {},
+ "exports": {
+ "import": {
+ "@solid-primitives/source": "./src/index.ts",
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "typesVersions": {},
+ "scripts": {
+ "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
+ "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
+ "vitest": "vitest -c ../../configs/vitest.config.ts",
+ "test": "pnpm run vitest",
+ "test:ssr": "pnpm run vitest --mode ssr"
+ },
+ "peerDependencies": {
+ "@solidjs/web": "^2.0.0-beta.10",
+ "solid-js": "^2.0.0-beta.10"
+ },
+ "dependencies": {
+ "@solid-primitives/utils": "workspace:^"
+ },
+ "devDependencies": {
+ "@solidjs/web": "2.0.0-beta.10",
+ "solid-js": "2.0.0-beta.10"
+ }
+}
diff --git a/packages/orientation/src/index.ts b/packages/orientation/src/index.ts
new file mode 100644
index 000000000..a57ced870
--- /dev/null
+++ b/packages/orientation/src/index.ts
@@ -0,0 +1,118 @@
+import { createSignal, onCleanup, type Accessor } from "solid-js";
+import { isServer } from "@solidjs/web";
+import { noop } from "@solid-primitives/utils";
+
+export type OrientationType =
+ | "landscape-primary"
+ | "landscape-secondary"
+ | "portrait-primary"
+ | "portrait-secondary"
+ | "unknown";
+
+export interface OrientationState {
+ readonly angle: number;
+ readonly type: OrientationType;
+}
+
+const DEFAULT_STATE: OrientationState = { angle: 0, type: "portrait-primary" };
+
+function angleToType(angle: number): OrientationType {
+ switch (angle) {
+ case 0:
+ return "portrait-primary";
+ case 180:
+ return "portrait-secondary";
+ case 90:
+ return "landscape-primary";
+ case -90:
+ case 270:
+ return "landscape-secondary";
+ default:
+ return "unknown";
+ }
+}
+
+function readOrientation(): OrientationState {
+ const orient = screen.orientation as ScreenOrientation | undefined;
+ if (orient) {
+ return { angle: orient.angle, type: orient.type as OrientationType };
+ }
+ const angle = ((window as any).orientation as number | undefined) ?? 0;
+ return { angle, type: angleToType(angle) };
+}
+
+/**
+ * Attaches a listener for screen orientation changes.
+ *
+ * Uses the [Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API)
+ * when available, falling back to the deprecated `orientationchange` event.
+ *
+ * @param onChange called with the new {@link OrientationState} on every orientation change
+ * @returns cleanup function to remove the listener
+ *
+ * @example
+ * ```ts
+ * const cleanup = makeOrientation(({ angle, type }) => {
+ * console.log(angle, type);
+ * });
+ * // remove listener
+ * cleanup();
+ * ```
+ */
+export function makeOrientation(onChange: (state: OrientationState) => void): VoidFunction {
+ if (isServer) return noop;
+
+ const handler = () => onChange(readOrientation());
+ const orient = screen.orientation as ScreenOrientation | undefined;
+
+ if (orient) {
+ orient.addEventListener("change", handler);
+ return () => orient.removeEventListener("change", handler);
+ }
+
+ window.addEventListener("orientationchange", handler);
+ return () => window.removeEventListener("orientationchange", handler);
+}
+
+/**
+ * Creates a reactive primitive tracking the screen orientation.
+ *
+ * Returns reactive signals for `angle` (degrees: 0, 90, 180, 270) and `type`
+ * (e.g. `"portrait-primary"`). On the server, returns static defaults
+ * (`angle: 0`, `type: "portrait-primary"`).
+ *
+ * @returns object with `angle` and `type` signal accessors
+ *
+ * @example
+ * ```ts
+ * const { angle, type } = createOrientation();
+ *
+ * createEffect(() => {
+ * console.log(angle(), type());
+ * });
+ * ```
+ */
+export function createOrientation(): {
+ angle: Accessor;
+ type: Accessor;
+} {
+ if (isServer) {
+ return {
+ angle: () => DEFAULT_STATE.angle,
+ type: () => DEFAULT_STATE.type,
+ };
+ }
+
+ const initial = readOrientation();
+ const [angle, setAngle] = createSignal(initial.angle);
+ const [type, setType] = createSignal(initial.type);
+
+ const cleanup = makeOrientation(state => {
+ setAngle(state.angle);
+ setType(state.type);
+ });
+
+ onCleanup(cleanup);
+
+ return { angle, type };
+}
diff --git a/packages/orientation/test/index.test.ts b/packages/orientation/test/index.test.ts
new file mode 100644
index 000000000..7f80b0631
--- /dev/null
+++ b/packages/orientation/test/index.test.ts
@@ -0,0 +1,191 @@
+import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
+import { createEffect, createRoot, flush } from "solid-js";
+import { makeOrientation, createOrientation, type OrientationState } from "../src/index.js";
+
+// Minimal mock for screen.orientation
+class MockScreenOrientation extends EventTarget {
+ angle = 0;
+ type = "portrait-primary";
+
+ simulate(angle: number, type: string) {
+ this.angle = angle;
+ this.type = type;
+ this.dispatchEvent(new Event("change"));
+ }
+}
+
+const mockOrientation = new MockScreenOrientation();
+
+beforeAll(() => {
+ Object.defineProperty(screen, "orientation", {
+ get: () => mockOrientation,
+ configurable: true,
+ });
+});
+
+afterAll(() => {
+ Object.defineProperty(screen, "orientation", {
+ get: () => undefined,
+ configurable: true,
+ });
+});
+
+beforeEach(() => {
+ mockOrientation.angle = 0;
+ mockOrientation.type = "portrait-primary";
+});
+
+describe("makeOrientation", () => {
+ test("calls onChange when orientation changes", () => {
+ const states: OrientationState[] = [];
+ const cleanup = makeOrientation(state => states.push({ ...state }));
+
+ mockOrientation.simulate(90, "landscape-primary");
+ expect(states).toHaveLength(1);
+ expect(states[0]).toEqual({ angle: 90, type: "landscape-primary" });
+
+ mockOrientation.simulate(0, "portrait-primary");
+ expect(states).toHaveLength(2);
+ expect(states[1]).toEqual({ angle: 0, type: "portrait-primary" });
+
+ cleanup();
+ });
+
+ test("does not fire before a change occurs", () => {
+ const states: OrientationState[] = [];
+ const cleanup = makeOrientation(state => states.push({ ...state }));
+
+ expect(states).toHaveLength(0);
+ cleanup();
+ });
+
+ test("cleanup removes event listener", () => {
+ const states: OrientationState[] = [];
+ const cleanup = makeOrientation(state => states.push({ ...state }));
+
+ cleanup();
+ mockOrientation.simulate(90, "landscape-primary");
+ expect(states).toHaveLength(0);
+ });
+
+ test("multiple listeners are independent", () => {
+ const a: number[] = [];
+ const b: number[] = [];
+
+ const cleanupA = makeOrientation(({ angle }) => a.push(angle));
+ const cleanupB = makeOrientation(({ angle }) => b.push(angle));
+
+ mockOrientation.simulate(90, "landscape-primary");
+ expect(a).toEqual([90]);
+ expect(b).toEqual([90]);
+
+ cleanupA();
+ mockOrientation.simulate(180, "portrait-secondary");
+ expect(a).toEqual([90]);
+ expect(b).toEqual([90, 180]);
+
+ cleanupB();
+ });
+});
+
+describe("createOrientation", () => {
+ test("returns current orientation as initial state", () => {
+ mockOrientation.angle = 270;
+ mockOrientation.type = "landscape-secondary";
+
+ createRoot(dispose => {
+ const { angle, type } = createOrientation();
+ expect(angle()).toBe(270);
+ expect(type()).toBe("landscape-secondary");
+ dispose();
+ });
+ });
+
+ test("updates signals when orientation changes", () => {
+ const { angle, type, dispose } = createRoot(dispose => {
+ const { angle, type } = createOrientation();
+ return { angle, type, dispose };
+ });
+
+ expect(angle()).toBe(0);
+ expect(type()).toBe("portrait-primary");
+
+ mockOrientation.simulate(90, "landscape-primary");
+ flush();
+ expect(angle()).toBe(90);
+ expect(type()).toBe("landscape-primary");
+
+ mockOrientation.simulate(180, "portrait-secondary");
+ flush();
+ expect(angle()).toBe(180);
+ expect(type()).toBe("portrait-secondary");
+
+ dispose();
+ });
+
+ test("stops updating after dispose", () => {
+ const { angle, type, dispose } = createRoot(dispose => {
+ const { angle, type } = createOrientation();
+ return { angle, type, dispose };
+ });
+
+ dispose();
+ mockOrientation.simulate(90, "landscape-primary");
+ flush();
+ expect(angle()).toBe(0);
+ expect(type()).toBe("portrait-primary");
+ });
+
+ test("tracks angle reactively via createEffect", () => {
+ const angles: number[] = [];
+
+ const dispose = createRoot(dispose => {
+ const { angle } = createOrientation();
+ createEffect(angle, (a: number) => {
+ angles.push(a);
+ });
+ return dispose;
+ });
+
+ flush(); // initial apply
+ mockOrientation.simulate(90, "landscape-primary");
+ flush(); // apply after change
+
+ expect(angles).toEqual([0, 90]);
+ dispose();
+ });
+
+ test("tracks type reactively via createEffect", () => {
+ const types: string[] = [];
+
+ const dispose = createRoot(dispose => {
+ const { type } = createOrientation();
+ createEffect(type, (t: string) => {
+ types.push(t);
+ });
+ return dispose;
+ });
+
+ flush(); // initial apply
+ mockOrientation.simulate(90, "landscape-primary");
+ flush(); // apply after change
+
+ expect(types).toEqual(["portrait-primary", "landscape-primary"]);
+ dispose();
+ });
+
+ test("multiple independent instances each update", () => {
+ const { a, b, dispose } = createRoot(dispose => {
+ const a = createOrientation();
+ const b = createOrientation();
+ return { a, b, dispose };
+ });
+
+ mockOrientation.simulate(270, "landscape-secondary");
+ flush();
+ expect(a.angle()).toBe(270);
+ expect(b.angle()).toBe(270);
+
+ dispose();
+ });
+});
diff --git a/packages/orientation/test/server.test.ts b/packages/orientation/test/server.test.ts
new file mode 100644
index 000000000..0aeebeb34
--- /dev/null
+++ b/packages/orientation/test/server.test.ts
@@ -0,0 +1,28 @@
+import { describe, test, expect } from "vitest";
+import { makeOrientation, createOrientation } from "../src/index.js";
+
+describe("makeOrientation (SSR)", () => {
+ test("returns a no-op cleanup without throwing", () => {
+ let called = false;
+ const cleanup = makeOrientation(() => {
+ called = true;
+ });
+ expect(typeof cleanup).toBe("function");
+ expect(called).toBe(false);
+ expect(() => cleanup()).not.toThrow();
+ });
+});
+
+describe("createOrientation (SSR)", () => {
+ test("returns static default state", () => {
+ const { angle, type } = createOrientation();
+ expect(angle()).toBe(0);
+ expect(type()).toBe("portrait-primary");
+ });
+
+ test("angle and type are callable functions", () => {
+ const { angle, type } = createOrientation();
+ expect(typeof angle).toBe("function");
+ expect(typeof type).toBe("function");
+ });
+});
diff --git a/packages/orientation/tsconfig.json b/packages/orientation/tsconfig.json
new file mode 100644
index 000000000..dc1970e16
--- /dev/null
+++ b/packages/orientation/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "references": [
+ {
+ "path": "../utils"
+ }
+ ],
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 55b312627..f99928e4a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -137,9 +137,6 @@ importers:
'@solid-primitives/event-listener':
specifier: workspace:^
version: link:../event-listener
- '@solid-primitives/static-store':
- specifier: workspace:^
- version: link:../static-store
'@solid-primitives/utils':
specifier: workspace:^
version: link:../utils
@@ -647,6 +644,19 @@ importers:
specifier: ^1.9.7
version: 1.9.7
+ packages/orientation:
+ dependencies:
+ '@solid-primitives/utils':
+ specifier: workspace:^
+ version: link:../utils
+ devDependencies:
+ '@solidjs/web':
+ specifier: 2.0.0-beta.10
+ version: 2.0.0-beta.10(solid-js@2.0.0-beta.10)
+ solid-js:
+ specifier: 2.0.0-beta.10
+ version: 2.0.0-beta.10
+
packages/page-visibility:
dependencies:
'@solid-primitives/event-listener':
From 81880539868814b1a85c5ec2e897a8f6814edf78 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Sun, 10 May 2026 09:33:00 -0400
Subject: [PATCH 2/2] Adapted the examples to use 2.0 effects
---
packages/orientation/README.md | 11 +++++++----
packages/orientation/src/index.ts | 9 ++++++---
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/packages/orientation/README.md b/packages/orientation/README.md
index 5c17c36ef..76cbeef26 100644
--- a/packages/orientation/README.md
+++ b/packages/orientation/README.md
@@ -53,10 +53,13 @@ import { createEffect } from "solid-js";
const { angle, type } = createOrientation();
-createEffect(() => {
- console.log(angle()); // 0 | 90 | 180 | 270
- console.log(type()); // "portrait-primary" | "landscape-primary" | ...
-});
+createEffect(
+ () => ({ angle: angle(), type: type() }),
+ ({ angle, type }) => {
+ console.log(angle); // 0 | 90 | 180 | 270
+ console.log(type); // "portrait-primary" | "landscape-primary" | ...
+ }
+);
```
## Types
diff --git a/packages/orientation/src/index.ts b/packages/orientation/src/index.ts
index a57ced870..d9bd3834f 100644
--- a/packages/orientation/src/index.ts
+++ b/packages/orientation/src/index.ts
@@ -87,9 +87,12 @@ export function makeOrientation(onChange: (state: OrientationState) => void): Vo
* ```ts
* const { angle, type } = createOrientation();
*
- * createEffect(() => {
- * console.log(angle(), type());
- * });
+ * createEffect(
+ * () => ({ angle: angle(), type: type() }),
+ * ({ angle, type }) => {
+ * console.log(angle, type);
+ * }
+ * );
* ```
*/
export function createOrientation(): {