From 77ef3c787ba72b28e2bd1031cb9a2481c0bd7b51 Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Fri, 1 Jul 2022 23:08:50 +0200 Subject: [PATCH 01/11] chore(env) - Add env check before starting app --- app/shared/env/env-check.js | 24 ++++++++++++++++++++++++ package.json | 3 ++- remix.config.js | 4 ++++ yarn.lock | 5 +++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 app/shared/env/env-check.js diff --git a/app/shared/env/env-check.js b/app/shared/env/env-check.js new file mode 100644 index 0000000000..6d4ff966ed --- /dev/null +++ b/app/shared/env/env-check.js @@ -0,0 +1,24 @@ +// @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. + */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { z } = require("zod"); + +const envSchema = z.object({ + DATABASE_URL: z.string().url(), + AUTH_SECRET: z.string().min(10), +}); + +const env = envSchema.safeParse(process.env); + +if (!env.success) { + // eslint-disable-next-line no-console + console.error( + "❌ Invalid environment variables:", + JSON.stringify(env.error.format(), null, 4) + ); + process.exit(1); +} +module.exports.env = env.data; diff --git a/package.json b/package.json index 8257c6b6b7..be370051f7 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "prettier": "^2.6.2", "prisma": "^3.14.0", "relative-deps": "^1.0.7", - "typescript": "4.6.4" + "typescript": "4.6.4", + "zod": "^3.17.3" }, "engines": { "node": ">=14 <=16" diff --git a/remix.config.js b/remix.config.js index aa3040c9de..d65207f6af 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { env } = require("./app/shared/env/env-check"); + /** * @type {import('@remix-run/dev').AppConfig} */ diff --git a/yarn.lock b/yarn.lock index c61b26d9b1..28d3530356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15898,6 +15898,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@^3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba" + integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg== + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From c70cf39f79aaeb2886ee300f08e4c528ddd76050 Mon Sep 17 00:00:00 2001 From: Sultan <105234327+natlusyht@users.noreply.github.com> Date: Sat, 2 Jul 2022 00:23:30 +0300 Subject: [PATCH 02/11] abstract requestPointerLock API within the abstraction decide whether to use requestPointerLock or setPointerCapture based on browser type. --- .../primitives/numeric-gesture-control.ts | 141 +++++++++++------- 1 file changed, 89 insertions(+), 52 deletions(-) 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..b4c3f75fd6 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 | null; + 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: null, 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,57 @@ export const numericGestureControl = ( }; }; -const cursorNode = new Range().createContextualFragment(` - - - - - - -`).firstElementChild as HTMLElement; +const requestPointerLock = ( + state: State, + event: PointerEvent, + targetNode: HTMLElement +) => { + targetNode.setPointerCapture(event.pointerId); + + 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)"; + cursorNode.style.setProperty( + "--drop-shadow", + `drop-shadow(${ + state.direction === "horizontal" ? "0 1px" : "1px 0" + } 1.1px rgba(0,0,0,.4))` + ); + state.cursor = cursorNode; + targetNode.ownerDocument.documentElement.append(state.cursor as Node); + targetNode.onpointermove = () => { + (state.cursor as SVGElement).style[ + state.direction === "horizontal" ? "left" : "top" + ] = `${state.offset}px`; + }; + } +}; + +const exitPointerLock = ( + state: State, + event: PointerEvent, + targetNode: HTMLElement +) => { + targetNode.releasePointerCapture(event.pointerId); + + if (shouldUsePointerLock()) { + (state.cursor as SVGElement).remove(); + targetNode.onpointermove = null; + targetNode.ownerDocument.exitPointerLock(); + } +}; + +const shouldUsePointerLock = () => { + return "chrome" in globalThis; +}; From 02c3cf4a39e3210a8c8917566c368d0696c982f0 Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 12:36:25 +0200 Subject: [PATCH 03/11] MR fixes; remove zod --- app/shared/env/env-check.js | 24 ------------------------ env-check.js | 25 +++++++++++++++++++++++++ package.json | 3 +-- remix.config.js | 2 +- yarn.lock | 5 ----- 5 files changed, 27 insertions(+), 32 deletions(-) delete mode 100644 app/shared/env/env-check.js create mode 100644 env-check.js diff --git a/app/shared/env/env-check.js b/app/shared/env/env-check.js deleted file mode 100644 index 6d4ff966ed..0000000000 --- a/app/shared/env/env-check.js +++ /dev/null @@ -1,24 +0,0 @@ -// @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. - */ -/* eslint-disable @typescript-eslint/no-var-requires */ -const { z } = require("zod"); - -const envSchema = z.object({ - DATABASE_URL: z.string().url(), - AUTH_SECRET: z.string().min(10), -}); - -const env = envSchema.safeParse(process.env); - -if (!env.success) { - // eslint-disable-next-line no-console - console.error( - "❌ Invalid environment variables:", - JSON.stringify(env.error.format(), null, 4) - ); - process.exit(1); -} -module.exports.env = env.data; diff --git a/env-check.js b/env-check.js new file mode 100644 index 0000000000..9274dd1e8d --- /dev/null +++ b/env-check.js @@ -0,0 +1,25 @@ +// @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. + */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +if (process.env.NODE_ENV !== "production") { + 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/package.json b/package.json index be370051f7..8257c6b6b7 100644 --- a/package.json +++ b/package.json @@ -132,8 +132,7 @@ "prettier": "^2.6.2", "prisma": "^3.14.0", "relative-deps": "^1.0.7", - "typescript": "4.6.4", - "zod": "^3.17.3" + "typescript": "4.6.4" }, "engines": { "node": ">=14 <=16" diff --git a/remix.config.js b/remix.config.js index d65207f6af..d59b7f996a 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -const { env } = require("./app/shared/env/env-check"); +require("./env-check"); /** * @type {import('@remix-run/dev').AppConfig} diff --git a/yarn.lock b/yarn.lock index 28d3530356..c61b26d9b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15898,11 +15898,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.17.3: - version "3.17.3" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba" - integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg== - zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From cce5c3f8b2e4072761df8e4f226832b04c76bd8b Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 12:39:45 +0200 Subject: [PATCH 04/11] add better comments --- env-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env-check.js b/env-check.js index 9274dd1e8d..cc5c117f4a 100644 --- a/env-check.js +++ b/env-check.js @@ -3,8 +3,8 @@ * 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. */ -/* eslint-disable @typescript-eslint/no-var-requires */ +// Remix does not read .env files when building for production if (process.env.NODE_ENV !== "production") { const REQUIRED_ENVS = ["DATABASE_URL", "AUTH_SECRET"]; From 543620e5db6a44a9badf26adad56741d3d5525ba Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 12:40:21 +0200 Subject: [PATCH 05/11] remove eslint disables --- remix.config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/remix.config.js b/remix.config.js index d59b7f996a..d04137991b 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars require("./env-check"); /** From 64214e1c1b396504eb0485ca9016f6dc7cd7c702 Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 15:24:52 +0200 Subject: [PATCH 06/11] require in prod too --- env-check.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/env-check.js b/env-check.js index cc5c117f4a..d35e21f4d7 100644 --- a/env-check.js +++ b/env-check.js @@ -5,21 +5,23 @@ */ // Remix does not read .env files when building for production -if (process.env.NODE_ENV !== "production") { - const REQUIRED_ENVS = ["DATABASE_URL", "AUTH_SECRET"]; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const dotenv = require("dotenv"); +dotenv.config(); - const errors = []; +const REQUIRED_ENVS = ["DATABASE_URL", "AUTH_SECRET"]; - REQUIRED_ENVS.map((env) => { - if (!process.env[env]) - errors.push(`👉 The ${env} environment variable is required`); - }); +const errors = []; - 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); - } +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); } From 79bead419cee77d7edf6b6277b47cd6010dace84 Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 15:27:34 +0200 Subject: [PATCH 07/11] fix actions --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From 3ad31f6e054e13d6f0c7b1d13fc383cec385fa54 Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 19:20:27 +0200 Subject: [PATCH 08/11] add coments --- env-check.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/env-check.js b/env-check.js index d35e21f4d7..0ed05d5901 100644 --- a/env-check.js +++ b/env-check.js @@ -4,10 +4,12 @@ * It has to be a `.js`-file to be imported there. */ -// Remix does not read .env files when building for production +// 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 -const dotenv = require("dotenv"); -dotenv.config(); +require("dotenv").config(); const REQUIRED_ENVS = ["DATABASE_URL", "AUTH_SECRET"]; From a0b814155837e7592e38781a2a3a3690d49733ed Mon Sep 17 00:00:00 2001 From: Sara Vieira Date: Sat, 2 Jul 2022 19:26:19 +0200 Subject: [PATCH 09/11] fix prettier --- env-check.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/env-check.js b/env-check.js index 0ed05d5901..c39ecf1862 100644 --- a/env-check.js +++ b/env-check.js @@ -4,10 +4,12 @@ * 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. +/** + * 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(); From cd71346a68e2fbe601806c709e259f7de2f381d4 Mon Sep 17 00:00:00 2001 From: Sultan <105234327+natlusyht@users.noreply.github.com> Date: Sun, 3 Jul 2022 12:45:40 +0300 Subject: [PATCH 10/11] contextual comment about implementation, code nits --- .../primitives/numeric-gesture-control.ts | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) 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 b4c3f75fd6..7fd010397c 100644 --- a/app/shared/design-system/components/primitives/numeric-gesture-control.ts +++ b/app/shared/design-system/components/primitives/numeric-gesture-control.ts @@ -30,7 +30,7 @@ type Options = { type State = { value: number; - cursor: SVGElement | null; + cursor: SVGElement | undefined; offset: number; velocity: number; direction: string; @@ -47,7 +47,7 @@ export const numericGestureControl = ( const eventNames = ["pointerup", "pointerdown", "pointermove"] as const; const state: State = { value: initialValue, - cursor: null, + cursor: undefined, offset: 0, velocity: direction === "horizontal" ? 1 : -1, direction: direction, @@ -118,12 +118,19 @@ const requestPointerLock = ( event: PointerEvent, targetNode: HTMLElement ) => { - targetNode.setPointerCapture(event.pointerId); - - if (shouldUsePointerLock()) { + // 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(` - + @@ -134,19 +141,19 @@ const requestPointerLock = ( cursorNode.style.top = `${event.offsetY}px`; cursorNode.style.transform = state.direction === "horizontal" ? "rotate(0deg)" : "rotate(90deg)"; - cursorNode.style.setProperty( - "--drop-shadow", - `drop-shadow(${ - state.direction === "horizontal" ? "0 1px" : "1px 0" - } 1.1px rgba(0,0,0,.4))` - ); state.cursor = cursorNode; - targetNode.ownerDocument.documentElement.append(state.cursor as Node); + if (state.cursor) { + targetNode.ownerDocument.documentElement.append(state.cursor); + } targetNode.onpointermove = () => { - (state.cursor as SVGElement).style[ - state.direction === "horizontal" ? "left" : "top" - ] = `${state.offset}px`; + if (state.cursor) { + state.cursor.style[ + state.direction === "horizontal" ? "left" : "top" + ] = `${state.offset}px`; + } }; + } else { + targetNode.setPointerCapture(event.pointerId); } }; @@ -155,15 +162,16 @@ const exitPointerLock = ( event: PointerEvent, targetNode: HTMLElement ) => { - targetNode.releasePointerCapture(event.pointerId); - - if (shouldUsePointerLock()) { - (state.cursor as SVGElement).remove(); + if (shouldUsePointerLock) { + if (state.cursor) { + state.cursor.remove(); + state.cursor = undefined; + } targetNode.onpointermove = null; targetNode.ownerDocument.exitPointerLock(); + } else { + targetNode.releasePointerCapture(event.pointerId); } }; -const shouldUsePointerLock = () => { - return "chrome" in globalThis; -}; +const shouldUsePointerLock = "chrome" in globalThis; From d314690c5efd2880278398980482aa6186e8a5ab Mon Sep 17 00:00:00 2001 From: Sultan <105234327+natlusyht@users.noreply.github.com> Date: Sun, 3 Jul 2022 14:56:48 +0300 Subject: [PATCH 11/11] nit: typescript undefined type --- .../components/primitives/numeric-gesture-control.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7fd010397c..ba71e5a8a4 100644 --- a/app/shared/design-system/components/primitives/numeric-gesture-control.ts +++ b/app/shared/design-system/components/primitives/numeric-gesture-control.ts @@ -30,7 +30,7 @@ type Options = { type State = { value: number; - cursor: SVGElement | undefined; + cursor?: SVGElement; offset: number; velocity: number; direction: string;