diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4920f3563a..2abcfb0323 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,6 +4,8 @@ jobs:
checks:
env:
NODE_ENV: test
+ DATABASE_URL: postgres://
+ AUTH_SECRET: test
runs-on: ${{ matrix.os }}
strategy:
matrix:
diff --git a/app/shared/design-system/components/primitives/numeric-gesture-control.ts b/app/shared/design-system/components/primitives/numeric-gesture-control.ts
index 19c38312e9..ba71e5a8a4 100644
--- a/app/shared/design-system/components/primitives/numeric-gesture-control.ts
+++ b/app/shared/design-system/components/primitives/numeric-gesture-control.ts
@@ -28,6 +28,14 @@ type Options = {
}) => void;
};
+type State = {
+ value: number;
+ cursor?: SVGElement;
+ offset: number;
+ velocity: number;
+ direction: string;
+};
+
export const numericGestureControl = (
targetNode: HTMLInputElement,
{
@@ -36,68 +44,50 @@ export const numericGestureControl = (
onValueChange = () => null,
}: Options
) => {
- const { ownerDocument } = targetNode;
const eventNames = ["pointerup", "pointerdown", "pointermove"] as const;
- const state = {
+ const state: State = {
value: initialValue,
- cursor: cursorNode.cloneNode(true) as HTMLElement,
+ cursor: undefined,
offset: 0,
velocity: direction === "horizontal" ? 1 : -1,
- position: direction === "horizontal" ? "left" : "top",
- rotation: direction === "horizontal" ? "rotate(0deg)" : "rotate(90deg)",
- direction: direction === "horizontal" ? "ew-resize" : "ns-resize",
- dropShadow: `drop-shadow(${
- direction === "horizontal" ? "0 1px" : "1px 0"
- } 1.1px rgba(0,0,0,.4))`,
+ direction: direction,
+ };
+ const handleCursor = (
+ targetNode: HTMLElement | HTMLInputElement,
+ shouldSetCursor: boolean
+ ) => {
+ if (shouldSetCursor) {
+ targetNode.style.setProperty(
+ "cursor",
+ direction === "horizontal" ? "ew-resize" : "ns-resize"
+ );
+ } else {
+ targetNode.style.removeProperty("cursor");
+ }
};
const handleEvent = (event: PointerEvent) => {
- const { type, pressure, offsetX, offsetY, movementY, movementX } = event;
+ const { type, offsetX, offsetY, movementY, movementX } = event;
const offset = direction === "horizontal" ? offsetX : offsetY;
const movement = direction === "horizontal" ? movementX : -movementY;
switch (type) {
case "pointerup": {
- ownerDocument.exitPointerLock();
+ state.offset = 0;
+ handleCursor(targetNode.ownerDocument.documentElement, false);
+ exitPointerLock(state, event, targetNode);
break;
}
case "pointerdown": {
- const handlePointerLockEvent = () => {
- switch (ownerDocument.pointerLockElement) {
- case targetNode: {
- state.offset = offset;
- state.cursor.style.position = "absolute";
- state.cursor.style.top = `${offsetY}px`;
- state.cursor.style.left = `${offsetX}px`;
- state.cursor.style.transform = state.rotation;
- state.cursor.style.setProperty("--drop-shadow", state.dropShadow);
- ownerDocument.documentElement.append(state.cursor);
- break;
- }
- case null: {
- ownerDocument.removeEventListener(
- "pointerlockchange",
- handlePointerLockEvent
- );
- state.cursor.remove();
- break;
- }
- }
- };
- ownerDocument.addEventListener(
- "pointerlockchange",
- handlePointerLockEvent
- );
- targetNode.requestPointerLock();
+ state.offset = offset;
+ handleCursor(targetNode.ownerDocument.documentElement, true);
+ requestPointerLock(state, event, targetNode);
break;
}
case "pointermove": {
- if (pressure) {
+ if (state.offset) {
if (state.offset < 0) state.offset = globalThis.innerWidth + 1;
else if (state.offset > globalThis.innerWidth) state.offset = 1;
state.value += movement;
state.offset += movement * state.velocity;
- state.cursor.style[
- state.position as "top" | "left"
- ] = `${state.offset}px`;
onValueChange({
target: targetNode,
value: state.value,
@@ -108,13 +98,14 @@ export const numericGestureControl = (
}
}
};
- targetNode.style.setProperty("cursor", state.direction);
+
+ handleCursor(targetNode, true);
eventNames.forEach((eventName) =>
targetNode.addEventListener(eventName, handleEvent)
);
return {
disconnectedCallback: () => {
- targetNode.style.removeProperty("cursor");
+ handleCursor(targetNode, false);
eventNames.forEach((eventName) =>
targetNode.removeEventListener(eventName, handleEvent)
);
@@ -122,11 +113,65 @@ export const numericGestureControl = (
};
};
-const cursorNode = new Range().createContextualFragment(`
-
-`).firstElementChild as HTMLElement;
+const requestPointerLock = (
+ state: State,
+ event: PointerEvent,
+ targetNode: HTMLElement
+) => {
+ // The pointer lock api nukes the cursor on requestng a pointer lock,
+ // creating and managing the visual que of the cursor is thus left to the author
+ // we create and append an svg that serves as the visual que of where the cursor currently is
+ // taking into account horizontal/vertical orientation of the cursor itself, and update its position on move.
+ // we only use pointerLock api on chromium based browsers, because they feature an unobtrusive ux when activating it
+ // other browsers show a warning banner, making the use of it in this scenario subpar: in which case we fallback to using non-pointerLock means:
+ // albeit without an infinite cursor ux.
+ if (shouldUsePointerLock) {
+ targetNode.requestPointerLock();
+ const cursorNode = new Range().createContextualFragment(`
+ `).firstElementChild as SVGElement;
+ cursorNode.style.position = "absolute";
+ cursorNode.style.left = `${event.offsetX}px`;
+ cursorNode.style.top = `${event.offsetY}px`;
+ cursorNode.style.transform =
+ state.direction === "horizontal" ? "rotate(0deg)" : "rotate(90deg)";
+ state.cursor = cursorNode;
+ if (state.cursor) {
+ targetNode.ownerDocument.documentElement.append(state.cursor);
+ }
+ targetNode.onpointermove = () => {
+ if (state.cursor) {
+ state.cursor.style[
+ state.direction === "horizontal" ? "left" : "top"
+ ] = `${state.offset}px`;
+ }
+ };
+ } else {
+ targetNode.setPointerCapture(event.pointerId);
+ }
+};
+
+const exitPointerLock = (
+ state: State,
+ event: PointerEvent,
+ targetNode: HTMLElement
+) => {
+ if (shouldUsePointerLock) {
+ if (state.cursor) {
+ state.cursor.remove();
+ state.cursor = undefined;
+ }
+ targetNode.onpointermove = null;
+ targetNode.ownerDocument.exitPointerLock();
+ } else {
+ targetNode.releasePointerCapture(event.pointerId);
+ }
+};
+
+const shouldUsePointerLock = "chrome" in globalThis;
diff --git a/env-check.js b/env-check.js
new file mode 100644
index 0000000000..c39ecf1862
--- /dev/null
+++ b/env-check.js
@@ -0,0 +1,31 @@
+// @ts-check
+/**
+ * This file is included in `/remix.config.js` which ensures the app isn't built with invalid env vars.
+ * It has to be a `.js`-file to be imported there.
+ */
+
+/**
+ * Remix does not read .env files when building for production so this would make it to that when you tried to run yarn build locally
+ * you would get an error because your environment variables are in the .env file.
+ * `dot-env` makes it so that when you run this file the `process.env` is always populated with your `.env` file if it finds one,
+ * if not it will read the environment variables set by your hosting provider like remix does.
+ */
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+require("dotenv").config();
+
+const REQUIRED_ENVS = ["DATABASE_URL", "AUTH_SECRET"];
+
+const errors = [];
+
+REQUIRED_ENVS.map((env) => {
+ if (!process.env[env])
+ errors.push(`👉 The ${env} environment variable is required`);
+});
+
+if (errors.length) {
+ // eslint-disable-next-line no-console
+ console.error("❌ Invalid environment variables:");
+ // eslint-disable-next-line no-console
+ console.error(errors.join("\n"));
+ process.exit(1);
+}
diff --git a/remix.config.js b/remix.config.js
index fef8bb9e10..22eca3aaef 100644
--- a/remix.config.js
+++ b/remix.config.js
@@ -1,3 +1,5 @@
+require("./env-check");
+
/**
* @type {import('@remix-run/dev').AppConfig}
*/